├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── assets ├── .pixiproject ├── fonts │ ├── CozetteVector.ttf │ ├── fa-regular-400.ttf │ └── fa-solid-900.ttf ├── fox.png ├── fox_bg.png ├── palettes │ ├── apollo.hex │ ├── endesga-16.hex │ ├── endesga-32.hex │ ├── journey.hex │ ├── lospec500.hex │ ├── pear36.hex │ ├── pico-8.hex │ └── resurrect-64.hex ├── pixi.atlas ├── pixi.png ├── src │ ├── cursors.pixi │ └── misc.pixi └── themes │ ├── pixi_dark.json │ └── pixi_light.json ├── build.zig ├── build.zig.zon ├── contributor.md ├── readme.md └── src ├── App.zig ├── Assets.zig ├── Atlas.zig ├── File.zig ├── Layer.zig ├── Sprite.zig ├── algorithms ├── algorithms.zig └── brezenham.zig ├── deps ├── cgif │ ├── cgif.c │ ├── cgif_raw.c │ └── inc │ │ ├── cgif.h │ │ └── cgif_raw.h ├── nfd-zig │ ├── build.zig │ ├── nativefiledialog │ │ └── src │ │ │ ├── common.h │ │ │ ├── include │ │ │ └── nfd.h │ │ │ ├── nfd_cocoa.m │ │ │ ├── nfd_common.c │ │ │ ├── nfd_common.h │ │ │ ├── nfd_gtk.c │ │ │ ├── nfd_win.cpp │ │ │ ├── nfd_zenity.c │ │ │ └── simple_exec.h │ └── src │ │ ├── c.zig │ │ ├── demo.zig │ │ └── lib.zig └── zip │ ├── build.zig │ ├── src │ ├── miniz.h │ ├── zip.c │ └── zip.h │ └── zip.zig ├── editor ├── Colors.zig ├── Constants.zig ├── Editor.zig ├── Project.zig ├── Recents.zig ├── Settings.zig ├── Sidebar.zig ├── Theme.zig ├── Tools.zig ├── artboard │ ├── Artboard.zig │ ├── canvas.zig │ ├── canvas_pack.zig │ ├── flipbook │ │ ├── canvas.zig │ │ ├── flipbook.zig │ │ ├── menu.zig │ │ └── timeline.zig │ ├── infobar.zig │ ├── menu.zig │ └── rulers.zig ├── explorer │ ├── Explorer.zig │ ├── animations.zig │ ├── files.zig │ ├── keyframe_animations.zig │ ├── layers.zig │ ├── project.zig │ ├── settings.zig │ ├── sprites.zig │ └── tools.zig └── popups │ ├── Popups.zig │ ├── about.zig │ ├── animation.zig │ ├── file_confirm_close.zig │ ├── file_setup.zig │ ├── folder.zig │ ├── heightmap.zig │ ├── layer_setup.zig │ ├── print.zig │ ├── references.zig │ └── rename.zig ├── generated ├── atlas.zig └── paths.zig ├── gfx ├── Batcher.zig ├── Camera.zig ├── Quad.zig ├── Texture.zig └── gfx.zig ├── input ├── Hotkeys.zig ├── Mouse.zig └── input.zig ├── internal ├── Animation.zig ├── Atlas.zig ├── Buffers.zig ├── File.zig ├── Frame.zig ├── History.zig ├── Keyframe.zig ├── KeyframeAnimation.zig ├── Layer.zig ├── Palette.zig ├── Reference.zig └── Sprite.zig ├── math ├── color.zig ├── direction.zig ├── math.zig ├── rect.zig └── tween.zig ├── pixi.zig ├── shaders ├── compute.wgsl ├── default.wgsl └── shaders.zig └── tools ├── LDTKTileset.zig ├── Packer.zig ├── font_awesome.zig ├── fs.zig ├── gif.zig ├── process_assets.zig ├── quantize ├── dither.zig ├── fixed-stack.zig ├── kd-tree.zig ├── kdtree-benchmark.zig ├── median-cut.zig └── quantize.zig ├── timer.zig └── watcher ├── LinuxWatcher.zig ├── MacosWatcher.zig └── WindowsWatcher.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | assets/palettes/endesga-32.hex eol=crlf 2 | assets/palettes/resurrect-64.hex eol=crlf 3 | assets/palettes/apollo.hex eol=crlf 4 | assets/palettes/cc-29.hex eol=crlf 5 | assets/palettes/pico-8.hex eol=crlf 6 | assets/palettes/pear36.hex eol=crlf 7 | assets/palettes/journey.hex eol=crlf 8 | assets/palettes/lospec500.hex eol=crlf 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: foxnne 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: foxnne 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | paths-ignore: 5 | - "doc/**" 6 | - "README.md" 7 | - "**.md" 8 | - "LICENSE**" 9 | pull_request: 10 | paths-ignore: 11 | - "doc/**" 12 | - "README.md" 13 | - "**.md" 14 | - "LICENSE**" 15 | jobs: 16 | x86_64-linux: 17 | runs-on: ubuntu-latest 18 | # We want to run on external PRs, but not on our own internal PRs as they'll be run by the push 19 | # to the branch. 20 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 21 | env: 22 | DISPLAY: ':99.0' 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Setup Zig 27 | uses: mlugg/setup-zig@v1 28 | with: 29 | version: 2024.11.0-mach 30 | mirror: 'https://pkg.machengine.org/zig' 31 | - name: Update 32 | run: sudo apt-get update 33 | - name: Get GTK3 34 | run: sudo apt install libgtk-3-dev 35 | - name: Launch xvfb 36 | run: Xvfb :99 -screen 0 1680x720x24 > /dev/null 2>&1 & 37 | - name: Build 38 | run: zig build 39 | - name: Upload Artifacts 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: x86_64-linux 43 | path: zig-out/bin 44 | - name: x86_64-linux -> x86_64-windows 45 | run: zig build -Dtarget=x86_64-windows-gnu 46 | x86_64-windows: 47 | runs-on: windows-latest 48 | # We want to run on external PRs, but not on our own internal PRs as they'll be run by the push 49 | # to the branch. 50 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | - name: Setup Zig 55 | uses: mlugg/setup-zig@v1 56 | with: 57 | version: 2024.11.0-mach 58 | mirror: 'https://pkg.machengine.org/zig' 59 | - name: Build 60 | run: zig build 61 | - name: Upload Artifacts 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: x86_64-windows 65 | path: zig-out/bin 66 | arm64-macos: 67 | runs-on: macos-14 68 | # We want to run on external PRs, but not on our own internal PRs as they'll be run by the push 69 | # to the branch. 70 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 71 | steps: 72 | - name: Checkout 73 | uses: actions/checkout@v4 74 | - name: Setup Zig 75 | uses: mlugg/setup-zig@v1 76 | with: 77 | version: 2024.11.0-mach 78 | mirror: 'https://pkg.machengine.org/zig' 79 | - name: build 80 | run: zig build 81 | - name: Upload Artifacts 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: arm64-macos 85 | path: zig-out/bin 86 | 87 | # arm64-macos: 88 | # If it runs on 13 it should run on 14 as well 89 | # x86_64-macos: 90 | # runs-on: macos-13 91 | # # We want to run on external PRs, but not on our own internal PRs as they'll be run by the push 92 | # # to the branch. 93 | # if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 94 | # steps: 95 | # - name: Checkout 96 | # uses: actions/checkout@v4 97 | # - name: Setup Zig 98 | # uses: mlugg/setup-zig@v1 99 | # with: 100 | # version: 2024.11.0-mach 101 | # mirror: 'https://pkg.machengine.org/zig' 102 | # - name: build 103 | # run: zig build 104 | # - name: Upload Artifacts 105 | # uses: actions/upload-artifact@v4 106 | # with: 107 | # name: x86_64-macos 108 | # path: zig-out/bin 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-arm-cache 3 | zig-out/ 4 | buildrunner.zig 5 | .DS_Store 6 | imgui.ini 7 | recents.json 8 | .temp/ 9 | *.code-workspace 10 | 11 | .zig-cache/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug", 9 | "type": "lldb-mi", 10 | "request": "launch", 11 | "target": "./zig-out/bin/pixi", 12 | "cwd": "${workspaceRoot}", 13 | "valuesFormatting": "parseText" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.sublime_color_scheme": "jsonc", 4 | "chrono": "c" 5 | }, 6 | "zig.initialSetupDone": true 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "options": { 6 | "env": { 7 | "ZIG_SYSTEM_LINKER_HACK": "1", 8 | "MTL_SHADER_VALIDATION": "1", 9 | "MTL_SHADER_VALIDATION_GLOBAL_MEMORY": "1", 10 | "MTL_SHADER_VALIDATION_TEXTURE_USAGE": "1", 11 | "MTL_DEBUG_LAYER": "1", 12 | "METAL_DEVICE_WRAPPER_TYPE": "1", 13 | "MTL_DEBUG_LAYER_VALIDATE_LOAD_ACTIONS": "1", 14 | "MTL_DEBUG_LAYER_VALIDATE_UNRETAINED_RESOURCES": "0x4", 15 | }, 16 | }, 17 | "tasks": [ 18 | { 19 | "label": "Build Project", 20 | "type": "shell", 21 | "command": "zig build", 22 | "group": { 23 | "kind": "build", 24 | "isDefault": true 25 | }, 26 | "presentation": { 27 | "clear": true 28 | } 29 | }, 30 | { 31 | "label": "Build and Run Project", 32 | "type": "shell", 33 | "command": "zig build run", 34 | "group": { 35 | "kind": "build", 36 | "isDefault": true 37 | }, 38 | "presentation": { 39 | "clear": true 40 | } 41 | }, 42 | { 43 | "label": "Build and Run Project (release-fast)", 44 | "type": "shell", 45 | "command": "zig build run -Doptimize=ReleaseFast", 46 | "group": { 47 | "kind": "build", 48 | "isDefault": true 49 | }, 50 | "presentation": { 51 | "clear": true 52 | } 53 | }, 54 | { 55 | "label": "Build and Run Project (release-small)", 56 | "type": "shell", 57 | "command": "zig build run -Doptimize=ReleaseSmall", 58 | "group": { 59 | "kind": "build", 60 | "isDefault": true 61 | }, 62 | "presentation": { 63 | "clear": true 64 | } 65 | }, 66 | { 67 | "label": "Build and Run Project (release-safe)", 68 | "type": "shell", 69 | "command": "zig build run -Doptimize=ReleaseSafe", 70 | "group": { 71 | "kind": "build", 72 | "isDefault": true 73 | }, 74 | "presentation": { 75 | "clear": true 76 | } 77 | }, 78 | { 79 | "label": "Test Project", 80 | "type": "shell", 81 | "command": "zig build test", 82 | "group": "build", 83 | "presentation": { 84 | "clear": true, 85 | }, 86 | }, 87 | { 88 | "label": "Test File", 89 | "type": "shell", 90 | "command": "zig test ${file}", 91 | "presentation": { 92 | "clear": true 93 | }, 94 | "group": "build", 95 | }, 96 | { 97 | "label": "Process Assets", 98 | "type": "shell", 99 | "command": "zig build process-assets", 100 | "group": { 101 | "kind": "build", 102 | "isDefault": true 103 | }, 104 | "presentation": { 105 | "clear": true 106 | } 107 | }, 108 | ], 109 | "inputs": [ 110 | { 111 | "id": "zigTarget", 112 | "type": "command", 113 | "command": "zig.build.getTargets", 114 | }, 115 | { 116 | "id": "zigLastTarget", 117 | "type": "command", 118 | "command": "zig.build.getLastTargetOrPrompt" 119 | } 120 | ] 121 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colton Franklin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/.pixiproject: -------------------------------------------------------------------------------- 1 | {"packed_texture_output":"pixi.png","packed_heightmap_output":null,"packed_atlas_output":"pixi.atlas","generated_zig_output":"../generated/pixi.zig","pack_on_save":true} -------------------------------------------------------------------------------- /assets/fonts/CozetteVector.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnne/pixi/c0bdfa3c7cfbd73f46221ea446d0f029a09ffbef/assets/fonts/CozetteVector.ttf -------------------------------------------------------------------------------- /assets/fonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnne/pixi/c0bdfa3c7cfbd73f46221ea446d0f029a09ffbef/assets/fonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /assets/fonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnne/pixi/c0bdfa3c7cfbd73f46221ea446d0f029a09ffbef/assets/fonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /assets/fox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnne/pixi/c0bdfa3c7cfbd73f46221ea446d0f029a09ffbef/assets/fox.png -------------------------------------------------------------------------------- /assets/fox_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnne/pixi/c0bdfa3c7cfbd73f46221ea446d0f029a09ffbef/assets/fox_bg.png -------------------------------------------------------------------------------- /assets/palettes/apollo.hex: -------------------------------------------------------------------------------- 1 | 172038 2 | 253a5e 3 | 3c5e8b 4 | 4f8fba 5 | 73bed3 6 | a4dddb 7 | 19332d 8 | 25562e 9 | 468232 10 | 75a743 11 | a8ca58 12 | d0da91 13 | 4d2b32 14 | 7a4841 15 | ad7757 16 | c09473 17 | d7b594 18 | e7d5b3 19 | 341c27 20 | 602c2c 21 | 884b2b 22 | be772b 23 | de9e41 24 | e8c170 25 | 241527 26 | 411d31 27 | 752438 28 | a53030 29 | cf573c 30 | da863e 31 | 1e1d39 32 | 402751 33 | 7a367b 34 | a23e8c 35 | c65197 36 | df84a5 37 | 090a14 38 | 10141f 39 | 151d28 40 | 202e37 41 | 394a50 42 | 577277 43 | 819796 44 | a8b5b2 45 | c7cfcc 46 | ebede9 47 | -------------------------------------------------------------------------------- /assets/palettes/endesga-16.hex: -------------------------------------------------------------------------------- 1 | E4A672 2 | B86F50 3 | 743F39 4 | 3F2832 5 | 9E2835 6 | E53B44 7 | FB922B 8 | FFE762 9 | 63C64D 10 | 327345 11 | 193D3F 12 | 4F6781 13 | AFBFD2 14 | FFFFFF 15 | 2CE8F4 16 | 0484D1 17 | -------------------------------------------------------------------------------- /assets/palettes/endesga-32.hex: -------------------------------------------------------------------------------- 1 | be4a2f 2 | d77643 3 | ead4aa 4 | e4a672 5 | b86f50 6 | 733e39 7 | 3e2731 8 | a22633 9 | e43b44 10 | f77622 11 | feae34 12 | fee761 13 | 63c74d 14 | 3e8948 15 | 265c42 16 | 193c3e 17 | 124e89 18 | 0099db 19 | 2ce8f5 20 | ffffff 21 | c0cbdc 22 | 8b9bb4 23 | 5a6988 24 | 3a4466 25 | 262b44 26 | 181425 27 | ff0044 28 | 68386c 29 | b55088 30 | f6757a 31 | e8b796 32 | c28569 33 | -------------------------------------------------------------------------------- /assets/palettes/journey.hex: -------------------------------------------------------------------------------- 1 | 050914 2 | 110524 3 | 3b063a 4 | 691749 5 | 9c3247 6 | d46453 7 | f5a15d 8 | ffcf8e 9 | ff7a7d 10 | ff417d 11 | d61a88 12 | 94007a 13 | 42004e 14 | 220029 15 | 100726 16 | 25082c 17 | 3d1132 18 | 73263d 19 | bd4035 20 | ed7b39 21 | ffb84a 22 | fff540 23 | c6d831 24 | 77b02a 25 | 429058 26 | 2c645e 27 | 153c4a 28 | 052137 29 | 0e0421 30 | 0c0b42 31 | 032769 32 | 144491 33 | 488bd4 34 | 78d7ff 35 | b0fff1 36 | faffff 37 | c7d4e1 38 | 928fb8 39 | 5b537d 40 | 392946 41 | 24142c 42 | 0e0f2c 43 | 132243 44 | 1a466b 45 | 10908e 46 | 28c074 47 | 3dff6e 48 | f8ffb8 49 | f0c297 50 | cf968c 51 | 8f5765 52 | 52294b 53 | 0f022e 54 | 35003b 55 | 64004c 56 | 9b0e3e 57 | d41e3c 58 | ed4c40 59 | ff9757 60 | d4662f 61 | 9c341a 62 | 691b22 63 | 450c28 64 | 2d002e 65 | -------------------------------------------------------------------------------- /assets/palettes/lospec500.hex: -------------------------------------------------------------------------------- 1 | 10121c 2 | 2c1e31 3 | 6b2643 4 | ac2847 5 | ec273f 6 | 94493a 7 | de5d3a 8 | e98537 9 | f3a833 10 | 4d3533 11 | 6e4c30 12 | a26d3f 13 | ce9248 14 | dab163 15 | e8d282 16 | f7f3b7 17 | 1e4044 18 | 006554 19 | 26854c 20 | 5ab552 21 | 9de64e 22 | 008b8b 23 | 62a477 24 | a6cb96 25 | d3eed3 26 | 3e3b65 27 | 3859b3 28 | 3388de 29 | 36c5f4 30 | 6dead6 31 | 5e5b8c 32 | 8c78a5 33 | b0a7b8 34 | deceed 35 | 9a4d76 36 | c878af 37 | cc99ff 38 | fa6e79 39 | ffa2ac 40 | ffd1d5 41 | f6e8e0 42 | ffffff 43 | -------------------------------------------------------------------------------- /assets/palettes/pear36.hex: -------------------------------------------------------------------------------- 1 | 5e315b 2 | 8c3f5d 3 | ba6156 4 | f2a65e 5 | ffe478 6 | cfff70 7 | 8fde5d 8 | 3ca370 9 | 3d6e70 10 | 323e4f 11 | 322947 12 | 473b78 13 | 4b5bab 14 | 4da6ff 15 | 66ffe3 16 | ffffeb 17 | c2c2d1 18 | 7e7e8f 19 | 606070 20 | 43434f 21 | 272736 22 | 3e2347 23 | 57294b 24 | 964253 25 | e36956 26 | ffb570 27 | ff9166 28 | eb564b 29 | b0305c 30 | 73275c 31 | 422445 32 | 5a265e 33 | 80366b 34 | bd4882 35 | ff6b97 36 | ffb5b5 37 | -------------------------------------------------------------------------------- /assets/palettes/pico-8.hex: -------------------------------------------------------------------------------- 1 | 000000 2 | 1D2B53 3 | 7E2553 4 | 008751 5 | AB5236 6 | 5F574F 7 | C2C3C7 8 | FFF1E8 9 | FF004D 10 | FFA300 11 | FFEC27 12 | 00E436 13 | 29ADFF 14 | 83769C 15 | FF77A8 16 | FFCCAA 17 | -------------------------------------------------------------------------------- /assets/palettes/resurrect-64.hex: -------------------------------------------------------------------------------- 1 | 2e222f 2 | 3e3546 3 | 625565 4 | 966c6c 5 | ab947a 6 | 694f62 7 | 7f708a 8 | 9babb2 9 | c7dcd0 10 | ffffff 11 | 6e2727 12 | b33831 13 | ea4f36 14 | f57d4a 15 | ae2334 16 | e83b3b 17 | fb6b1d 18 | f79617 19 | f9c22b 20 | 7a3045 21 | 9e4539 22 | cd683d 23 | e6904e 24 | fbb954 25 | 4c3e24 26 | 676633 27 | a2a947 28 | d5e04b 29 | fbff86 30 | 165a4c 31 | 239063 32 | 1ebc73 33 | 91db69 34 | cddf6c 35 | 313638 36 | 374e4a 37 | 547e64 38 | 92a984 39 | b2ba90 40 | 0b5e65 41 | 0b8a8f 42 | 0eaf9b 43 | 30e1b9 44 | 8ff8e2 45 | 323353 46 | 484a77 47 | 4d65b4 48 | 4d9be6 49 | 8fd3ff 50 | 45293f 51 | 6b3e75 52 | 905ea9 53 | a884f3 54 | eaaded 55 | 753c54 56 | a24b6f 57 | cf657f 58 | ed8099 59 | 831c5d 60 | c32454 61 | f04f78 62 | f68181 63 | fca790 64 | fdcbb0 65 | -------------------------------------------------------------------------------- /assets/pixi.atlas: -------------------------------------------------------------------------------- 1 | {"sprites":[{"source":[0,0,22,22],"origin":[0,22]},{"source":[22,0,22,22],"origin":[0,22]},{"source":[158,0,15,16],"origin":[0,14]},{"source":[110,0,21,21],"origin":[0,21]},{"source":[44,0,22,22],"origin":[-1,23]},{"source":[66,0,22,22],"origin":[-1,23]},{"source":[88,0,22,22],"origin":[-1,23]},{"source":[131,0,27,18],"origin":[3,20]},{"source":[176,0,2,2],"origin":[0,0]},{"source":[204,0,2,2],"origin":[0,0]},{"source":[178,0,2,2],"origin":[0,0]},{"source":[180,0,2,2],"origin":[0,0]},{"source":[182,0,2,2],"origin":[0,0]},{"source":[184,0,2,2],"origin":[0,0]},{"source":[186,0,2,2],"origin":[0,0]},{"source":[188,0,2,2],"origin":[0,0]},{"source":[190,0,2,2],"origin":[0,0]},{"source":[192,0,2,2],"origin":[0,0]},{"source":[194,0,2,2],"origin":[0,0]},{"source":[196,0,2,2],"origin":[0,0]},{"source":[198,0,2,2],"origin":[0,0]},{"source":[200,0,2,2],"origin":[0,0]},{"source":[202,0,2,2],"origin":[0,0]},{"source":[173,0,3,5],"origin":[-15,-14]}],"animations":[{"name":"pencil_default","start":0,"length":1,"fps":1},{"name":"eraser_default","start":1,"length":1,"fps":1},{"name":"bucket_default","start":2,"length":1,"fps":1},{"name":"dropper_default","start":3,"length":1,"fps":1},{"name":"selection_default","start":4,"length":1,"fps":1},{"name":"selection_add_default","start":5,"length":1,"fps":1},{"name":"selection_rem_default","start":6,"length":1,"fps":1},{"name":"fox_default","start":7,"length":16,"fps":8},{"name":"logo_default","start":23,"length":1,"fps":1}]} -------------------------------------------------------------------------------- /assets/pixi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnne/pixi/c0bdfa3c7cfbd73f46221ea446d0f029a09ffbef/assets/pixi.png -------------------------------------------------------------------------------- /assets/src/cursors.pixi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnne/pixi/c0bdfa3c7cfbd73f46221ea446d0f029a09ffbef/assets/src/cursors.pixi -------------------------------------------------------------------------------- /assets/src/misc.pixi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnne/pixi/c0bdfa3c7cfbd73f46221ea446d0f029a09ffbef/assets/src/misc.pixi -------------------------------------------------------------------------------- /assets/themes/pixi_dark.json: -------------------------------------------------------------------------------- 1 | {"background":{"value":[1.3333334028720856e-01,1.3725490868091583e-01,1.6470588743686676e-01,1.0e+00]},"foreground":{"value":[1.6470588743686676e-01,1.725490242242813e-01,2.1176470816135406e-01,1.0e+00]},"text":{"value":[9.019607901573181e-01,6.86274528503418e-01,5.372549295425415e-01,1.0e+00]},"text_secondary":{"value":[6.217507719993591e-01,6.217508316040039e-01,6.89373254776001e-01,1.0e+00]},"text_background":{"value":[3.803131878376007e-01,3.803131878376007e-01,4.1416895389556885e-01,1.0e+00]},"text_blue":{"value":[4.313725531101227e-01,5.882353186607361e-01,7.843137383460999e-01,1.0e+00]},"text_orange":{"value":[7.176470756530762e-01,4.431372582912445e-01,3.764705955982208e-01,1.0e+00]},"text_yellow":{"value":[8.392156958580017e-01,7.803921699523926e-01,5.098039507865906e-01,1.0e+00]},"text_red":{"value":[8.078431487083435e-01,4.7058823704719543e-01,4.117647111415863e-01,1.0e+00]},"highlight_primary":{"value":[1.8431372940540314e-01,7.019608020782471e-01,5.29411792755127e-01,1.0e+00]},"hover_primary":{"value":[2.980392277240753e-01,5.803921818733215e-01,4.8235294222831726e-01,1.0e+00]},"highlight_secondary":{"value":[2.980392277240753e-01,1.882352977991104e-01,2.6274511218070984e-01,1.0e+00]},"hover_secondary":{"value":[4.117647111415863e-01,1.9607843458652496e-01,2.666666805744171e-01,1.0e+00]},"checkerboard_primary":{"value":[5.882353186607361e-01,5.882353186607361e-01,5.882353186607361e-01,1.0e+00]},"checkerboard_secondary":{"value":[2.1568627655506134e-01,2.1568627655506134e-01,2.1568627655506134e-01,1.0e+00]},"modal_dim":{"value":[0.0e+00,0.0e+00,0.0e+00,1.882352977991104e-01]}} -------------------------------------------------------------------------------- /assets/themes/pixi_light.json: -------------------------------------------------------------------------------- 1 | {"background":{"value":[7.956403493881226e-01,7.956403493881226e-01,7.956403493881226e-01,1.0e+00]},"foreground":{"value":[8.747252821922302e-01,8.747252821922302e-01,8.747252821922302e-01,1.0e+00]},"text":{"value":[3.0109888315200806e-01,3.0109888315200806e-01,3.0109888315200806e-01,1.0e+00]},"text_secondary":{"value":[5.28610348701477e-01,5.041243433952332e-01,5.05725622177124e-01,1.0e+00]},"text_background":{"value":[6.021798253059387e-01,5.545961260795593e-01,5.545961260795593e-01,1.0e+00]},"text_blue":{"value":[4.313725531101227e-01,5.882353186607361e-01,7.843137383460999e-01,1.0e+00]},"text_orange":{"value":[7.176470756530762e-01,4.431372582912445e-01,3.764705955982208e-01,1.0e+00]},"text_yellow":{"value":[8.392156958580017e-01,7.803921699523926e-01,5.098039507865906e-01,1.0e+00]},"text_red":{"value":[8.078431487083435e-01,4.7058823704719543e-01,4.117647111415863e-01,1.0e+00]},"highlight_primary":{"value":[9.340659379959106e-01,6.199734210968018e-01,6.821017265319824e-01,1.0e+00]},"hover_primary":{"value":[9.208791255950928e-01,7.103924751281738e-01,7.103924751281738e-01,1.0e+00]},"highlight_secondary":{"value":[7.91208803653717e-01,7.877309918403625e-01,7.903909087181091e-01,1.0e+00]},"hover_secondary":{"value":[8.901098966598511e-01,7.688202261924744e-01,7.688202261924744e-01,1.0e+00]},"checkerboard_primary":{"value":[5.882353186607361e-01,5.882353186607361e-01,5.882353186607361e-01,1.0e+00]},"checkerboard_secondary":{"value":[2.1568627655506134e-01,2.1568627655506134e-01,2.1568627655506134e-01,1.0e+00]},"modal_dim":{"value":[0.0e+00,0.0e+00,0.0e+00,1.882352977991104e-01]}} -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const mach = @import("mach"); 5 | 6 | const nfd = @import("src/deps/nfd-zig/build.zig"); 7 | const zip = @import("src/deps/zip/build.zig"); 8 | 9 | const content_dir = "assets/"; 10 | 11 | const ProcessAssetsStep = @import("src/tools/process_assets.zig"); 12 | 13 | pub fn build(b: *std.Build) !void { 14 | const target = b.standardTargetOptions(.{}); 15 | const optimize = b.standardOptimizeOption(.{}); 16 | 17 | // Create our pixi module, where our Modules declaration lives 18 | const pixi_mod = b.createModule(.{ 19 | .root_source_file = b.path("src/pixi.zig"), 20 | .optimize = optimize, 21 | .target = target, 22 | }); 23 | 24 | const zstbi = b.dependency("zstbi", .{ .target = target, .optimize = optimize }); 25 | const zmath = b.dependency("zmath", .{ .target = target, .optimize = optimize }); 26 | 27 | const zip_pkg = zip.package(b, .{}); 28 | 29 | // Add mach import to our app. 30 | const mach_dep = b.dependency("mach", .{ 31 | .target = target, 32 | .optimize = optimize, 33 | }); 34 | 35 | const zig_imgui_dep = b.dependency("zig_imgui", .{ .target = target, .optimize = optimize }); 36 | 37 | const imgui_module = b.addModule("zig-imgui", .{ 38 | .root_source_file = zig_imgui_dep.path("src/imgui.zig"), 39 | .imports = &.{ 40 | .{ .name = "mach", .module = mach_dep.module("mach") }, 41 | }, 42 | }); 43 | 44 | const timerModule = b.addModule("timer", .{ .root_source_file = .{ .cwd_relative = "src/tools/timer.zig" } }); 45 | 46 | // quantization library 47 | const quantizeLib = b.addStaticLibrary(.{ 48 | .name = "quantize", 49 | .root_source_file = .{ .cwd_relative = "src/tools/quantize/quantize.zig" }, 50 | .target = target, 51 | .optimize = optimize, 52 | }); 53 | addImport(quantizeLib, "timer", timerModule); 54 | const quantizeModule = quantizeLib.root_module; 55 | 56 | // zgif library 57 | const zgifLibrary = b.addStaticLibrary(.{ 58 | .name = "zgif", 59 | .root_source_file = .{ .cwd_relative = "src/tools/gif.zig" }, 60 | .target = target, 61 | .optimize = optimize, 62 | }); 63 | addCGif(b, zgifLibrary); 64 | addImport(zgifLibrary, "quantize", quantizeModule); 65 | const zgif_module = zgifLibrary.root_module; 66 | zgif_module.addImport("zstbi", zstbi.module("root")); 67 | // Have Mach create the executable for us 68 | // The mod we pass as .app must contain the Modules definition 69 | // And the Modules must include an App containing the main schedule 70 | const exe = mach.addExecutable(mach_dep.builder, .{ 71 | .name = "Pixi", 72 | .app = pixi_mod, 73 | .target = target, 74 | .optimize = optimize, 75 | }); 76 | b.installArtifact(exe); 77 | 78 | if (optimize != .Debug) { 79 | switch (target.result.os.tag) { 80 | .windows => exe.subsystem = .Windows, 81 | else => exe.subsystem = .Posix, 82 | } 83 | } 84 | 85 | const run_cmd = b.addRunArtifact(exe); 86 | const run_step = b.step("run", "Run the example"); 87 | 88 | pixi_mod.addImport("mach", mach_dep.module("mach")); 89 | pixi_mod.addImport("zstbi", zstbi.module("root")); 90 | pixi_mod.addImport("zmath", zmath.module("root")); 91 | pixi_mod.addImport("nfd", nfd.getModule(b)); 92 | pixi_mod.addImport("zip", zip_pkg.module); 93 | pixi_mod.addImport("zig-imgui", imgui_module); 94 | pixi_mod.addImport("zgif", zgif_module); 95 | 96 | const nfd_lib = nfd.makeLib(b, target, optimize); 97 | pixi_mod.addImport("nfd", nfd_lib); 98 | 99 | if (target.result.isDarwin()) { 100 | // // MacOS: this must be defined for macOS 13.3 and older. 101 | // // Critically, this MUST NOT be included as a -D__kernel_ptr_semantics flag. If it is, 102 | // // then this macro will not be defined even if `defineCMacro` was also called! 103 | //nfd_lib.addCMacro("__kernel_ptr_semantics", ""); 104 | //mach.addPaths(nfd_lib); 105 | if (mach_dep.builder.lazyDependency("xcode_frameworks", .{})) |dep| { 106 | nfd_lib.addSystemIncludePath(dep.path("include")); 107 | } 108 | } 109 | 110 | exe.linkLibCpp(); 111 | 112 | exe.linkLibrary(zig_imgui_dep.artifact("imgui")); 113 | exe.linkLibrary(zstbi.artifact("zstbi")); 114 | zip.link(exe); 115 | 116 | const assets = try ProcessAssetsStep.init(b, "assets", "src/generated/"); 117 | var process_assets_step = b.step("process-assets", "generates struct for all assets"); 118 | process_assets_step.dependOn(&assets.step); 119 | exe.step.dependOn(process_assets_step); 120 | 121 | const install_content_step = b.addInstallDirectory(.{ 122 | .source_dir = .{ .cwd_relative = thisDir() ++ "/" ++ content_dir }, 123 | .install_dir = .{ .custom = "" }, 124 | .install_subdir = "bin/" ++ content_dir, 125 | }); 126 | exe.step.dependOn(&install_content_step.step); 127 | 128 | const installArtifact = b.addInstallArtifact(exe, .{}); 129 | run_cmd.step.dependOn(&installArtifact.step); 130 | run_step.dependOn(&run_cmd.step); 131 | b.getInstallStep().dependOn(&installArtifact.step); 132 | } 133 | 134 | inline fn thisDir() []const u8 { 135 | return comptime std.fs.path.dirname(@src().file) orelse "."; 136 | } 137 | 138 | fn addImport( 139 | compile: *std.Build.Step.Compile, 140 | name: [:0]const u8, 141 | module: *std.Build.Module, 142 | ) void { 143 | compile.root_module.addImport(name, module); 144 | } 145 | 146 | fn addCGif(b: *std.Build, compile: *std.Build.Step.Compile) void { 147 | compile.addIncludePath(std.Build.path(b, "src/deps/cgif/inc")); 148 | compile.addCSourceFile(.{ .file = std.Build.path(b, "src/deps/cgif/cgif.c") }); 149 | compile.addCSourceFile(.{ .file = std.Build.path(b, "src/deps/cgif/cgif_raw.c") }); 150 | } 151 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .paths = .{ 3 | "src", 4 | "build.zig", 5 | "build.zig.zon", 6 | "assets", 7 | }, 8 | .name = "pixi", 9 | .version = "0.0.2", 10 | .mach_zig_version = "2024.11.0-mach", 11 | .dependencies = .{ 12 | .mach = .{ 13 | //.path = "src/.temp/mach-dev/", 14 | .url = "https://github.com/foxnne/mach/archive/24e6db38d0e4274367ff2595aa8199e0f0157875.tar.gz", 15 | .hash = "12200958b6ba6cef9104aba34e6968f89b3d27b3e988c7f52aca7cb08ea0e5f118be", 16 | }, 17 | .zig_imgui = .{ 18 | //.path = "src/.temp/zig-imgui/", 19 | .url = "https://github.com/foxnne/zig-imgui/archive/5aa9281b29a61db91f4db29cdaf158321a91beb4.tar.gz", 20 | .hash = "1220630398f144a39075c8574b0ff9915dbfb64a9398af05f334fab3c11108ebd42f", 21 | }, 22 | .zstbi = .{ 23 | .url = "https://github.com/foxnne/zstbi/archive/d9a0947365b1ee8131fcf518feac8dfe896cfcfa.tar.gz", 24 | .hash = "1220ce381d38f9ce6db428405b7884d139cab8173d3bf7d950582991d941c868423a", 25 | }, 26 | .zmath = .{ 27 | .url = "https://github.com/foxnne/zmath/archive/9620a611a8c039711dc780bf296d8dc100d16d3a.tar.gz", 28 | .hash = "12201b9f28adb918c2ffaf75b57a4b714a864038f8ba3e2d9f730187d0a852b952e1", 29 | }, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /contributor.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | ## Contributing 7 | 8 | Hello and thank you so much for considering contributing to Pixi! 9 | 10 | By suggestion, this document will hopefully serve as a good starting point for understanding Pixi's internals and where things are. However, if you ever have any questions or would like 11 | to have a conversation about Pixi, please reach out to me on discord or add an issue. I'm "foxnne" on discord as well. 12 | 13 | ### Overview 14 | 15 | Pixi is built using several game development libraries by others in the Zig community, as well as a C library for handling zipped files. The dependencies are as follows: 16 | - ***mach-core***: Handles windowing and input, and uses the new zig package manager. This library and dependencies will be downloaded to the cache on build. 17 | - ***nfd_zig***: Native file dialogs wrapper, copied into the src/deps folder. 18 | - ***zgui***: Wrapper for Dear Imgui, which is copied into the src/deps/zig-gamedev folder. 19 | - ***zmath***: Math library, primarily using this for vector math and matrices. As above, this is copied into the src/deps/zig-gamedev folder. 20 | - ***zstbi***: Wrapper for stbi provided by zig-gamedev. This handles loading and resizing images. As above, this is copied into the src/deps/zig-gamedev folder. 21 | - ***zip***: Wrapper for the zip library, copied into the src/deps folder. 22 | 23 | Outside of the `src` folder, we have `assets` which contain all assets that we would like to be copied over next to the executable and used by Pixi at runtime. 24 | 25 | `pixi.zig` holds all the main loop information and init, update, and deinit functions. Mach-core handles the main entry point and calls these functions for us. Mach-core is multi-threaded in the sense that there are two update loops, one which is run on the main thread, and one that runs in a separate thread. For more information about mach-core please see [the mach-core website](https://machengine.org/core/). 26 | 27 | Please note that we need to handle native file dialogs from the main thread, which is currently how Pixi handles it. I tried to set this up as a request/response. 28 | 29 | Inside of the `src` folder we have several subfolders. I tried to organize the project based on a few categories as follows: 30 | 31 | Outside of these subfolders, please note that `assets.zig` is generated so don't edit this file. 32 | 33 | - **algorithms**: This folder holds any generalized algorithms for use in pixel art operations. As of writing this, it only currently contains the brezenham algorithm used 34 | by the stroke/pencil tool. This algorithm handles quick mouse movements when drawing and prevents broken lines, as each frame a line is drawn from the previous frame. 35 | 36 | - **deps**: This folder holds the previously outlined dependencies, except for those that are using the new zig package manager. 37 | - **editor**: This folder holds individual files generally with simple *draw()* functions that mimic the layout of the editor itself. I tried to use subfolders and similar to 38 | set the project up in a way that was easy to understand from looking at the editor itself. 39 | - i.e. `editor/artboard/canvas.zig` is the file responsible for the canvas within the main artboard, while `editor/artboard/flipbook/canvas.zig` is the canvas within the flipbook. 40 | - Note that `editor.zig` contains a bit more than just drawing of the editor panels, and contains many of the main *editor* related functions, like loading and opening files, setting the project folder, 41 | saving files, and importing png files. 42 | 43 | - **gfx**: Pixi is set up similar to a game, with the flipbook and main artboard having a camera. Each file actually has its own Camera, which allows u 44 | to have individual views per file, and not a shared camera between all files. That means you can be working on two files and not have your camera move around as you switch. 45 | - Other things in gfx are general things related to textures, atlases, quads, etc. Some of this is unused currently and can be removed. 46 | 47 | - **input**: Input holds hotkeys and mouse information. 48 | - `Hotkeys.zig` is my attempt at trying to set up configurable hotkeys in the future. 49 | 50 | - **math**: General math functions I've written or picked up over time. 51 | - **shaders**: Currently doesn't get used, but in the future if we support using the GPU for some operations, the wgsl files would live here. 52 | - **storage**: This is where History, and the containers used to store information are. internal and external contain the structs used to describe a pixi file internally, with additional information for the program to use, or externally, which should be easily exported as JSON. 53 | - **tools**: A few helpful things such as font-awesome mapping, an example of the build step to process assets, and the Packer struct, which is responsible for packing all sprites to an atlas. 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 |

6 | 7 | ![buildworkflow](https://github.com/foxnne/pixi/actions/workflows/build.yml/badge.svg) 8 | 9 | # 10 | **Pixi** is an cross-platform open-source pixel art editor and animation editor written in [Zig](https://github.com/ziglang/zig). 11 | 12 | #### Check out the [user guide](https://github.com/foxnne/pixi/wiki/User-Guide)! 13 | 14 | ![pixi_explanatory_workflow](https://github.com/foxnne/pixi/assets/49629865/51e16f4d-634e-461d-ba5e-41cc4fa8229e) 15 | 16 | Screenshot 2023-08-09 at 1 15 03 AM 17 | 18 | Screenshot 2023-08-09 at 1 12 48 AM 19 | 20 | 21 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R5R4LL2PJ) 22 | 23 | ## Currently supported features 24 | - [x] Typical pixel art operations. (draw, erase, dropper, bucket, selection, transformation, etc) 25 | - [x] Create animations and preview easily, edit directly on the preview. 26 | - [x] View previous and next frames of the animation. 27 | - [x] Set sprite origins for drawing sprites easily in game frameworks. 28 | - [x] Import and slice existing .png spritesheets. 29 | - [x] Intuitive and customizeable user interface. 30 | - [x] Sprite packing 31 | - [x] Theming 32 | - [x] Automatic packing and export on file save 33 | - [x] Also a zig library offering modules for handling assets 34 | - [x] NEW: Export animations as .gifs 35 | 36 | ## User Interface 37 | - The user interface is driven by [Dear Imgui](https://github.com/ocornut/imgui) which should be familiar to many. 38 | - The general layout takes many ideas from VSCode, as well as general project setup using folders. 39 | 40 | ## Compilation 41 | - [Linux] Ensure `gtk+3-devel` or similar is installed (for native file dialogs). 42 | - Install zig using [zigup](https://github.com/marler8997/zigup) `zigup 0.14.0-dev.2577+271452d22` or manually and add to PATH. 43 | - Zig version required is latest mach nominated version, find [here.]https://machengine.org/docs/nominated-zig/#2024110-mach(https://machengine.org/docs/nominated-zig/#2024110-mach) 44 | - Clone pixi. 45 | - Build. 46 | - ```git clone https://github.com/foxnne/pixi.git``` 47 | - ```cd pixi``` 48 | - ```zig build run``` 49 | 50 | ## Credits 51 | - The wonderful [Dear Imgui](https://github.com/ocornut/imgui) used for almost all of the user interface. 52 | - [emidoots](https://github.com/emidoots) for all the help and [mach](https://github.com/hexops/mach). 53 | - [michal-z](https://github.com/michal-z) for all the help and [zig-gamedev](https://github.com/michal-z/zig-gamedev). 54 | - [prime31](https://github.com/prime31) for all the help. 55 | - Any and all contributors 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Atlas.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fs = @import("tools/fs.zig"); 3 | const pixi = @import("pixi.zig"); 4 | 5 | const Atlas = @This(); 6 | 7 | const Sprite = @import("Sprite.zig"); 8 | const Animation = @import("internal/Animation.zig"); 9 | 10 | sprites: []Sprite, 11 | animations: []Animation, 12 | 13 | pub fn loadFromFile(allocator: std.mem.Allocator, file: [:0]const u8) !Atlas { 14 | const read = try fs.read(allocator, file); 15 | defer allocator.free(read); 16 | 17 | const options = std.json.ParseOptions{ .duplicate_field_behavior = .use_first, .ignore_unknown_fields = true }; 18 | const parsed = try std.json.parseFromSlice(Atlas, allocator, read, options); 19 | defer parsed.deinit(); 20 | 21 | const animations = try allocator.dupe(Animation, parsed.value.animations); 22 | 23 | for (animations) |*animation| { 24 | animation.name = try allocator.dupeZ(u8, animation.name); 25 | } 26 | 27 | return .{ 28 | .sprites = try allocator.dupe(Sprite, parsed.value.sprites), 29 | .animations = animations, 30 | }; 31 | } 32 | 33 | pub fn spriteName(atlas: *Atlas, allocator: std.mem.Allocator, index: usize) ![]const u8 { 34 | for (atlas.animations) |animation| { 35 | if (index >= animation.start and index < animation.start + animation.length) { 36 | if (animation.length > 1) { 37 | const frame: usize = index - animation.start; 38 | return std.fmt.allocPrint(allocator, "{s}_{d}", .{ animation.name, frame }); 39 | } else { 40 | return std.fmt.allocPrint(allocator, "{s}", .{animation.name}); 41 | } 42 | } 43 | } 44 | 45 | return std.fmt.allocPrint(allocator, "Sprite_{d}", .{index}); 46 | } 47 | 48 | pub fn deinit(atlas: *Atlas, allocator: std.mem.Allocator) void { 49 | for (atlas.animations) |*animation| { 50 | allocator.free(animation.name); 51 | } 52 | 53 | allocator.free(atlas.sprites); 54 | allocator.free(atlas.animations); 55 | } 56 | -------------------------------------------------------------------------------- /src/File.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("pixi.zig"); 3 | 4 | const File = @This(); 5 | 6 | version: std.SemanticVersion, 7 | width: u32, 8 | height: u32, 9 | tile_width: u32, 10 | tile_height: u32, 11 | layers: []pixi.Layer, 12 | sprites: []pixi.Sprite, 13 | animations: []pixi.Animation, 14 | 15 | pub fn deinit(self: *File, allocator: std.mem.Allocator) void { 16 | for (self.layers) |*layer| { 17 | allocator.free(layer.name); 18 | } 19 | for (self.animations) |*animation| { 20 | allocator.free(animation.name); 21 | } 22 | allocator.free(self.layers); 23 | allocator.free(self.sprites); 24 | allocator.free(self.animations); 25 | } 26 | -------------------------------------------------------------------------------- /src/Layer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Layer = @This(); 4 | 5 | name: [:0]const u8, 6 | visible: bool = true, 7 | collapse: bool = false, 8 | 9 | pub fn deinit(layer: *Layer, allocator: std.mem.Allocator) void { 10 | allocator.free(layer.name); 11 | } 12 | -------------------------------------------------------------------------------- /src/Sprite.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("pixi.zig"); 3 | 4 | const Sprite = @This(); 5 | 6 | source: [4]u32, 7 | origin: [2]i32, 8 | -------------------------------------------------------------------------------- /src/algorithms/algorithms.zig: -------------------------------------------------------------------------------- 1 | pub const brezenham = @import("brezenham.zig"); 2 | -------------------------------------------------------------------------------- /src/algorithms/brezenham.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | pub fn process(start: [2]f32, end: [2]f32) ![][2]f32 { 5 | var output = std.ArrayList([2]f32).init(pixi.app.allocator); 6 | 7 | var x1 = start[0]; 8 | var y1 = start[1]; 9 | var x2 = end[0]; 10 | var y2 = end[1]; 11 | 12 | const steep = @abs(y2 - y1) > @abs(x2 - x1); 13 | if (steep) { 14 | std.mem.swap(f32, &x1, &y1); 15 | std.mem.swap(f32, &x2, &y2); 16 | } 17 | 18 | if (x1 > x2) { 19 | std.mem.swap(f32, &x1, &x2); 20 | std.mem.swap(f32, &y1, &y2); 21 | } 22 | 23 | const dx: f32 = x2 - x1; 24 | const dy: f32 = @abs(y2 - y1); 25 | 26 | var err: f32 = dx / 2.0; 27 | const ystep: i32 = if (y1 < y2) 1 else -1; 28 | var y: i32 = @as(i32, @intFromFloat(y1)); 29 | 30 | const maxX: i32 = @as(i32, @intFromFloat(x2)); 31 | 32 | var x: i32 = @as(i32, @intFromFloat(x1)); 33 | while (x <= maxX) : (x += 1) { 34 | if (steep) { 35 | try output.append(.{ @as(f32, @floatFromInt(y)), @as(f32, @floatFromInt(x)) }); 36 | } else { 37 | try output.append(.{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }); 38 | } 39 | 40 | err -= dy; 41 | if (err < 0) { 42 | y += ystep; 43 | err += dx; 44 | } 45 | } 46 | 47 | return output.toOwnedSlice(); 48 | } 49 | -------------------------------------------------------------------------------- /src/deps/cgif/inc/cgif.h: -------------------------------------------------------------------------------- 1 | #ifndef CGIF_H 2 | #define CGIF_H 3 | 4 | #include 5 | #include 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | // flags to set the GIF/frame-attributes 12 | #define CGIF_ATTR_IS_ANIMATED (1uL << 1) // make an animated GIF (default is non-animated GIF) 13 | #define CGIF_ATTR_NO_GLOBAL_TABLE (1uL << 2) // disable global color table (global color table is default) 14 | #define CGIF_ATTR_HAS_TRANSPARENCY (1uL << 3) // first entry in color table contains transparency (alpha channel) 15 | #define CGIF_ATTR_NO_LOOP (1uL << 4) // don't loop a GIF animation: only play it one time. 16 | 17 | #define CGIF_GEN_KEEP_IDENT_FRAMES (1uL << 0) // keep frames that are identical to previous frame (default is to drop them) 18 | 19 | #define CGIF_FRAME_ATTR_USE_LOCAL_TABLE (1uL << 0) // use a local color table for a frame (local color table is not used by default) 20 | #define CGIF_FRAME_ATTR_HAS_ALPHA (1uL << 1) // alpha channel index provided by user (transIndex field) 21 | #define CGIF_FRAME_ATTR_HAS_SET_TRANS (1uL << 2) // transparency setting provided by user (transIndex field) 22 | #define CGIF_FRAME_ATTR_INTERLACED (1uL << 3) // encode frame interlaced (default is not interlaced) 23 | // flags to decrease GIF-size 24 | #define CGIF_FRAME_GEN_USE_TRANSPARENCY (1uL << 0) // use transparency optimization (setting pixels identical to previous frame transparent) 25 | #define CGIF_FRAME_GEN_USE_DIFF_WINDOW (1uL << 1) // do encoding just for the sub-window that has changed from previous frame 26 | 27 | #define CGIF_INFINITE_LOOP (0x0000uL) // for animated GIF: 0 specifies infinite loop 28 | 29 | typedef enum { 30 | CGIF_ERROR = -1, // something unspecified failed 31 | CGIF_OK = 0, // everything OK 32 | CGIF_EWRITE, // writing GIF data failed 33 | CGIF_EALLOC, // allocating memory failed 34 | CGIF_ECLOSE, // final call to fclose failed 35 | CGIF_EOPEN, // failed to open output file 36 | CGIF_EINDEX, // invalid index in image data provided by user 37 | // internal section (values subject to change) 38 | CGIF_PENDING, 39 | } cgif_result; 40 | 41 | typedef struct st_gif CGIF; // struct for the full GIF 42 | typedef struct st_gifconfig CGIF_Config; // global cofinguration parameters of the GIF 43 | typedef struct st_frameconfig CGIF_FrameConfig; // local configuration parameters for a frame 44 | 45 | typedef int cgif_write_fn(void* pContext, const uint8_t* pData, const size_t numBytes); // callback function for stream-based output 46 | 47 | // prototypes 48 | CGIF* cgif_newgif (CGIF_Config* pConfig); // creates a new GIF (returns pointer to new GIF or NULL on error) 49 | int cgif_addframe (CGIF* pGIF, CGIF_FrameConfig* pConfig); // adds the next frame to an existing GIF (returns 0 on success) 50 | int cgif_close (CGIF* pGIF); // close file and free allocated memory (returns 0 on success) 51 | 52 | // CGIF_Config type (parameters passed by user) 53 | // note: must stay AS IS for backward compatibility 54 | struct st_gifconfig { 55 | uint8_t* pGlobalPalette; // global color table of the GIF 56 | const char* path; // path of the GIF to be created, mutually exclusive with pWriteFn 57 | uint32_t attrFlags; // fixed attributes of the GIF (e.g. whether it is animated or not) 58 | uint32_t genFlags; // flags that determine how the GIF is generated (e.g. optimization) 59 | uint16_t width; // width of each frame in the GIF 60 | uint16_t height; // height of each frame in the GIF 61 | uint16_t numGlobalPaletteEntries; // size of the global color table 62 | uint16_t numLoops; // number of repetitons of an animated GIF (set to INFINITE_LOOP for infinite loop) 63 | cgif_write_fn *pWriteFn; // callback function for chunks of output data, mutually exclusive with path 64 | void* pContext; // opaque pointer passed as the first parameter to pWriteFn 65 | }; 66 | 67 | // CGIF_FrameConfig type (parameters passed by user) 68 | // note: must stay AS IS for backward compatibility 69 | struct st_frameconfig { 70 | uint8_t* pLocalPalette; // local color table of a frame 71 | uint8_t* pImageData; // image data to be encoded 72 | uint32_t attrFlags; // fixed attributes of the GIF frame 73 | uint32_t genFlags; // flags that determine how the GIF frame is created (e.g. optimization) 74 | uint16_t delay; // delay before the next frame is shown (units of 0.01 s) 75 | uint16_t numLocalPaletteEntries; // size of the local color table 76 | uint8_t transIndex; // introduced with V0.2.0 77 | }; 78 | 79 | #ifdef __cplusplus 80 | } 81 | #endif 82 | 83 | #endif // CGIF_H 84 | -------------------------------------------------------------------------------- /src/deps/cgif/inc/cgif_raw.h: -------------------------------------------------------------------------------- 1 | #ifndef CGIF_RAW_H 2 | #define CGIF_RAW_H 3 | 4 | #include 5 | 6 | #include "cgif.h" 7 | 8 | #ifdef __cplusplus 9 | extern "C" { 10 | #endif 11 | 12 | #define DISPOSAL_METHOD_LEAVE (1uL << 2) 13 | #define DISPOSAL_METHOD_BACKGROUND (2uL << 2) 14 | #define DISPOSAL_METHOD_PREVIOUS (3uL << 2) 15 | 16 | // flags to set the GIF attributes 17 | #define CGIF_RAW_ATTR_IS_ANIMATED (1uL << 0) // make an animated GIF (default is non-animated GIF) 18 | #define CGIF_RAW_ATTR_NO_LOOP (1uL << 1) // don't loop a GIF animation: only play it one time. 19 | 20 | // flags to set the Frame attributes 21 | #define CGIF_RAW_FRAME_ATTR_HAS_TRANS (1uL << 0) // provided transIndex should be set 22 | #define CGIF_RAW_FRAME_ATTR_INTERLACED (1uL << 1) // encode frame interlaced 23 | 24 | // CGIFRaw_Config type 25 | // note: internal sections, subject to change. 26 | typedef struct { 27 | cgif_write_fn *pWriteFn; // callback function for chunks of output data 28 | void* pContext; // opaque pointer passed as the first parameter to pWriteFn 29 | uint8_t* pGCT; // global color table of the GIF 30 | uint32_t attrFlags; // fixed attributes of the GIF (e.g. whether it is animated or not) 31 | uint16_t width; // effective width of each frame in the GIF 32 | uint16_t height; // effective height of each frame in the GIF 33 | uint16_t sizeGCT; // size of the global color table (GCT) 34 | uint16_t numLoops; // number of repetitons of an animated GIF (set to INFINITE_LOOP resp. 0 for infinite loop, use CGIF_ATTR_NO_LOOP if you don't want any repetition) 35 | } CGIFRaw_Config; 36 | 37 | // CGIFRaw_FrameConfig type 38 | // note: internal sections, subject to chage. 39 | typedef struct { 40 | uint8_t* pLCT; // local color table of the frame (LCT) 41 | uint8_t* pImageData; // image data to be encoded (indices to CT) 42 | uint32_t attrFlags; // fixed attributes of the GIF frame 43 | uint16_t width; // width of frame 44 | uint16_t height; // height of frame 45 | uint16_t top; // top offset of frame 46 | uint16_t left; // left offset of frame 47 | uint16_t delay; // delay before the next frame is shown (units of 0.01 s [cs]) 48 | uint16_t sizeLCT; // size of the local color table (LCT) 49 | uint8_t disposalMethod; // specifies how this frame should be disposed after being displayed. 50 | uint8_t transIndex; // transparency index 51 | } CGIFRaw_FrameConfig; 52 | 53 | // CGIFRaw type 54 | // note: internal sections, subject to change. 55 | typedef struct { 56 | CGIFRaw_Config config; // configutation parameters of the GIF (see above) 57 | cgif_result curResult; // current result status of GIFRaw stream 58 | } CGIFRaw; 59 | 60 | // prototypes 61 | CGIFRaw* cgif_raw_newgif (const CGIFRaw_Config* pConfig); 62 | cgif_result cgif_raw_addframe (CGIFRaw* pGIF, const CGIFRaw_FrameConfig* pConfig); 63 | cgif_result cgif_raw_close (CGIFRaw* pGIF); 64 | 65 | #ifdef __cplusplus 66 | } 67 | #endif 68 | 69 | #endif // CGIF_RAW_H 70 | -------------------------------------------------------------------------------- /src/deps/nfd-zig/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = std.builtin; 3 | 4 | fn sdkPath(comptime suffix: []const u8) []const u8 { 5 | if (suffix[0] != '/') @compileError("relToPath requires an absolute path!"); 6 | return comptime blk: { 7 | const root_dir = std.fs.path.dirname(@src().file) orelse "."; 8 | break :blk root_dir ++ suffix; 9 | }; 10 | } 11 | 12 | pub fn makeLib(b: *std.Build, target: std.Build.ResolvedTarget, optimize: builtin.OptimizeMode) *std.Build.Module { 13 | // const lib = b.addStaticLibrary(.{ 14 | // .name = "nfd", 15 | // .root_source_file = .{ .path = sdkPath("/src/lib.zig") }, 16 | // .target = target, 17 | // .optimize = optimize, 18 | // }); 19 | 20 | const nfd_mod = b.addModule("nfd", .{ 21 | .root_source_file = .{ .cwd_relative = sdkPath("/src/lib.zig") }, 22 | .target = target, 23 | .optimize = optimize, 24 | .link_libc = true, 25 | }); 26 | 27 | const cflags = [_][]const u8{"-Wall"}; 28 | nfd_mod.addIncludePath(.{ .cwd_relative = sdkPath("/nativefiledialog/src/include") }); 29 | nfd_mod.addCSourceFile(.{ .file = .{ .cwd_relative = sdkPath("/nativefiledialog/src/nfd_common.c") }, .flags = &cflags }); 30 | switch (target.result.os.tag) { 31 | .macos => nfd_mod.addCSourceFile(.{ .file = .{ .cwd_relative = sdkPath("/nativefiledialog/src/nfd_cocoa.m") }, .flags = &cflags }), 32 | .windows => nfd_mod.addCSourceFile(.{ .file = .{ .cwd_relative = sdkPath("/nativefiledialog/src/nfd_win.cpp") }, .flags = &cflags }), 33 | .linux => nfd_mod.addCSourceFile(.{ .file = .{ .cwd_relative = sdkPath("/nativefiledialog/src/nfd_gtk.c") }, .flags = &cflags }), 34 | else => @panic("unsupported OS"), 35 | } 36 | 37 | switch (target.result.os.tag) { 38 | .macos => nfd_mod.linkFramework("AppKit", .{}), 39 | .windows => { 40 | nfd_mod.linkSystemLibrary("shell32", .{}); 41 | nfd_mod.linkSystemLibrary("ole32", .{}); 42 | nfd_mod.linkSystemLibrary("uuid", .{}); // needed by MinGW 43 | }, 44 | .linux => { 45 | nfd_mod.linkSystemLibrary("atk-1.0", .{}); 46 | nfd_mod.linkSystemLibrary("gdk-3", .{}); 47 | nfd_mod.linkSystemLibrary("gtk-3", .{}); 48 | nfd_mod.linkSystemLibrary("glib-2.0", .{}); 49 | nfd_mod.linkSystemLibrary("gobject-2.0", .{}); 50 | }, 51 | else => @panic("unsupported OS"), 52 | } 53 | 54 | return nfd_mod; 55 | } 56 | 57 | pub fn getModule(b: *std.Build) *std.Build.Module { 58 | return b.createModule(.{ .root_source_file = .{ .cwd_relative = sdkPath("/src/lib.zig") } }); 59 | } 60 | 61 | pub fn build(b: *std.Build) void { 62 | const target = b.standardTargetOptions(.{}); 63 | const optimize = b.standardOptimizeOption(.{}); 64 | const lib = makeLib(b, target, optimize); 65 | 66 | var demo = b.addExecutable(.{ 67 | .name = "demo", 68 | .root_source_file = .{ .cwd_relative = "/src/demo.zig" }, 69 | .target = target, 70 | .optimize = optimize, 71 | }); 72 | demo.addModule("nfd", getModule(b)); 73 | demo.linkLibrary(lib); 74 | demo.install(); 75 | 76 | const run_demo_cmd = demo.run(); 77 | run_demo_cmd.step.dependOn(b.getInstallStep()); 78 | 79 | const run_demo_step = b.step("run", "Run the demo"); 80 | run_demo_step.dependOn(&run_demo_cmd.step); 81 | } 82 | -------------------------------------------------------------------------------- /src/deps/nfd-zig/nativefiledialog/src/common.h: -------------------------------------------------------------------------------- 1 | /* 2 | Native File Dialog 3 | 4 | Internal, common across platforms 5 | 6 | http://www.frogtoss.com/labs 7 | */ 8 | 9 | 10 | #ifndef _NFD_COMMON_H 11 | #define _NFD_COMMON_H 12 | 13 | #define NFD_MAX_STRLEN 256 14 | #define _NFD_UNUSED(x) ((void)x) 15 | 16 | void *NFDi_Malloc( size_t bytes ); 17 | void NFDi_Free( void *ptr ); 18 | void NFDi_SetError( const char *msg ); 19 | void NFDi_SafeStrncpy( char *dst, const char *src, size_t maxCopy ); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/deps/nfd-zig/nativefiledialog/src/include/nfd.h: -------------------------------------------------------------------------------- 1 | /* 2 | Native File Dialog 3 | 4 | User API 5 | 6 | http://www.frogtoss.com/labs 7 | */ 8 | 9 | 10 | #ifndef _NFD_H 11 | #define _NFD_H 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | #include 18 | 19 | /* denotes UTF-8 char */ 20 | typedef char nfdchar_t; 21 | 22 | /* opaque data structure -- see NFD_PathSet_* */ 23 | typedef struct { 24 | nfdchar_t *buf; 25 | size_t *indices; /* byte offsets into buf */ 26 | size_t count; /* number of indices into buf */ 27 | }nfdpathset_t; 28 | 29 | typedef enum { 30 | NFD_ERROR, /* programmatic error */ 31 | NFD_OKAY, /* user pressed okay, or successful return */ 32 | NFD_CANCEL /* user pressed cancel */ 33 | }nfdresult_t; 34 | 35 | 36 | /* nfd_.c */ 37 | 38 | /* single file open dialog */ 39 | nfdresult_t NFD_OpenDialog( const nfdchar_t *filterList, 40 | const nfdchar_t *defaultPath, 41 | nfdchar_t **outPath ); 42 | 43 | /* multiple file open dialog */ 44 | nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList, 45 | const nfdchar_t *defaultPath, 46 | nfdpathset_t *outPaths ); 47 | 48 | /* save dialog */ 49 | nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList, 50 | const nfdchar_t *defaultPath, 51 | nfdchar_t **outPath ); 52 | 53 | 54 | /* select folder dialog */ 55 | nfdresult_t NFD_PickFolder( const nfdchar_t *defaultPath, 56 | nfdchar_t **outPath); 57 | 58 | /* nfd_common.c */ 59 | 60 | /* get last error -- set when nfdresult_t returns NFD_ERROR */ 61 | const char *NFD_GetError( void ); 62 | /* get the number of entries stored in pathSet */ 63 | size_t NFD_PathSet_GetCount( const nfdpathset_t *pathSet ); 64 | /* Get the UTF-8 path at offset index */ 65 | nfdchar_t *NFD_PathSet_GetPath( const nfdpathset_t *pathSet, size_t index ); 66 | /* Free the pathSet */ 67 | void NFD_PathSet_Free( nfdpathset_t *pathSet ); 68 | 69 | 70 | #ifdef __cplusplus 71 | } 72 | #endif 73 | 74 | #endif 75 | -------------------------------------------------------------------------------- /src/deps/nfd-zig/nativefiledialog/src/nfd_common.c: -------------------------------------------------------------------------------- 1 | /* 2 | Native File Dialog 3 | 4 | http://www.frogtoss.com/labs 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include "nfd_common.h" 11 | 12 | static char g_errorstr[NFD_MAX_STRLEN] = {0}; 13 | 14 | /* public routines */ 15 | 16 | const char *NFD_GetError( void ) 17 | { 18 | return g_errorstr; 19 | } 20 | 21 | size_t NFD_PathSet_GetCount( const nfdpathset_t *pathset ) 22 | { 23 | assert(pathset); 24 | return pathset->count; 25 | } 26 | 27 | nfdchar_t *NFD_PathSet_GetPath( const nfdpathset_t *pathset, size_t num ) 28 | { 29 | assert(pathset); 30 | assert(num < pathset->count); 31 | 32 | return pathset->buf + pathset->indices[num]; 33 | } 34 | 35 | void NFD_PathSet_Free( nfdpathset_t *pathset ) 36 | { 37 | assert(pathset); 38 | NFDi_Free( pathset->indices ); 39 | NFDi_Free( pathset->buf ); 40 | } 41 | 42 | /* internal routines */ 43 | 44 | void *NFDi_Malloc( size_t bytes ) 45 | { 46 | void *ptr = malloc(bytes); 47 | if ( !ptr ) 48 | NFDi_SetError("NFDi_Malloc failed."); 49 | 50 | return ptr; 51 | } 52 | 53 | void NFDi_Free( void *ptr ) 54 | { 55 | assert(ptr); 56 | free(ptr); 57 | } 58 | 59 | void NFDi_SetError( const char *msg ) 60 | { 61 | int bTruncate = NFDi_SafeStrncpy( g_errorstr, msg, NFD_MAX_STRLEN ); 62 | assert( !bTruncate ); _NFD_UNUSED(bTruncate); 63 | } 64 | 65 | 66 | int NFDi_SafeStrncpy( char *dst, const char *src, size_t maxCopy ) 67 | { 68 | size_t n = maxCopy; 69 | char *d = dst; 70 | 71 | assert( src ); 72 | assert( dst ); 73 | 74 | while ( n > 0 && *src != '\0' ) 75 | { 76 | *d++ = *src++; 77 | --n; 78 | } 79 | 80 | /* Truncation case - 81 | terminate string and return true */ 82 | if ( n == 0 ) 83 | { 84 | dst[maxCopy-1] = '\0'; 85 | return 1; 86 | } 87 | 88 | /* No truncation. Append a single NULL and return. */ 89 | *d = '\0'; 90 | return 0; 91 | } 92 | 93 | 94 | /* adapted from microutf8 */ 95 | int32_t NFDi_UTF8_Strlen( const nfdchar_t *str ) 96 | { 97 | /* This function doesn't properly check validity of UTF-8 character 98 | sequence, it is supposed to use only with valid UTF-8 strings. */ 99 | 100 | int32_t character_count = 0; 101 | int32_t i = 0; /* Counter used to iterate over string. */ 102 | nfdchar_t maybe_bom[4]; 103 | 104 | /* If there is UTF-8 BOM ignore it. */ 105 | if (strlen(str) > 2) 106 | { 107 | strncpy(maybe_bom, str, 3); 108 | maybe_bom[3] = 0; 109 | if (strcmp(maybe_bom, (nfdchar_t*)NFD_UTF8_BOM) == 0) 110 | i += 3; 111 | } 112 | 113 | while(str[i]) 114 | { 115 | if (str[i] >> 7 == 0) 116 | { 117 | /* If bit pattern begins with 0 we have ascii character. */ 118 | ++character_count; 119 | } 120 | else if (str[i] >> 6 == 3) 121 | { 122 | /* If bit pattern begins with 11 it is beginning of UTF-8 byte sequence. */ 123 | ++character_count; 124 | } 125 | else if (str[i] >> 6 == 2) 126 | ; /* If bit pattern begins with 10 it is middle of utf-8 byte sequence. */ 127 | else 128 | { 129 | /* In any other case this is not valid UTF-8. */ 130 | return -1; 131 | } 132 | ++i; 133 | } 134 | 135 | return character_count; 136 | } 137 | 138 | int NFDi_IsFilterSegmentChar( char ch ) 139 | { 140 | return (ch==','||ch==';'||ch=='\0'); 141 | } 142 | 143 | -------------------------------------------------------------------------------- /src/deps/nfd-zig/nativefiledialog/src/nfd_common.h: -------------------------------------------------------------------------------- 1 | /* 2 | Native File Dialog 3 | 4 | Internal, common across platforms 5 | 6 | http://www.frogtoss.com/labs 7 | */ 8 | 9 | 10 | #ifndef _NFD_COMMON_H 11 | #define _NFD_COMMON_H 12 | 13 | #include "nfd.h" 14 | 15 | #include 16 | 17 | #ifdef __cplusplus 18 | extern "C" { 19 | #endif 20 | 21 | #define NFD_MAX_STRLEN 256 22 | #define _NFD_UNUSED(x) ((void)x) 23 | 24 | #define NFD_UTF8_BOM "\xEF\xBB\xBF" 25 | 26 | 27 | void *NFDi_Malloc( size_t bytes ); 28 | void NFDi_Free( void *ptr ); 29 | void NFDi_SetError( const char *msg ); 30 | int NFDi_SafeStrncpy( char *dst, const char *src, size_t maxCopy ); 31 | int32_t NFDi_UTF8_Strlen( const nfdchar_t *str ); 32 | int NFDi_IsFilterSegmentChar( char ch ); 33 | 34 | #ifdef __cplusplus 35 | } 36 | #endif 37 | 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /src/deps/nfd-zig/src/c.zig: -------------------------------------------------------------------------------- 1 | pub const nfdchar_t = u8; 2 | pub const nfdpathset_t = extern struct { 3 | buf: [*c]nfdchar_t, 4 | indices: [*c]usize, 5 | count: usize, 6 | }; 7 | pub const NFD_ERROR: c_int = 0; 8 | pub const NFD_OKAY: c_int = 1; 9 | pub const NFD_CANCEL: c_int = 2; 10 | pub const nfdresult_t = c_int; 11 | pub extern fn NFD_OpenDialog(filterList: [*c]const nfdchar_t, defaultPath: [*c]const nfdchar_t, outPath: [*c][*c]nfdchar_t) nfdresult_t; 12 | pub extern fn NFD_OpenDialogMultiple(filterList: [*c]const nfdchar_t, defaultPath: [*c]const nfdchar_t, outPaths: [*c]nfdpathset_t) nfdresult_t; 13 | pub extern fn NFD_SaveDialog(filterList: [*c]const nfdchar_t, defaultPath: [*c]const nfdchar_t, outPath: [*c][*c]nfdchar_t) nfdresult_t; 14 | pub extern fn NFD_PickFolder(defaultPath: [*c]const nfdchar_t, outPath: [*c][*c]nfdchar_t) nfdresult_t; 15 | pub extern fn NFD_GetError() [*c]const u8; 16 | pub extern fn NFD_PathSet_GetCount(pathSet: [*c]const nfdpathset_t) usize; 17 | pub extern fn NFD_PathSet_GetPath(pathSet: [*c]const nfdpathset_t, index: usize) [*c]nfdchar_t; 18 | pub extern fn NFD_PathSet_Free(pathSet: [*c]nfdpathset_t) void; 19 | -------------------------------------------------------------------------------- /src/deps/nfd-zig/src/demo.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const nfd = @import("nfd"); 3 | 4 | pub fn main() !void { 5 | const file_path = try nfd.saveFileDialog("txt", null); 6 | if (file_path) |path| { 7 | defer nfd.freePath(path); 8 | std.debug.print("saveFileDialog result: {s}\n", .{path}); 9 | 10 | const open_path = try nfd.openFileDialog("txt", path); 11 | if (open_path) |path2| { 12 | defer nfd.freePath(path2); 13 | std.debug.print("openFileDialog result: {s}\n", .{path2}); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/deps/nfd-zig/src/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("c.zig"); 3 | const log = std.log.scoped(.nfd); 4 | 5 | pub const Error = error{ 6 | NfdError, 7 | }; 8 | 9 | pub fn makeError() Error { 10 | if (c.NFD_GetError()) |ptr| { 11 | log.debug("{s}\n", .{ 12 | std.mem.span(ptr), 13 | }); 14 | } 15 | return error.NfdError; 16 | } 17 | 18 | /// Open single file dialog 19 | pub fn openFileDialog(filter: ?[:0]const u8, default_path: ?[:0]const u8) Error!?[:0]const u8 { 20 | var out_path: [*c]u8 = null; 21 | 22 | // allocates using malloc 23 | const result = c.NFD_OpenDialog(if (filter != null) filter.?.ptr else null, if (default_path != null) default_path.?.ptr else null, &out_path); 24 | 25 | return switch (result) { 26 | c.NFD_OKAY => if (out_path == null) null else std.mem.sliceTo(out_path, 0), 27 | c.NFD_ERROR => makeError(), 28 | else => null, 29 | }; 30 | } 31 | 32 | /// Open save dialog 33 | pub fn saveFileDialog(filter: ?[:0]const u8, default_path: ?[:0]const u8) Error!?[:0]const u8 { 34 | var out_path: [*c]u8 = null; 35 | 36 | // allocates using malloc 37 | const result = c.NFD_SaveDialog(if (filter != null) filter.?.ptr else null, if (default_path != null) default_path.?.ptr else null, &out_path); 38 | 39 | return switch (result) { 40 | c.NFD_OKAY => if (out_path == null) null else std.mem.sliceTo(out_path, 0), 41 | c.NFD_ERROR => makeError(), 42 | else => null, 43 | }; 44 | } 45 | 46 | /// Open folder dialog 47 | pub fn openFolderDialog(default_path: ?[:0]const u8) Error!?[:0]const u8 { 48 | var out_path: [*c]u8 = null; 49 | 50 | // allocates using malloc 51 | const result = c.NFD_PickFolder(if (default_path != null) default_path.?.ptr else null, &out_path); 52 | 53 | return switch (result) { 54 | c.NFD_OKAY => if (out_path == null) null else std.mem.sliceTo(out_path, 0), 55 | c.NFD_ERROR => makeError(), 56 | else => null, 57 | }; 58 | } 59 | 60 | pub fn freePath(path: []const u8) void { 61 | std.c.free(@as(*anyopaque, @ptrFromInt(@intFromPtr(path.ptr)))); 62 | } 63 | -------------------------------------------------------------------------------- /src/deps/zip/build.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | 4 | pub fn build(_: *std.Build) !void {} 5 | 6 | pub const Package = struct { 7 | module: *std.Build.Module, 8 | }; 9 | 10 | pub fn package(b: *std.Build, _: struct {}) Package { 11 | const module = b.createModule(.{ 12 | .root_source_file = .{ .cwd_relative = thisDir() ++ "/zip.zig" }, 13 | }); 14 | return .{ .module = module }; 15 | } 16 | 17 | pub fn link(exe: *std.Build.Step.Compile) void { 18 | exe.linkLibC(); 19 | exe.addIncludePath(.{ .cwd_relative = thisDir() ++ "/src" }); 20 | const c_flags = [_][]const u8{"-fno-sanitize=undefined"}; 21 | exe.addCSourceFile(.{ .file = .{ .cwd_relative = thisDir() ++ "/src/zip.c" }, .flags = &c_flags }); 22 | } 23 | 24 | inline fn thisDir() []const u8 { 25 | return comptime std.fs.path.dirname(@src().file) orelse "."; 26 | } 27 | -------------------------------------------------------------------------------- /src/deps/zip/zip.zig: -------------------------------------------------------------------------------- 1 | //usingnamespace @cImport(@cInclude("zip.h")); 2 | pub extern fn zip_strerror(errnum: c_int) [*c]const u8; 3 | pub const struct_zip_t = opaque {}; 4 | pub extern fn zip_open(zipname: [*c]const u8, level: c_int, mode: u8) ?*struct_zip_t; 5 | pub extern fn zip_close(zip: ?*struct_zip_t) void; 6 | pub extern fn zip_is64(zip: ?*struct_zip_t) c_int; 7 | pub extern fn zip_entry_open(zip: ?*struct_zip_t, entryname: [*c]const u8) c_int; 8 | pub extern fn zip_entry_openbyindex(zip: ?*struct_zip_t, index: c_int) c_int; 9 | pub extern fn zip_entry_close(zip: ?*struct_zip_t) c_int; 10 | pub extern fn zip_entry_name(zip: ?*struct_zip_t) [*c]const u8; 11 | pub extern fn zip_entry_index(zip: ?*struct_zip_t) c_int; 12 | pub extern fn zip_entry_isdir(zip: ?*struct_zip_t) c_int; 13 | pub extern fn zip_entry_size(zip: ?*struct_zip_t) c_ulonglong; 14 | pub extern fn zip_entry_crc32(zip: ?*struct_zip_t) c_uint; 15 | pub extern fn zip_entry_write(zip: ?*struct_zip_t, buf: ?*const anyopaque, bufsize: usize) c_int; 16 | pub extern fn zip_entry_fwrite(zip: ?*struct_zip_t, filename: [*c]const u8) c_int; 17 | pub extern fn zip_entry_read(zip: ?*struct_zip_t, buf: [*c]?*anyopaque, bufsize: [*c]usize) isize; 18 | pub extern fn zip_entry_noallocread(zip: ?*struct_zip_t, buf: ?*anyopaque, bufsize: usize) isize; 19 | pub extern fn zip_entry_fread(zip: ?*struct_zip_t, filename: [*c]const u8) c_int; 20 | pub extern fn zip_entry_extract(zip: ?*struct_zip_t, on_extract: ?fn (?*anyopaque, c_ulonglong, ?*const anyopaque, usize) callconv(.C) usize, arg: ?*anyopaque) c_int; 21 | pub extern fn zip_entries_total(zip: ?*struct_zip_t) c_int; 22 | pub extern fn zip_entries_delete(zip: ?*struct_zip_t, entries: [*c]const [*c]u8, len: usize) c_int; 23 | pub extern fn zip_stream_extract(stream: [*c]const u8, size: usize, dir: [*c]const u8, on_extract: ?fn ([*c]const u8, ?*anyopaque) callconv(.C) c_int, arg: ?*anyopaque) c_int; 24 | pub extern fn zip_stream_open(stream: [*c]const u8, size: usize, level: c_int, mode: u8) ?*struct_zip_t; 25 | pub extern fn zip_stream_copy(zip: ?*struct_zip_t, buf: [*c]?*anyopaque, bufsize: [*c]usize) isize; 26 | pub extern fn zip_stream_close(zip: ?*struct_zip_t) void; 27 | pub extern fn zip_create(zipname: [*c]const u8, filenames: [*c][*c]const u8, len: usize) c_int; 28 | pub extern fn zip_extract(zipname: [*c]const u8, dir: [*c]const u8, on_extract_entry: ?fn ([*c]const u8, ?*anyopaque) callconv(.C) c_int, arg: ?*anyopaque) c_int; 29 | pub const ZIP_DEFAULT_COMPRESSION_LEVEL = @as(c_int, 6); 30 | pub const ZIP_ENOINIT = -@as(c_int, 1); 31 | pub const ZIP_EINVENTNAME = -@as(c_int, 2); 32 | pub const ZIP_ENOENT = -@as(c_int, 3); 33 | pub const ZIP_EINVMODE = -@as(c_int, 4); 34 | pub const ZIP_EINVLVL = -@as(c_int, 5); 35 | pub const ZIP_ENOSUP64 = -@as(c_int, 6); 36 | pub const ZIP_EMEMSET = -@as(c_int, 7); 37 | pub const ZIP_EWRTENT = -@as(c_int, 8); 38 | pub const ZIP_ETDEFLINIT = -@as(c_int, 9); 39 | pub const ZIP_EINVIDX = -@as(c_int, 10); 40 | pub const ZIP_ENOHDR = -@as(c_int, 11); 41 | pub const ZIP_ETDEFLBUF = -@as(c_int, 12); 42 | pub const ZIP_ECRTHDR = -@as(c_int, 13); 43 | pub const ZIP_EWRTHDR = -@as(c_int, 14); 44 | pub const ZIP_EWRTDIR = -@as(c_int, 15); 45 | pub const ZIP_EOPNFILE = -@as(c_int, 16); 46 | pub const ZIP_EINVENTTYPE = -@as(c_int, 17); 47 | pub const ZIP_EMEMNOALLOC = -@as(c_int, 18); 48 | pub const ZIP_ENOFILE = -@as(c_int, 19); 49 | pub const ZIP_ENOPERM = -@as(c_int, 20); 50 | pub const ZIP_EOOMEM = -@as(c_int, 21); 51 | pub const ZIP_EINVZIPNAME = -@as(c_int, 22); 52 | pub const ZIP_EMKDIR = -@as(c_int, 23); 53 | pub const ZIP_ESYMLINK = -@as(c_int, 24); 54 | pub const ZIP_ECLSZIP = -@as(c_int, 25); 55 | pub const ZIP_ECAPSIZE = -@as(c_int, 26); 56 | pub const ZIP_EFSEEK = -@as(c_int, 27); 57 | pub const ZIP_EFREAD = -@as(c_int, 28); 58 | pub const ZIP_EFWRITE = -@as(c_int, 29); 59 | pub const zip_t = struct_zip_t; 60 | -------------------------------------------------------------------------------- /src/editor/Colors.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | const Self = @This(); 5 | 6 | primary: [4]u8 = .{ 255, 255, 255, 255 }, 7 | secondary: [4]u8 = .{ 0, 0, 0, 255 }, 8 | height: u8 = 0, 9 | palette: ?pixi.Internal.Palette = null, 10 | keyframe_palette: ?pixi.Internal.Palette = null, 11 | -------------------------------------------------------------------------------- /src/editor/Constants.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | pub const max_name_len = 128; 5 | pub const max_path_len = std.fs.max_path_bytes; 6 | -------------------------------------------------------------------------------- /src/editor/Project.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | const Project = @This(); 5 | 6 | pub var parsed: ?std.json.Parsed(Project) = null; 7 | pub var read: ?[]u8 = null; 8 | 9 | /// Path for the final packed texture to save 10 | packed_texture_output: ?[]const u8 = null, 11 | 12 | /// Path for the final packed heightmap to save 13 | packed_heightmap_output: ?[]const u8 = null, 14 | 15 | /// Path for the final packed atlas to save 16 | packed_atlas_output: ?[]const u8 = null, 17 | 18 | /// If true, the entire project will be repacked and exported on any project file save 19 | pack_on_save: bool = true, 20 | 21 | pub fn load() !?Project { 22 | if (pixi.editor.folder) |folder| { 23 | const file = try std.fs.path.join(pixi.editor.arena.allocator(), &.{ folder, ".pixiproject" }); 24 | 25 | if (pixi.fs.read(pixi.app.allocator, file) catch null) |r| { 26 | read = r; 27 | 28 | const options = std.json.ParseOptions{ .duplicate_field_behavior = .use_first, .ignore_unknown_fields = true }; 29 | if (std.json.parseFromSlice(Project, pixi.app.allocator, r, options) catch null) |p| { 30 | parsed = p; 31 | 32 | if (p.value.packed_atlas_output) |packed_atlas_output| { 33 | @memcpy(pixi.editor.buffers.atlas_path[0..packed_atlas_output.len], packed_atlas_output); 34 | } 35 | 36 | if (p.value.packed_texture_output) |packed_texture_output| { 37 | @memcpy(pixi.editor.buffers.texture_path[0..packed_texture_output.len], packed_texture_output); 38 | } 39 | 40 | if (p.value.packed_heightmap_output) |packed_heightmap_output| { 41 | @memcpy(pixi.editor.buffers.heightmap_path[0..packed_heightmap_output.len], packed_heightmap_output); 42 | } 43 | 44 | return p.value; 45 | } else { 46 | std.log.debug("Failed to parse project file!", .{}); 47 | } 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | pub fn save(project: *Project) !void { 55 | if (pixi.editor.folder) |folder| { 56 | const file = try std.fs.path.join(pixi.editor.arena.allocator(), &.{ folder, ".pixiproject" }); 57 | var handle = try std.fs.createFileAbsolute(file, .{}); 58 | defer handle.close(); 59 | 60 | const out_stream = handle.writer(); 61 | const options = std.json.StringifyOptions{}; 62 | 63 | try std.json.stringify(Project{ 64 | .packed_atlas_output = project.packed_atlas_output, 65 | .packed_texture_output = project.packed_texture_output, 66 | .packed_heightmap_output = project.packed_heightmap_output, 67 | .pack_on_save = project.pack_on_save, 68 | }, options, out_stream); 69 | 70 | return; 71 | } 72 | 73 | return error.FailedToSaveProject; 74 | } 75 | 76 | /// Project output assets will be exported to a join of parent_folder and the individual output paths for each asset 77 | pub fn exportAssets(project: *Project, parent_folder: [:0]const u8) !void { 78 | if (project.packed_atlas_output) |packed_atlas_output| { 79 | const path = try std.fs.path.joinZ(pixi.editor.arena.allocator(), &.{ parent_folder, packed_atlas_output }); 80 | try pixi.editor.atlas.save(path, .data); 81 | } 82 | 83 | if (project.packed_texture_output) |packed_texture_output| { 84 | const path = try std.fs.path.joinZ(pixi.editor.arena.allocator(), &.{ parent_folder, packed_texture_output }); 85 | try pixi.editor.atlas.save(path, .texture); 86 | } 87 | 88 | if (project.packed_heightmap_output) |packed_heightmap_output| { 89 | const path = try std.fs.path.joinZ(pixi.editor.arena.allocator(), &.{ parent_folder, packed_heightmap_output }); 90 | try pixi.editor.atlas.save(path, .heightmap); 91 | } 92 | } 93 | 94 | pub fn deinit(project: *Project) void { 95 | if (read) |r| pixi.app.allocator.free(r); 96 | 97 | if (parsed) |p| { 98 | p.deinit(); 99 | parsed = null; 100 | } else { 101 | if (project.packed_atlas_output) |atlas| { 102 | pixi.app.allocator.free(atlas); 103 | } 104 | if (project.packed_texture_output) |texture| { 105 | pixi.app.allocator.free(texture); 106 | } 107 | if (project.packed_heightmap_output) |heightmap| { 108 | pixi.app.allocator.free(heightmap); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/editor/Recents.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | const Recents = @This(); 5 | 6 | folders: std.ArrayList([:0]const u8), 7 | exports: std.ArrayList([:0]const u8), 8 | 9 | pub fn load(allocator: std.mem.Allocator) !Recents { 10 | var folders = std.ArrayList([:0]const u8).init(allocator); 11 | var exports = std.ArrayList([:0]const u8).init(allocator); 12 | 13 | if (pixi.fs.read(allocator, "recents.json") catch null) |read| { 14 | defer allocator.free(read); 15 | 16 | const options = std.json.ParseOptions{ .duplicate_field_behavior = .use_first, .ignore_unknown_fields = true }; 17 | if (std.json.parseFromSlice(RecentsJson, allocator, read, options) catch null) |parsed| { 18 | defer parsed.deinit(); 19 | 20 | for (parsed.value.folders) |folder| { 21 | const dir_opt = std.fs.openDirAbsoluteZ(folder, .{}) catch null; 22 | if (dir_opt != null) 23 | try folders.append(try allocator.dupeZ(u8, folder)); 24 | } 25 | 26 | for (parsed.value.exports) |exp| { 27 | if (std.fs.path.dirname(exp)) |path| { 28 | const dir_opt = std.fs.openDirAbsolute(path, .{}) catch null; 29 | if (dir_opt != null) 30 | try exports.append(try allocator.dupeZ(u8, exp)); 31 | } 32 | } 33 | } 34 | } 35 | 36 | return .{ .folders = folders, .exports = exports }; 37 | } 38 | 39 | pub fn indexOfFolder(recents: *Recents, path: [:0]const u8) ?usize { 40 | if (recents.folders.items.len == 0) return null; 41 | 42 | for (recents.folders.items, 0..) |folder, i| { 43 | if (std.mem.eql(u8, folder, path)) 44 | return i; 45 | } 46 | return null; 47 | } 48 | 49 | pub fn indexOfExport(recents: *Recents, path: [:0]const u8) ?usize { 50 | if (recents.exports.items.len == 0) return null; 51 | 52 | for (recents.exports.items, 0..) |exp, i| { 53 | if (std.mem.eql(u8, exp, path)) 54 | return i; 55 | } 56 | return null; 57 | } 58 | 59 | pub fn appendFolder(recents: *Recents, path: [:0]const u8) !void { 60 | if (recents.indexOfFolder(path)) |index| { 61 | pixi.app.allocator.free(path); 62 | const folder = recents.folders.swapRemove(index); 63 | try recents.folders.append(folder); 64 | } else { 65 | if (recents.folders.items.len >= pixi.editor.settings.max_recents) { 66 | const folder = recents.folders.swapRemove(0); 67 | pixi.app.allocator.free(folder); 68 | } 69 | 70 | try recents.folders.append(path); 71 | } 72 | } 73 | 74 | pub fn appendExport(recents: *Recents, path: [:0]const u8) !void { 75 | if (recents.indexOfExport(path)) |index| { 76 | const exp = recents.exports.swapRemove(index); 77 | try recents.exports.append(exp); 78 | } else { 79 | if (recents.exports.items.len >= pixi.editor.settings.max_recents) { 80 | const exp = recents.folders.swapRemove(0); 81 | pixi.app.allocator.free(exp); 82 | } 83 | try recents.exports.append(path); 84 | } 85 | } 86 | 87 | pub fn save(recents: *Recents) !void { 88 | var handle = try std.fs.cwd().createFile("recents.json", .{}); 89 | defer handle.close(); 90 | 91 | const out_stream = handle.writer(); 92 | const options = std.json.StringifyOptions{}; 93 | 94 | try std.json.stringify(RecentsJson{ .folders = recents.folders.items, .exports = recents.exports.items }, options, out_stream); 95 | } 96 | 97 | pub fn deinit(recents: *Recents) void { 98 | for (recents.folders.items) |folder| { 99 | pixi.app.allocator.free(folder); 100 | } 101 | 102 | for (recents.exports.items) |exp| { 103 | pixi.app.allocator.free(exp); 104 | } 105 | 106 | recents.folders.clearAndFree(); 107 | recents.folders.deinit(); 108 | 109 | recents.exports.clearAndFree(); 110 | recents.exports.deinit(); 111 | } 112 | 113 | const RecentsJson = struct { 114 | folders: [][:0]const u8, 115 | exports: [][:0]const u8, 116 | }; 117 | -------------------------------------------------------------------------------- /src/editor/Settings.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const pixi = @import("../pixi.zig"); 3 | const std = @import("std"); 4 | 5 | const Settings = @This(); 6 | 7 | pub var parsed: ?std.json.Parsed(Settings) = null; 8 | 9 | pub const InputScheme = enum { mouse, trackpad }; 10 | pub const FlipbookView = enum { sequential, grid }; 11 | pub const Compatibility = enum { none, ldtk }; 12 | 13 | /// Width of the explorer bar. 14 | explorer_width: f32 = 200.0, 15 | 16 | /// Width of the explorer grip. 17 | explorer_grip: f32 = 18.0, 18 | 19 | /// Whether or not the artboard is split 20 | split_artboard: bool = false, 21 | 22 | /// The horizontal ratio of the artboard split 23 | split_artboard_ratio: f32 = 0.5, 24 | 25 | /// Alignment of explorer separator titles 26 | explorer_title_align: f32 = 0.0, 27 | 28 | /// Height of the flipbook window. 29 | flipbook_height: f32 = 0.3, 30 | 31 | /// Flipbook view, sequential or grid 32 | flipbook_view: FlipbookView = .sequential, 33 | 34 | /// Font size set when loading the editor. 35 | font_size: f32 = 13.0, 36 | 37 | /// Height of the infobar. 38 | info_bar_height: f32 = 24.0, 39 | 40 | /// When a new window is opened, describes the height of the window. 41 | initial_window_height: u32 = 720, 42 | 43 | /// When a new window is opened, describes the width of the window. 44 | initial_window_width: u32 = 1280, 45 | 46 | /// Which control scheme to use for zooming and panning. 47 | /// TODO: Remove builtin check and offer a setup menu if settings.json doesn't exist. 48 | input_scheme: InputScheme = if (builtin.os.tag == .macos) .trackpad else .mouse, 49 | 50 | /// Sensitivity when panning via scrolling with trackpad. 51 | pan_sensitivity: f32 = 15.0, 52 | 53 | /// Whether or not to show rulers on the canvas. 54 | show_rulers: bool = true, 55 | 56 | /// Width of the sidebar. 57 | sidebar_width: f32 = 50, 58 | 59 | /// Height of the sprite edit panel 60 | sprite_edit_height: f32 = 100, 61 | 62 | /// Height of the animation edit panel 63 | animation_edit_height: f32 = 100, 64 | 65 | /// Time of editor animations in seconds 66 | /// If set to 0.0, animations are effectively disabled. 67 | editor_animation_time: f32 = 0.75, 68 | 69 | /// Time it takes for a hotkey to repeat if possible, 70 | /// example: sizing up or down the current tool size 71 | hotkey_repeat_time: f32 = 0.075, 72 | 73 | /// Maximum zoom sensitivity applied at last zoom steps. 74 | zoom_max_sensitivity: f32 = 1.0, 75 | 76 | /// Minimum zoom sensitivity applied at first zoom steps. 77 | zoom_min_sensitivity: f32 = 0.1, 78 | 79 | /// Setting to control overall zoom sensitivity 80 | zoom_sensitivity: f32 = 100.0, 81 | 82 | /// Predetermined zoom steps, each is pixel perfect. 83 | zoom_steps: [21]f32 = [_]f32{ 0.125, 0.167, 0.2, 0.25, 0.333, 0.5, 1, 2, 3, 4, 5, 6, 8, 12, 18, 28, 38, 50, 70, 90, 128 }, 84 | 85 | /// Amount of time it takes for the zoom correction. 86 | zoom_time: f32 = 0.2, 87 | 88 | /// Amount of time after zooming that the tooltip hangs around. 89 | zoom_tooltip_time: f32 = 0.6, 90 | 91 | /// Amount of time before zoom is corrected, increase if fighting while zooming slowly. 92 | zoom_wait_time: f32 = 0.1, 93 | 94 | /// Maximum file size 95 | max_file_size: [2]i32 = .{ 4096, 4096 }, 96 | 97 | /// Maximum number of recents before removing oldest 98 | max_recents: usize = 10, 99 | 100 | /// Automatically switch layers when using eyedropper tool 101 | eyedropper_auto_switch_layer: bool = true, 102 | 103 | /// Width and height of the eyedropper preview 104 | eyedropper_preview_size: f32 = 64.0, 105 | 106 | /// Drop shadow opacity (shows between artboard and flipbook) 107 | shadow_opacity: f32 = 0.1, 108 | 109 | /// Drop shadow length (shows between artboard and flipbook) 110 | shadow_length: f32 = 14.0, 111 | 112 | /// Stroke max size 113 | stroke_max_size: i32 = 64, 114 | 115 | /// Hue shift for suggested 116 | suggested_hue_shift: f32 = 0.25, 117 | 118 | /// Saturation shift for suggested colors 119 | suggested_sat_shift: f32 = 0.65, 120 | 121 | /// Lightness shift for suggested colors 122 | suggested_lit_shift: f32 = 0.75, 123 | 124 | /// Opacity of the reference window background 125 | reference_window_opacity: f32 = 50.0, 126 | 127 | /// Currently applied theme name 128 | theme: [:0]const u8, 129 | 130 | /// Temporary switch to allow ctrl on macos for zoom 131 | zoom_ctrl: bool = false, 132 | 133 | /// Setting to generate a compatiblity layer between pixi and level editors 134 | compatibility: Compatibility = .none, 135 | 136 | /// Radius of the color chips in palettes and suggested colors 137 | color_chip_radius: f32 = 12.0, 138 | 139 | /// Loads settings or if fails, returns default settings 140 | pub fn load(allocator: std.mem.Allocator) !Settings { 141 | if (pixi.fs.read(allocator, "settings.json") catch null) |data| { 142 | defer allocator.free(data); 143 | 144 | const options = std.json.ParseOptions{ 145 | .duplicate_field_behavior = .use_first, 146 | .ignore_unknown_fields = true, 147 | }; 148 | if (std.json.parseFromSlice(Settings, allocator, data, options) catch null) |p| { 149 | parsed = p; 150 | return p.value; 151 | } 152 | } 153 | 154 | return .{ 155 | .theme = try allocator.dupeZ(u8, "pixi_dark.json"), 156 | }; 157 | } 158 | 159 | pub fn save(settings: *Settings, allocator: std.mem.Allocator) !void { 160 | const str = try std.json.stringifyAlloc(allocator, settings, .{}); 161 | defer allocator.free(str); 162 | 163 | var file = try std.fs.cwd().createFile("settings.json", .{}); 164 | defer file.close(); 165 | 166 | try file.writeAll(str); 167 | } 168 | 169 | pub fn deinit(settings: *Settings, allocator: std.mem.Allocator) void { 170 | defer parsed = null; 171 | if (parsed) |p| p.deinit() else allocator.free(settings.theme); 172 | } 173 | -------------------------------------------------------------------------------- /src/editor/Sidebar.zig: -------------------------------------------------------------------------------- 1 | const pixi = @import("../pixi.zig"); 2 | const Core = @import("mach").Core; 3 | 4 | const App = pixi.App; 5 | const Editor = pixi.Editor; 6 | 7 | const Pane = @import("explorer/Explorer.zig").Pane; 8 | 9 | const imgui = @import("zig-imgui"); 10 | 11 | pub const Sidebar = @This(); 12 | 13 | pub const mach_module = .sidebar; 14 | pub const mach_systems = .{ .init, .deinit, .draw }; 15 | 16 | pub fn init(sidebar: *Sidebar) !void { 17 | sidebar.* = .{}; 18 | } 19 | 20 | pub fn deinit() void { 21 | // TODO: Free memory 22 | } 23 | 24 | pub fn draw(app: *App, editor: *Editor) !void { 25 | imgui.pushStyleVar(imgui.StyleVar_WindowRounding, 0.0); 26 | defer imgui.popStyleVar(); 27 | imgui.setNextWindowPos(.{ 28 | .x = 0.0, 29 | .y = 0.0, 30 | }, imgui.Cond_Always); 31 | imgui.setNextWindowSize(.{ 32 | .x = editor.settings.sidebar_width, 33 | .y = app.window_size[1], 34 | }, imgui.Cond_None); 35 | imgui.pushStyleVarImVec2(imgui.StyleVar_SelectableTextAlign, .{ .x = 0.5, .y = 0.5 }); 36 | imgui.pushStyleColorImVec4(imgui.Col_Header, editor.theme.foreground.toImguiVec4()); 37 | imgui.pushStyleColorImVec4(imgui.Col_WindowBg, editor.theme.foreground.toImguiVec4()); 38 | defer imgui.popStyleVar(); 39 | defer imgui.popStyleColorEx(2); 40 | 41 | var sidebar_flags: imgui.WindowFlags = 0; 42 | sidebar_flags |= imgui.WindowFlags_NoTitleBar; 43 | sidebar_flags |= imgui.WindowFlags_NoResize; 44 | sidebar_flags |= imgui.WindowFlags_NoMove; 45 | sidebar_flags |= imgui.WindowFlags_NoCollapse; 46 | sidebar_flags |= imgui.WindowFlags_NoScrollbar; 47 | sidebar_flags |= imgui.WindowFlags_NoScrollWithMouse; 48 | sidebar_flags |= imgui.WindowFlags_NoBringToFrontOnFocus; 49 | 50 | if (imgui.begin("Sidebar", null, sidebar_flags)) { 51 | imgui.pushStyleColorImVec4(imgui.Col_HeaderHovered, editor.theme.foreground.toImguiVec4()); 52 | imgui.pushStyleColorImVec4(imgui.Col_HeaderActive, editor.theme.foreground.toImguiVec4()); 53 | defer imgui.popStyleColorEx(2); 54 | 55 | drawOption(.files, pixi.fa.folder_open, editor); 56 | drawOption(.tools, pixi.fa.pencil_alt, editor); 57 | drawOption(.sprites, pixi.fa.th, editor); 58 | drawOption(.animations, pixi.fa.play_circle, editor); 59 | drawOption(.keyframe_animations, pixi.fa.key, editor); 60 | drawOption(.pack, pixi.fa.box_open, editor); 61 | drawOption(.settings, pixi.fa.cog, editor); 62 | } 63 | 64 | imgui.end(); 65 | } 66 | 67 | fn drawOption(option: Pane, icon: [:0]const u8, editor: *Editor) void { 68 | const position = imgui.getCursorPos(); 69 | const selectable_width = (editor.settings.sidebar_width - 8); 70 | const selectable_height = (editor.settings.sidebar_width - 8); 71 | imgui.dummy(.{ 72 | .x = selectable_width, 73 | .y = selectable_height, 74 | }); 75 | 76 | imgui.setCursorPos(position); 77 | if (editor.explorer.pane == option) { 78 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.highlight_primary.toImguiVec4()); 79 | } else if (imgui.isItemHovered(imgui.HoveredFlags_None)) { 80 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.text.toImguiVec4()); 81 | } else { 82 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.text_secondary.toImguiVec4()); 83 | } 84 | 85 | const selectable_flags: imgui.SelectableFlags = imgui.SelectableFlags_DontClosePopups; 86 | if (imgui.selectableEx(icon, editor.explorer.pane == option, selectable_flags, .{ .x = selectable_width, .y = selectable_height })) { 87 | editor.explorer.pane = option; 88 | if (option == .sprites) 89 | editor.tools.set(.pointer); 90 | } 91 | imgui.popStyleColor(); 92 | } 93 | -------------------------------------------------------------------------------- /src/editor/Tools.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | const Self = @This(); 5 | 6 | pub const Tool = enum(u32) { 7 | pointer, 8 | pencil, 9 | eraser, 10 | animation, 11 | heightmap, 12 | bucket, 13 | selection, 14 | }; 15 | 16 | pub const Shape = enum(u32) { 17 | circle, 18 | square, 19 | }; 20 | 21 | current: Tool = .pointer, 22 | previous: Tool = .pointer, 23 | stroke_size: u8 = 1, 24 | stroke_shape: Shape = .circle, 25 | 26 | pub fn set(self: *Self, tool: Tool) void { 27 | if (self.current != tool) { 28 | if (pixi.editor.getFile(pixi.editor.open_file_index)) |file| { 29 | if (file.transform_texture != null and tool != .pointer) 30 | return; 31 | 32 | switch (tool) { 33 | .heightmap => { 34 | file.heightmap.enable(); 35 | if (file.heightmap.layer == null) 36 | return; 37 | }, 38 | .pointer => { 39 | file.heightmap.disable(); 40 | 41 | if (self.current == .selection) 42 | file.selection_layer.clear(true); 43 | }, 44 | else => {}, 45 | } 46 | } 47 | self.previous = self.current; 48 | self.current = tool; 49 | } 50 | } 51 | 52 | pub fn swap(self: *Self) void { 53 | const temp = self.current; 54 | self.current = self.previous; 55 | self.previous = temp; 56 | } 57 | -------------------------------------------------------------------------------- /src/editor/artboard/canvas_pack.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../../pixi.zig"); 3 | const Core = @import("mach").Core; 4 | const Editor = pixi.Editor; 5 | const Packer = pixi.Packer; 6 | const imgui = @import("zig-imgui"); 7 | 8 | pub const PackTexture = enum { 9 | texture, 10 | heightmap, 11 | }; 12 | 13 | pub fn draw(mode: PackTexture, editor: *Editor, packer: *Packer) void { 14 | if (switch (mode) { 15 | .texture => editor.atlas.texture, 16 | .heightmap => editor.atlas.heightmap, 17 | }) |*texture| { 18 | var canvas_flags: imgui.WindowFlags = 0; 19 | canvas_flags |= imgui.WindowFlags_HorizontalScrollbar; 20 | defer imgui.endChild(); 21 | if (imgui.beginChild( 22 | "PackerCanvas", 23 | .{ .x = 0.0, .y = 0.0 }, 24 | imgui.ChildFlags_None, 25 | canvas_flags, 26 | )) { 27 | const window_width = imgui.getWindowWidth(); 28 | const window_height = imgui.getWindowHeight(); 29 | const file_width = @as(f32, @floatFromInt(texture.width)); 30 | const file_height = @as(f32, @floatFromInt(texture.height)); 31 | 32 | var camera = &packer.camera; 33 | 34 | // Handle zooming, panning and extents 35 | { 36 | var sprite_camera: pixi.gfx.Camera = .{ 37 | .zoom = @min(window_width / file_width, window_height / file_height), 38 | }; 39 | sprite_camera.setNearestZoomFloor(); 40 | if (!camera.zoom_initialized) { 41 | camera.zoom_initialized = true; 42 | camera.zoom = sprite_camera.zoom; 43 | } 44 | sprite_camera.setNearestZoomFloor(); 45 | camera.min_zoom = @min(sprite_camera.zoom, 1.0); 46 | 47 | camera.processPanZoom(.packer); 48 | } 49 | 50 | const width: f32 = @floatFromInt(texture.width); 51 | const height: f32 = @floatFromInt(texture.height); 52 | 53 | const rect: [4]f32 = .{ -width / 2.0, -height / 2.0, width, height }; 54 | camera.drawTexture(texture, rect, 0xFFFFFFFF); 55 | camera.drawRect(rect, 2.0, pixi.editor.theme.text_secondary.toU32()); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/editor/artboard/flipbook/flipbook.zig: -------------------------------------------------------------------------------- 1 | pub const canvas = @import("canvas.zig"); 2 | pub const menu = @import("menu.zig"); 3 | pub const timeline = @import("timeline.zig"); 4 | -------------------------------------------------------------------------------- /src/editor/artboard/infobar.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const pixi = @import("../../pixi.zig"); 4 | const Core = @import("mach").Core; 5 | const Editor = pixi.Editor; 6 | 7 | const imgui = @import("zig-imgui"); 8 | 9 | const spacer: [:0]const u8 = " "; 10 | 11 | pub fn draw(editor: *Editor) void { 12 | imgui.pushStyleColorImVec4(imgui.Col_Text, pixi.editor.theme.foreground.toImguiVec4()); 13 | defer imgui.popStyleColor(); 14 | 15 | const h = imgui.getTextLineHeightWithSpacing() + 6.0; 16 | const y = (imgui.getContentRegionAvail().y - h) / 2; 17 | const spacing: f32 = 3.0; 18 | imgui.setCursorPosY(y); 19 | imgui.setCursorPosX(5.0); 20 | 21 | if (editor.folder) |path| { 22 | imgui.setCursorPosY(y + 2.0); 23 | imgui.textColored(editor.theme.foreground.toImguiVec4(), pixi.fa.folder_open); 24 | imgui.setCursorPosY(y); 25 | imgui.sameLineEx(0.0, spacing); 26 | imgui.text(path); 27 | 28 | imgui.sameLine(); 29 | imgui.text(spacer); 30 | imgui.sameLine(); 31 | } 32 | 33 | if (editor.getFile(editor.open_file_index)) |file| { 34 | imgui.setCursorPosY(y + spacing); 35 | imgui.textColored(editor.theme.foreground.toImguiVec4(), pixi.fa.chess_board); 36 | imgui.setCursorPosY(y); 37 | imgui.sameLineEx(0.0, spacing); 38 | imgui.text("%dpx by %dpx", file.width, file.height); 39 | 40 | imgui.sameLine(); 41 | imgui.text(spacer); 42 | imgui.sameLine(); 43 | 44 | imgui.setCursorPosY(y + spacing); 45 | imgui.textColored(editor.theme.foreground.toImguiVec4(), pixi.fa.border_all); 46 | imgui.setCursorPosY(y); 47 | imgui.sameLineEx(0.0, spacing); 48 | imgui.text("%dpx by %dpx", file.tile_width, file.tile_height); 49 | 50 | imgui.sameLine(); 51 | imgui.text(spacer); 52 | imgui.sameLine(); 53 | } 54 | 55 | if (editor.saving()) { 56 | imgui.setCursorPosY(y + spacing); 57 | imgui.textColored(editor.theme.foreground.toImguiVec4(), pixi.fa.save); 58 | imgui.setCursorPosY(y); 59 | imgui.sameLineEx(0.0, spacing); 60 | imgui.text("Saving!..."); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/editor/artboard/menu.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../../pixi.zig"); 3 | const Core = @import("mach").Core; 4 | const Editor = pixi.Editor; 5 | const settings = pixi.settings; 6 | const zstbi = @import("zstbi"); 7 | const nfd = @import("nfd"); 8 | const imgui = @import("zig-imgui"); 9 | 10 | pub fn draw(editor: *Editor) !void { 11 | imgui.pushStyleVarImVec2(imgui.StyleVar_WindowPadding, .{ .x = 10.0, .y = 10.0 }); 12 | imgui.pushStyleVarImVec2(imgui.StyleVar_ItemSpacing, .{ .x = 6.0, .y = 6.0 }); 13 | defer imgui.popStyleVarEx(2); 14 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.text_secondary.toImguiVec4()); 15 | imgui.pushStyleColorImVec4(imgui.Col_PopupBg, editor.theme.foreground.toImguiVec4()); 16 | imgui.pushStyleColorImVec4(imgui.Col_HeaderHovered, editor.theme.background.toImguiVec4()); 17 | imgui.pushStyleColorImVec4(imgui.Col_HeaderActive, editor.theme.background.toImguiVec4()); 18 | imgui.pushStyleColorImVec4(imgui.Col_Header, editor.theme.background.toImguiVec4()); 19 | defer imgui.popStyleColorEx(5); 20 | if (imgui.beginMenuBar()) { 21 | defer imgui.endMenuBar(); 22 | if (imgui.beginMenu("File")) { 23 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.text.toImguiVec4()); 24 | if (imgui.menuItemEx("Open Folder...", if (editor.hotkeys.hotkey(.{ .procedure = .open_folder })) |hotkey| hotkey.shortcut else "", false, true)) { 25 | editor.popups.file_dialog_request = .{ 26 | .state = .folder, 27 | .type = .project, 28 | }; 29 | } 30 | if (editor.popups.file_dialog_response) |response| { 31 | if (response.type == .project) { 32 | try editor.setProjectFolder(response.path); 33 | nfd.freePath(response.path); 34 | editor.popups.file_dialog_response = null; 35 | } 36 | } 37 | 38 | if (imgui.beginMenu("Recents")) { 39 | defer imgui.endMenu(); 40 | 41 | for (editor.recents.folders.items) |folder| { 42 | if (imgui.menuItem(folder)) { 43 | try editor.setProjectFolder(folder); 44 | } 45 | } 46 | } 47 | 48 | imgui.separator(); 49 | 50 | const file = editor.getFile(editor.open_file_index); 51 | 52 | if (imgui.menuItemEx( 53 | "Export as .png...", 54 | if (editor.hotkeys.hotkey(.{ .procedure = .export_png })) |hotkey| hotkey.shortcut else "", 55 | false, 56 | file != null, 57 | )) { 58 | editor.popups.print = true; 59 | } 60 | 61 | if (imgui.menuItemEx( 62 | "Save", 63 | if (editor.hotkeys.hotkey(.{ .procedure = .save })) |hotkey| hotkey.shortcut else "", 64 | false, 65 | file != null and file.?.dirty(), 66 | )) { 67 | if (file) |f| { 68 | try f.save(); 69 | } 70 | } 71 | 72 | imgui.popStyleColor(); 73 | imgui.endMenu(); 74 | } 75 | if (imgui.beginMenu("View")) { 76 | defer imgui.endMenu(); 77 | 78 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.text.toImguiVec4()); 79 | defer imgui.popStyleColor(); 80 | 81 | if (imgui.menuItemEx("Split Artboard", null, editor.settings.split_artboard, true)) { 82 | editor.settings.split_artboard = !editor.settings.split_artboard; 83 | } 84 | 85 | if (imgui.beginMenu("Flipbook")) { 86 | defer imgui.endMenu(); 87 | 88 | if (editor.getFile(editor.open_file_index)) |file| { 89 | if (imgui.beginCombo("Flipbook View", switch (file.flipbook_view) { 90 | .canvas => "Canvas", 91 | .timeline => "Timeline", 92 | }, imgui.ComboFlags_None)) { 93 | defer imgui.endCombo(); 94 | 95 | if (imgui.selectableEx("Canvas", file.flipbook_view == .canvas, imgui.SelectableFlags_None, .{ .x = 0.0, .y = 0.0 })) { 96 | file.flipbook_view = .canvas; 97 | } 98 | 99 | if (imgui.selectableEx("Timeline", file.flipbook_view == .timeline, imgui.SelectableFlags_None, .{ .x = 0.0, .y = 0.0 })) { 100 | file.flipbook_view = .timeline; 101 | } 102 | } 103 | 104 | if (file.flipbook_view == .canvas) { 105 | if (imgui.beginMenu("Flipbook Canvas View")) { 106 | defer imgui.endMenu(); 107 | if (imgui.menuItemEx("Sequential", null, editor.settings.flipbook_view == .sequential, true)) { 108 | editor.settings.flipbook_view = .sequential; 109 | } 110 | 111 | if (imgui.menuItemEx("Grid", null, editor.settings.flipbook_view == .grid, true)) { 112 | editor.settings.flipbook_view = .grid; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | if (imgui.menuItemEx("References", "r", editor.popups.references, true)) { 120 | editor.popups.references = !editor.popups.references; 121 | } 122 | } 123 | if (imgui.beginMenu("Edit")) { 124 | defer imgui.endMenu(); 125 | 126 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.text.toImguiVec4()); 127 | defer imgui.popStyleColor(); 128 | 129 | if (editor.getFile(editor.open_file_index)) |file| { 130 | if (imgui.menuItemEx( 131 | "Undo", 132 | if (editor.hotkeys.hotkey(.{ .procedure = .undo })) |hotkey| hotkey.shortcut else "", 133 | false, 134 | file.history.undo_stack.items.len > 0, 135 | )) 136 | try file.undo(); 137 | 138 | if (imgui.menuItemEx( 139 | "Redo", 140 | if (editor.hotkeys.hotkey(.{ .procedure = .redo })) |hotkey| hotkey.shortcut else "", 141 | false, 142 | file.history.redo_stack.items.len > 0, 143 | )) 144 | try file.redo(); 145 | } 146 | } 147 | if (imgui.menuItem("About")) { 148 | editor.popups.about = true; 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/editor/artboard/rulers.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../../pixi.zig"); 3 | 4 | const Core = @import("mach").Core; 5 | const App = pixi.App; 6 | const Editor = pixi.Editor; 7 | const imgui = @import("zig-imgui"); 8 | 9 | pub fn draw(file: *pixi.Internal.File, editor: *Editor) !void { 10 | const file_width = @as(f32, @floatFromInt(file.width)); 11 | const file_height = @as(f32, @floatFromInt(file.height)); 12 | const tile_width = @as(f32, @floatFromInt(file.tile_width)); 13 | const tile_height = @as(f32, @floatFromInt(file.tile_height)); 14 | const tiles_wide = @divExact(file.width, file.tile_width); 15 | const tiles_tall = @divExact(file.height, file.tile_height); 16 | const text_size = imgui.calcTextSize("0"); 17 | const layer_position: [2]f32 = .{ 18 | -file_width / 2, 19 | -file_height / 2, 20 | }; 21 | if (imgui.beginChild("TopRuler", .{ .x = 0.0, .y = 0.0 }, imgui.ChildFlags_None, imgui.WindowFlags_ChildWindow)) { 22 | const window_tl = imgui.getCursorScreenPos(); 23 | const layer_tl = file.camera.matrix().transformVec2(layer_position); 24 | const line_length = imgui.getWindowHeight() / 2.0; 25 | const tl: [2]f32 = .{ window_tl.x + layer_tl[0] + imgui.getTextLineHeightWithSpacing() * 1.5 / 2, window_tl.y }; 26 | 27 | if (imgui.getWindowDrawList()) |draw_list| { 28 | var i: usize = 0; 29 | while (i < @as(usize, @intCast(tiles_wide))) : (i += 1) { 30 | const offset = .{ (@as(f32, @floatFromInt(i)) * tile_width) * file.camera.zoom, 0.0 }; 31 | if (tile_width * file.camera.zoom > text_size.x * 4.0) { 32 | const text = try std.fmt.allocPrintZ(editor.arena.allocator(), "{d}", .{i}); 33 | 34 | draw_list.addText( 35 | .{ .x = tl[0] + offset[0] + (tile_width / 2.0 * file.camera.zoom) - (text_size.x / 2.0), .y = tl[1] + 4.0 }, 36 | pixi.editor.theme.text_secondary.toU32(), 37 | text.ptr, 38 | ); 39 | } 40 | draw_list.addLineEx( 41 | .{ .x = tl[0] + offset[0], .y = tl[1] + line_length / 2.0 }, 42 | .{ .x = tl[0] + offset[0], .y = tl[1] + line_length / 2.0 + line_length }, 43 | pixi.editor.theme.text_secondary.toU32(), 44 | 1.0, 45 | ); 46 | } 47 | draw_list.addLineEx( 48 | .{ .x = tl[0] + file_width * file.camera.zoom, .y = tl[1] + line_length / 2.0 }, 49 | .{ .x = tl[0] + file_width * file.camera.zoom, .y = tl[1] + line_length / 2.0 + line_length }, 50 | pixi.editor.theme.text_secondary.toU32(), 51 | 1.0, 52 | ); 53 | } 54 | imgui.endChild(); 55 | } 56 | 57 | if (imgui.beginChild("SideRuler", .{ .x = 0.0, .y = 0.0 }, imgui.ChildFlags_None, imgui.WindowFlags_ChildWindow)) { 58 | const window_tl = imgui.getCursorScreenPos(); 59 | const layer_tl = file.camera.matrix().transformVec2(layer_position); 60 | const tl: [2]f32 = .{ window_tl.x + (text_size.x / 2.0), window_tl.y + layer_tl[1] + 1.0 }; 61 | 62 | if (imgui.getWindowDrawList()) |draw_list| { 63 | var i: usize = 0; 64 | while (i < @as(usize, @intCast(tiles_tall))) : (i += 1) { 65 | const offset = .{ 0.0, @as(f32, @floatFromInt(i)) * tile_height * file.camera.zoom }; 66 | 67 | if (tile_height * file.camera.zoom > text_size.x * 4.0) { 68 | const text = try std.fmt.allocPrintZ(editor.arena.allocator(), "{d}", .{i}); 69 | 70 | draw_list.addText( 71 | .{ .x = tl[0], .y = tl[1] + offset[1] + (tile_height / 2.0 * file.camera.zoom) - (text_size.y / 2.0) }, 72 | pixi.editor.theme.text_secondary.toU32(), 73 | text.ptr, 74 | ); 75 | } 76 | draw_list.addLineEx( 77 | .{ .x = tl[0], .y = tl[1] + offset[1] }, 78 | .{ .x = tl[0] + imgui.getWindowWidth() / 2.0, .y = tl[1] + offset[1] }, 79 | pixi.editor.theme.text_secondary.toU32(), 80 | 1.0, 81 | ); 82 | } 83 | draw_list.addLineEx( 84 | .{ .x = tl[0], .y = tl[1] + file_height * file.camera.zoom }, 85 | .{ .x = tl[0] + imgui.getWindowWidth() / 2.0, .y = tl[1] + file_height * file.camera.zoom }, 86 | pixi.editor.theme.text_secondary.toU32(), 87 | 1.0, 88 | ); 89 | } 90 | imgui.endChild(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/editor/explorer/sprites.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const pixi = @import("../../pixi.zig"); 4 | const Editor = pixi.Editor; 5 | 6 | const imgui = @import("zig-imgui"); 7 | 8 | pub fn draw(editor: *Editor) !void { 9 | if (editor.getFile(editor.open_file_index)) |file| { 10 | imgui.pushStyleColorImVec4(imgui.Col_Header, editor.theme.background.toImguiVec4()); 11 | imgui.pushStyleColorImVec4(imgui.Col_HeaderHovered, editor.theme.background.toImguiVec4()); 12 | imgui.pushStyleColorImVec4(imgui.Col_HeaderActive, editor.theme.background.toImguiVec4()); 13 | defer imgui.popStyleColorEx(3); 14 | 15 | imgui.pushStyleVarImVec2(imgui.StyleVar_ItemSpacing, .{ .x = 4.0, .y = 4.0 }); 16 | imgui.pushStyleVarImVec2(imgui.StyleVar_SelectableTextAlign, .{ .x = 0.5, .y = 0.8 }); 17 | imgui.pushStyleVarImVec2(imgui.StyleVar_FramePadding, .{ .x = 6.0, .y = 6.0 }); 18 | defer imgui.popStyleVarEx(3); 19 | 20 | const selection = file.selected_sprites.items.len > 0; 21 | 22 | if (imgui.collapsingHeader(pixi.fa.wrench ++ " Tools", imgui.TreeNodeFlags_DefaultOpen)) { 23 | imgui.indent(); 24 | defer imgui.unindent(); 25 | 26 | if (imgui.beginChild("Sprite", .{ 27 | .x = -1.0, 28 | .y = editor.settings.sprite_edit_height, 29 | }, imgui.ChildFlags_None, imgui.WindowFlags_ChildWindow)) { 30 | defer imgui.endChild(); 31 | 32 | if (!selection) { 33 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.text_background.toImguiVec4()); 34 | defer imgui.popStyleColor(); 35 | imgui.textWrapped("Make a selection to begin editing sprite origins."); 36 | } else { 37 | imgui.pushStyleColorImVec4(imgui.Col_Button, editor.theme.background.toImguiVec4()); 38 | imgui.pushStyleColorImVec4(imgui.Col_ButtonHovered, editor.theme.foreground.toImguiVec4()); 39 | imgui.pushStyleColorImVec4(imgui.Col_ButtonActive, editor.theme.background.toImguiVec4()); 40 | defer imgui.popStyleColorEx(3); 41 | var x_same: bool = true; 42 | var y_same: bool = true; 43 | const first_sprite = file.sprites.slice().get(file.selected_sprites.items[0]); 44 | var origin_x: f32 = first_sprite.origin[0]; 45 | var origin_y: f32 = first_sprite.origin[1]; 46 | const tile_width = @as(f32, @floatFromInt(file.tile_width)); 47 | const tile_height = @as(f32, @floatFromInt(file.tile_height)); 48 | 49 | for (file.selected_sprites.items) |selected_index| { 50 | const sprite = file.sprites.slice().get(selected_index); 51 | if (origin_x != sprite.origin[0]) { 52 | x_same = false; 53 | } 54 | if (origin_y != sprite.origin[1]) { 55 | y_same = false; 56 | } 57 | 58 | if (!x_same and !y_same) { 59 | break; 60 | } 61 | } 62 | 63 | const label_origin_x = "X " ++ if (x_same) pixi.fa.link else pixi.fa.unlink; 64 | var changed_origin_x: bool = false; 65 | if (imgui.sliderFloatEx(label_origin_x, &origin_x, 0.0, tile_width, "%.0f", imgui.SliderFlags_None)) { 66 | changed_origin_x = true; 67 | } 68 | 69 | if (imgui.isItemActivated()) { 70 | try file.newHistorySelectedSprites(.origins); 71 | } 72 | 73 | if (changed_origin_x) 74 | file.setSelectedSpritesOriginX(origin_x); 75 | 76 | const label_origin_y = "Y " ++ if (y_same) pixi.fa.link else pixi.fa.unlink; 77 | var changed_origin_y: bool = false; 78 | if (imgui.sliderFloatEx(label_origin_y, &origin_y, 0.0, tile_height, "%.0f", imgui.SliderFlags_None)) { 79 | changed_origin_y = true; 80 | } 81 | 82 | if (imgui.isItemActivated()) { 83 | try file.newHistorySelectedSprites(.origins); 84 | } 85 | 86 | if (changed_origin_y) { 87 | file.setSelectedSpritesOriginY(origin_y); 88 | } 89 | 90 | if (imgui.buttonEx(" Center ", .{ .x = -1.0, .y = 0.0 })) { 91 | try file.newHistorySelectedSprites(.origins); 92 | file.setSelectedSpritesOrigin(.{ tile_width / 2.0, tile_height / 2.0 }); 93 | } 94 | } 95 | } 96 | } 97 | 98 | if (imgui.collapsingHeader(pixi.fa.th ++ " Sprites", imgui.TreeNodeFlags_DefaultOpen)) { 99 | imgui.pushStyleVarImVec2(imgui.StyleVar_FramePadding, .{ .x = 2.0, .y = 5.0 }); 100 | defer imgui.popStyleVar(); 101 | if (imgui.beginChild("Sprites", .{ .x = 0.0, .y = 0.0 }, imgui.ChildFlags_None, imgui.WindowFlags_ChildWindow)) { 102 | defer imgui.endChild(); 103 | 104 | var sprite_index: usize = 0; 105 | while (sprite_index < file.sprites.slice().len) : (sprite_index += 1) { 106 | const selected_sprite_index = file.spriteSelectionIndex(sprite_index); 107 | const contains = selected_sprite_index != null; 108 | const color = if (contains) editor.theme.text.toImguiVec4() else editor.theme.text_secondary.toImguiVec4(); 109 | imgui.pushStyleColorImVec4(imgui.Col_Text, color); 110 | defer imgui.popStyleColor(); 111 | 112 | const name = try file.calculateSpriteName(editor.arena.allocator(), sprite_index); 113 | 114 | if (imgui.selectableEx(name, contains, imgui.SelectableFlags_None, .{ .x = 0.0, .y = 0.0 })) { 115 | try file.makeSpriteSelection(sprite_index); 116 | } 117 | } 118 | } 119 | } 120 | } else { 121 | imgui.pushStyleColorImVec4(imgui.Col_Text, editor.theme.text_background.toImguiVec4()); 122 | imgui.textWrapped("Open a file to begin editing."); 123 | imgui.popStyleColor(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/editor/popups/about.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../../pixi.zig"); 3 | const imgui = @import("zig-imgui"); 4 | 5 | var timer: f32 = 0.0; 6 | var sprite_index: usize = 0; 7 | 8 | pub fn draw(editor: *pixi.Editor, assets: *pixi.Assets) !void { 9 | if (editor.popups.about) { 10 | imgui.openPopup("About", imgui.PopupFlags_None); 11 | } else return; 12 | 13 | if (timer <= 1.0) 14 | timer += pixi.app.delta_time 15 | else 16 | timer = 0.0; 17 | 18 | const texture = assets.getTexture(pixi.app.texture_id); 19 | const atlas = assets.getAtlas(pixi.app.atlas_id); 20 | 21 | const popup_width = 450; 22 | const popup_height = 450; 23 | 24 | const window_size = pixi.app.window_size; 25 | const window_center: [2]f32 = .{ window_size[0] / 2.0, window_size[1] / 2.0 }; 26 | 27 | imgui.setNextWindowPos(.{ 28 | .x = window_center[0] - popup_width / 2.0, 29 | .y = window_center[1] - popup_height / 2.0, 30 | }, imgui.Cond_None); 31 | imgui.setNextWindowSize(.{ 32 | .x = popup_width, 33 | .y = popup_height, 34 | }, imgui.Cond_None); 35 | 36 | var modal_flags: imgui.WindowFlags = 0; 37 | modal_flags |= imgui.WindowFlags_NoResize; 38 | modal_flags |= imgui.WindowFlags_NoCollapse; 39 | 40 | if (imgui.beginPopupModal( 41 | "About", 42 | &editor.popups.about, 43 | modal_flags, 44 | )) { 45 | defer imgui.endPopup(); 46 | imgui.spacing(); 47 | 48 | const fox_sprite = atlas.sprites[pixi.atlas.sprites.fox_default_0]; 49 | 50 | const src: [4]f32 = .{ 51 | @floatFromInt(fox_sprite.source[0]), 52 | @floatFromInt(fox_sprite.source[1]), 53 | @floatFromInt(fox_sprite.source[2]), 54 | @floatFromInt(fox_sprite.source[3]), 55 | }; 56 | 57 | const w = src[2] * 4.0; 58 | const h = src[3] * 4.0; 59 | const center: [2]f32 = .{ imgui.getWindowWidth() / 2.0, imgui.getWindowHeight() / 4.0 }; 60 | 61 | imgui.setCursorPosX(center[0] - w / 2.0); 62 | imgui.setCursorPosY(center[1] - h / 2.0); 63 | imgui.dummy(.{ .x = w, .y = h }); 64 | 65 | const dummy_pos = imgui.getItemRectMin(); 66 | 67 | const draw_list_opt = imgui.getWindowDrawList(); 68 | 69 | if (draw_list_opt) |draw_list| { 70 | draw_list.addCircleFilled( 71 | .{ .x = dummy_pos.x + w / 2, .y = dummy_pos.y + w / 2 }, 72 | w / 1.5, 73 | editor.theme.foreground.toU32(), 74 | 32, 75 | ); 76 | } 77 | 78 | const inv_w = 1.0 / @as(f32, @floatFromInt(texture.width)); 79 | const inv_h = 1.0 / @as(f32, @floatFromInt(texture.height)); 80 | 81 | imgui.setCursorPosX(center[0] - w / 2.0); 82 | imgui.setCursorPosY(center[1] - h / 6.0); 83 | imgui.imageEx( 84 | texture.texture_view, 85 | .{ .x = w, .y = h }, 86 | .{ .x = src[0] * inv_w, .y = src[1] * inv_h }, 87 | .{ .x = (src[0] + src[2]) * inv_w, .y = (src[1] + src[3]) * inv_h }, 88 | .{ .x = 1.0, .y = 1.0, .z = 1.0, .w = 1.0 }, 89 | .{ .x = 0.0, .y = 0.0, .z = 0.0, .w = 0.0 }, 90 | ); 91 | 92 | imgui.dummy(.{ .x = w, .y = h }); 93 | 94 | centerText("pixi.Editor"); 95 | centerText("https://github.com/foxnne/pixi"); 96 | 97 | const version = try std.fmt.allocPrintZ(editor.arena.allocator(), "Version {d}.{d}.{d}", .{ pixi.version.major, pixi.version.minor, pixi.version.patch }); 98 | 99 | centerText(version); 100 | 101 | imgui.pushStyleColorImVec4(imgui.Col_Text, pixi.editor.theme.text_background.toImguiVec4()); 102 | defer imgui.popStyleColor(); 103 | 104 | imgui.spacing(); 105 | imgui.spacing(); 106 | imgui.spacing(); 107 | imgui.spacing(); 108 | imgui.spacing(); 109 | imgui.spacing(); 110 | centerText("Credits"); 111 | centerText("__________________"); 112 | imgui.spacing(); 113 | imgui.spacing(); 114 | 115 | centerText("mach-core"); 116 | centerText("https://github.com/hexops/mach-core"); 117 | 118 | imgui.spacing(); 119 | imgui.spacing(); 120 | 121 | centerText("zig-gamedev"); 122 | centerText("https://github.com/michal-z/zig-gamedev"); 123 | 124 | imgui.spacing(); 125 | imgui.spacing(); 126 | 127 | centerText("zip"); 128 | centerText("https://github.com/kuba--/zip"); 129 | 130 | imgui.spacing(); 131 | imgui.spacing(); 132 | 133 | centerText("nfd-zig"); 134 | centerText("https://github.com/fabioarnold/nfd-zig"); 135 | } 136 | } 137 | 138 | fn centerText(text: [:0]const u8) void { 139 | const center = imgui.getWindowWidth() / 2.0; 140 | const text_width = imgui.calcTextSize(text).x; 141 | imgui.setCursorPosX(center - text_width / 2.0); 142 | imgui.text(text); 143 | } 144 | -------------------------------------------------------------------------------- /src/editor/popups/animation.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const pixi = @import("../../pixi.zig"); 4 | const Editor = pixi.Editor; 5 | 6 | const imgui = @import("zig-imgui"); 7 | 8 | const History = pixi.Internal.File.History; 9 | 10 | pub fn draw(editor: *Editor) !void { 11 | if (editor.getFile(editor.open_file_index)) |file| { 12 | const dialog_name = switch (editor.popups.animation_state) { 13 | .none => "None...", 14 | .create => "Create animation...", 15 | .edit => "Edit animation...", 16 | }; 17 | 18 | if (editor.popups.animation) { 19 | imgui.openPopup(dialog_name, imgui.PopupFlags_None); 20 | } else return; 21 | 22 | const popup_width: f32 = 350; 23 | const popup_height: f32 = 115; 24 | 25 | const window_size = pixi.app.window_size; 26 | const window_center: [2]f32 = .{ window_size[0] / 2.0, window_size[1] / 2.0 }; 27 | 28 | imgui.setNextWindowPos(.{ 29 | .x = window_center[0] - popup_width / 2.0, 30 | .y = window_center[1] - popup_height / 2.0, 31 | }, imgui.Cond_None); 32 | imgui.setNextWindowSize(.{ 33 | .x = popup_width, 34 | .y = 0.0, 35 | }, imgui.Cond_None); 36 | 37 | var modal_flags: imgui.WindowFlags = 0; 38 | modal_flags |= imgui.WindowFlags_NoResize; 39 | modal_flags |= imgui.WindowFlags_NoCollapse; 40 | 41 | if (imgui.beginPopupModal( 42 | dialog_name, 43 | &editor.popups.animation, 44 | modal_flags, 45 | )) { 46 | defer imgui.endPopup(); 47 | imgui.spacing(); 48 | 49 | const style = imgui.getStyle(); 50 | const spacing = style.item_spacing.x; 51 | const full_width = popup_width - (style.frame_padding.x * 2.0) - imgui.calcTextSize("Name").x; 52 | const half_width = (popup_width - (style.frame_padding.x * 2.0) - spacing) / 2.0; 53 | 54 | var input_text_flags: imgui.InputTextFlags = 0; 55 | input_text_flags |= imgui.InputTextFlags_AutoSelectAll; 56 | input_text_flags |= imgui.InputTextFlags_EnterReturnsTrue; 57 | 58 | imgui.pushItemWidth(full_width); 59 | const enter = imgui.inputText( 60 | "Name", 61 | editor.popups.animation_name[0.. :0], 62 | editor.popups.animation_name[0.. :0].len, 63 | input_text_flags, 64 | ); 65 | 66 | imgui.spacing(); 67 | if (editor.popups.animation_state == .create) { 68 | var fps = @as(i32, @intCast(editor.popups.animation_fps)); 69 | if (imgui.sliderInt("FPS", &fps, 1, 60)) { 70 | editor.popups.animation_fps = @as(usize, @intCast(fps)); 71 | } 72 | imgui.spacing(); 73 | } 74 | 75 | imgui.separator(); 76 | if (imgui.buttonEx("Cancel", .{ .x = half_width, .y = 0.0 })) { 77 | editor.popups.animation = false; 78 | } 79 | imgui.sameLine(); 80 | if (imgui.buttonEx("Ok", .{ .x = half_width, .y = 0.0 }) or enter) { 81 | switch (editor.popups.animation_state) { 82 | .create => { 83 | const name = std.mem.trimRight(u8, &editor.popups.animation_name, "\u{0}"); 84 | 85 | if (std.mem.indexOf(u8, name, "\u{0}")) |index| { 86 | try file.createAnimation( 87 | name[0..index], 88 | editor.popups.animation_fps, 89 | editor.popups.animation_start, 90 | editor.popups.animation_length, 91 | ); 92 | } else { 93 | try file.createAnimation( 94 | name, 95 | editor.popups.animation_fps, 96 | editor.popups.animation_start, 97 | editor.popups.animation_length, 98 | ); 99 | } 100 | }, 101 | .edit => { 102 | const name = std.mem.trimRight(u8, &editor.popups.animation_name, "\u{0}"); 103 | if (std.mem.indexOf(u8, name, "\u{0}")) |index| { 104 | try file.renameAnimation(name[0..index], editor.popups.animation_index); 105 | } else { 106 | try file.renameAnimation(name, editor.popups.animation_index); 107 | } 108 | }, 109 | else => unreachable, 110 | } 111 | editor.popups.animation_state = .none; 112 | editor.popups.animation = false; 113 | } 114 | 115 | imgui.popItemWidth(); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/editor/popups/file_confirm_close.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../../pixi.zig"); 3 | const Editor = pixi.Editor; 4 | const imgui = @import("zig-imgui"); 5 | 6 | pub fn draw(editor: *Editor) !void { 7 | if (editor.popups.file_confirm_close) { 8 | imgui.openPopup("Confirm close...", imgui.PopupFlags_None); 9 | } else return; 10 | 11 | const popup_width: f32 = 350; 12 | const popup_height: f32 = if (editor.popups.file_confirm_close_state == .one) 120 else 250; 13 | 14 | const window_size = pixi.app.window_size; 15 | const window_center: [2]f32 = .{ window_size[0] / 2.0, window_size[1] / 2.0 }; 16 | 17 | imgui.setNextWindowPos(.{ 18 | .x = window_center[0] - popup_width / 2.0, 19 | .y = window_center[1] - popup_height / 2.0, 20 | }, imgui.Cond_None); 21 | imgui.setNextWindowSize(.{ 22 | .x = popup_width, 23 | .y = popup_height, 24 | }, imgui.Cond_None); 25 | 26 | var modal_flags: imgui.WindowFlags = 0; 27 | modal_flags |= imgui.WindowFlags_NoResize; 28 | modal_flags |= imgui.WindowFlags_NoCollapse; 29 | 30 | if (imgui.beginPopupModal( 31 | "Confirm close...", 32 | &editor.popups.file_confirm_close, 33 | modal_flags, 34 | )) { 35 | defer imgui.endPopup(); 36 | imgui.spacing(); 37 | 38 | const style = imgui.getStyle(); 39 | const spacing = style.item_spacing.x; 40 | const full_width = popup_width - (style.frame_padding.x * 2.0) - imgui.calcTextSize("Name").x; 41 | const third_width = (popup_width - (style.frame_padding.x * 2.0) - spacing * 2.0) / 3.0; 42 | 43 | switch (pixi.editor.popups.file_confirm_close_state) { 44 | .one => { 45 | if (editor.getFile(editor.popups.file_confirm_close_index)) |file| { 46 | const base_name = std.fs.path.basename(file.path); 47 | const file_name = try std.fmt.allocPrintZ(pixi.app.allocator, "The file {s} has unsaved changes, are you sure you want to close?", .{base_name}); 48 | defer pixi.app.allocator.free(file_name); 49 | imgui.textWrapped(file_name); 50 | } 51 | }, 52 | .all => { 53 | imgui.textWrapped("The following files have unsaved changes, are you sure you want to close?"); 54 | imgui.spacing(); 55 | if (imgui.beginChild( 56 | "OpenFileArea", 57 | .{ .x = 0.0, .y = 120 }, 58 | imgui.ChildFlags_None, 59 | imgui.WindowFlags_None, 60 | )) { 61 | defer imgui.endChild(); 62 | for (editor.open_files.items) |file| { 63 | const base_name = std.fs.path.basename(file.path); 64 | 65 | const base_name_z = try std.fmt.allocPrintZ(pixi.app.allocator, "{s}", .{base_name}); 66 | defer pixi.app.allocator.free(base_name_z); 67 | 68 | if (file.dirty()) imgui.bulletText(base_name_z); 69 | } 70 | } 71 | }, 72 | else => unreachable, 73 | } 74 | 75 | imgui.separator(); 76 | 77 | imgui.setCursorPosY(popup_height - imgui.getTextLineHeightWithSpacing() * 2.0); 78 | 79 | imgui.pushItemWidth(full_width); 80 | if (imgui.buttonEx("Cancel", .{ .x = third_width, .y = 0.0 })) { 81 | pixi.editor.popups.file_confirm_close = false; 82 | if (editor.popups.file_confirm_close_exit) 83 | editor.popups.file_confirm_close_exit = false; 84 | } 85 | imgui.sameLine(); 86 | if (imgui.buttonEx(if (pixi.editor.popups.file_confirm_close_state == .one) "Close" else "Close All", .{ .x = third_width, .y = 0.0 })) { 87 | switch (pixi.editor.popups.file_confirm_close_state) { 88 | .one => { 89 | try editor.forceCloseFile(editor.popups.file_confirm_close_index); 90 | }, 91 | .all => { 92 | const len = editor.open_files.items.len; 93 | var i: usize = 0; 94 | while (i < len) : (i += 1) { 95 | try editor.forceCloseFile(0); 96 | } 97 | }, 98 | else => unreachable, 99 | } 100 | editor.popups.file_confirm_close = false; 101 | } 102 | imgui.sameLine(); 103 | if (imgui.buttonEx(if (pixi.editor.popups.file_confirm_close_state == .one) "Save & Close" else "Save & Close All", .{ .x = third_width, .y = 0.0 })) { 104 | switch (pixi.editor.popups.file_confirm_close_state) { 105 | .one => { 106 | try editor.save(); 107 | try editor.closeFile(editor.popups.file_confirm_close_index); 108 | }, 109 | .all => { 110 | try editor.saveAllFiles(); 111 | try editor.forceCloseAllFiles(); 112 | }, 113 | else => unreachable, 114 | } 115 | 116 | pixi.editor.popups.file_confirm_close = false; 117 | } 118 | 119 | if (editor.popups.file_confirm_close_exit and !editor.popups.file_confirm_close) { 120 | editor.popups.file_confirm_close_exit = false; 121 | pixi.app.should_close = true; 122 | } 123 | 124 | imgui.popItemWidth(); 125 | } 126 | if (!editor.popups.file_confirm_close) 127 | editor.popups.file_confirm_close_exit = false; 128 | } 129 | -------------------------------------------------------------------------------- /src/editor/popups/folder.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../../pixi.zig"); 3 | 4 | const App = pixi.App; 5 | 6 | const imgui = @import("zig-imgui"); 7 | 8 | const Popups = @import("Popups.zig"); 9 | const Editor = pixi.Editor; 10 | 11 | pub fn draw(popups: *Popups, app: *App) !void { 12 | const dialog_name = "New Folder..."; 13 | 14 | if (popups.folder) { 15 | imgui.openPopup(dialog_name, imgui.PopupFlags_None); 16 | } else return; 17 | 18 | const popup_width: f32 = 350; 19 | const popup_height: f32 = 115; 20 | 21 | const window_size = app.window_size; 22 | const window_center: [2]f32 = .{ window_size[0] / 2.0, window_size[1] / 2.0 }; 23 | 24 | imgui.setNextWindowPos(.{ 25 | .x = window_center[0] - popup_width / 2.0, 26 | .y = window_center[1] - popup_height / 2.0, 27 | }, imgui.Cond_None); 28 | imgui.setNextWindowSize(.{ 29 | .x = popup_width, 30 | .y = 0.0, 31 | }, imgui.Cond_None); 32 | 33 | var modal_flags: imgui.WindowFlags = 0; 34 | modal_flags |= imgui.WindowFlags_NoResize; 35 | modal_flags |= imgui.WindowFlags_NoCollapse; 36 | 37 | if (imgui.beginPopupModal(dialog_name, &popups.folder, modal_flags)) { 38 | defer imgui.endPopup(); 39 | imgui.spacing(); 40 | 41 | const style = imgui.getStyle(); 42 | const spacing = style.item_spacing.x; 43 | const full_width = popup_width - (style.frame_padding.x * 2.0) - (imgui.calcTextSize("Name").x + 10.0); 44 | const half_width = (popup_width - (style.frame_padding.x * 2.0) - spacing) / 2.0; 45 | 46 | const base_name = std.fs.path.basename(popups.folder_path[0..]); 47 | var base_index: usize = 0; 48 | if (std.mem.indexOf(u8, popups.folder_path[0..], base_name)) |index| { 49 | base_index = index; 50 | } 51 | imgui.pushItemWidth(full_width); 52 | 53 | var input_text_flags: imgui.InputTextFlags = 0; 54 | input_text_flags |= imgui.InputTextFlags_AutoSelectAll; 55 | input_text_flags |= imgui.InputTextFlags_EnterReturnsTrue; 56 | 57 | const enter = imgui.inputText( 58 | "Name", 59 | popups.folder_path[base_index.. :0], 60 | popups.folder_path[base_index.. :0].len, 61 | input_text_flags, 62 | ); 63 | 64 | if (imgui.buttonEx("Cancel", .{ .x = half_width, .y = 0.0 })) { 65 | popups.folder = false; 66 | } 67 | imgui.sameLine(); 68 | if (imgui.buttonEx("Ok", .{ .x = half_width, .y = 0.0 }) or enter) { 69 | const new_path = std.mem.trimRight(u8, popups.folder_path[0..], "\u{0}"); 70 | 71 | try std.fs.makeDirAbsolute(new_path); 72 | popups.folder = false; 73 | } 74 | 75 | imgui.popItemWidth(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/editor/popups/heightmap.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const pixi = @import("../../pixi.zig"); 4 | const Editor = pixi.Editor; 5 | 6 | const imgui = @import("zig-imgui"); 7 | 8 | pub fn draw(editor: *Editor) !void { 9 | if (editor.getFile(editor.open_file_index)) |file| { 10 | if (editor.popups.heightmap) { 11 | imgui.openPopup("Heightmap", imgui.PopupFlags_None); 12 | } else return; 13 | 14 | const popup_width: f32 = 350; 15 | const popup_height: f32 = 115; 16 | 17 | const window_size = pixi.app.window_size; 18 | const window_center: [2]f32 = .{ window_size[0] / 2.0, window_size[1] / 2.0 }; 19 | 20 | imgui.setNextWindowPos(.{ 21 | .x = window_center[0] - popup_width / 2.0, 22 | .y = window_center[1] - popup_height / 2.0, 23 | }, imgui.Cond_None); 24 | imgui.setNextWindowSize(.{ 25 | .x = popup_width, 26 | .y = 0.0, 27 | }, imgui.Cond_None); 28 | 29 | var modal_flags: imgui.WindowFlags = 0; 30 | modal_flags |= imgui.WindowFlags_NoResize; 31 | modal_flags |= imgui.WindowFlags_NoCollapse; 32 | 33 | if (imgui.beginPopupModal( 34 | "Heightmap", 35 | &editor.popups.heightmap, 36 | modal_flags, 37 | )) { 38 | defer imgui.endPopup(); 39 | imgui.spacing(); 40 | 41 | const style = imgui.getStyle(); 42 | const spacing = style.item_spacing.x; 43 | const half_width = (popup_width - (style.frame_padding.x * 2.0) - spacing) / 2.0; 44 | 45 | imgui.textWrapped("There currently is no heightmap layer, would you like to create a heightmap layer?"); 46 | 47 | imgui.spacing(); 48 | 49 | if (imgui.buttonEx("Cancel", .{ .x = half_width, .y = 0.0 })) { 50 | editor.popups.heightmap = false; 51 | } 52 | imgui.sameLine(); 53 | if (imgui.buttonEx("Create", .{ .x = half_width, .y = 0.0 })) { 54 | file.heightmap.layer = .{ 55 | .name = try pixi.app.allocator.dupeZ(u8, "heightmap"), 56 | .texture = try pixi.gfx.Texture.createEmpty(file.width, file.height, .{}), 57 | .id = file.newId(), 58 | }; 59 | try file.history.append(.{ .heightmap_restore_delete = .{ .action = .delete } }); 60 | editor.popups.heightmap = false; 61 | editor.tools.set(.heightmap); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/editor/popups/layer_setup.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const pixi = @import("../../pixi.zig"); 4 | const Editor = pixi.Editor; 5 | 6 | const imgui = @import("zig-imgui"); 7 | 8 | pub fn draw(editor: *Editor) !void { 9 | if (editor.getFile(editor.open_file_index)) |file| { 10 | const dialog_name = switch (editor.popups.layer_setup_state) { 11 | .none => "New Layer...", 12 | .rename => "Rename Layer...", 13 | .duplicate => "Duplicate Layer...", 14 | }; 15 | 16 | if (editor.popups.layer_setup) { 17 | imgui.openPopup(dialog_name, imgui.PopupFlags_None); 18 | } else return; 19 | 20 | const popup_width: f32 = 350; 21 | const popup_height: f32 = 115; 22 | 23 | const window_size = pixi.app.window_size; 24 | const window_center: [2]f32 = .{ window_size[0] / 2.0, window_size[1] / 2.0 }; 25 | 26 | imgui.setNextWindowPos(.{ 27 | .x = window_center[0] - popup_width / 2.0, 28 | .y = window_center[1] - popup_height / 2.0, 29 | }, imgui.Cond_None); 30 | imgui.setNextWindowSize(.{ 31 | .x = popup_width, 32 | .y = popup_height, 33 | }, imgui.Cond_None); 34 | 35 | var modal_flags: imgui.WindowFlags = 0; 36 | modal_flags |= imgui.WindowFlags_NoResize; 37 | modal_flags |= imgui.WindowFlags_NoCollapse; 38 | 39 | if (imgui.beginPopupModal( 40 | dialog_name, 41 | &editor.popups.layer_setup, 42 | modal_flags, 43 | )) { 44 | defer imgui.endPopup(); 45 | imgui.spacing(); 46 | 47 | const style = imgui.getStyle(); 48 | const spacing = style.item_spacing.x; 49 | const full_width = popup_width - (style.frame_padding.x * 2.0) - imgui.calcTextSize("Name").x; 50 | const half_width = (popup_width - (style.frame_padding.x * 2.0) - spacing) / 2.0; 51 | 52 | imgui.pushItemWidth(full_width); 53 | 54 | var input_text_flags: imgui.InputTextFlags = 0; 55 | input_text_flags |= imgui.InputTextFlags_AutoSelectAll; 56 | input_text_flags |= imgui.InputTextFlags_EnterReturnsTrue; 57 | 58 | const enter = imgui.inputText( 59 | "Name", 60 | editor.popups.layer_setup_name[0.. :0], 61 | editor.popups.layer_setup_name[0.. :0].len, 62 | input_text_flags, 63 | ); 64 | 65 | imgui.setCursorPosY(popup_height - imgui.getTextLineHeightWithSpacing() * 2.0); 66 | 67 | if (imgui.buttonEx("Cancel", .{ .x = half_width, .y = 0.0 })) { 68 | editor.popups.layer_setup = false; 69 | } 70 | imgui.sameLine(); 71 | if (imgui.buttonEx("Ok", .{ .x = half_width, .y = 0.0 }) or enter) { 72 | switch (pixi.editor.popups.layer_setup_state) { 73 | .none => { 74 | const new_name = std.mem.trimRight(u8, editor.popups.layer_setup_name[0..], "\u{0}"); 75 | if (std.mem.indexOf(u8, new_name, "\u{0}")) |index| { 76 | try file.createLayer(editor.popups.layer_setup_name[0..index :0]); 77 | } else { 78 | try file.createLayer(editor.popups.layer_setup_name[0..new_name.len :0]); 79 | } 80 | }, 81 | .rename => { 82 | const new_name = std.mem.trimRight(u8, pixi.editor.popups.layer_setup_name[0..], "\u{0}"); 83 | if (std.mem.indexOf(u8, new_name, "\u{0}")) |index| { 84 | try file.renameLayer(editor.popups.layer_setup_name[0..index :0], editor.popups.layer_setup_index); 85 | } else { 86 | try file.renameLayer(editor.popups.layer_setup_name[0..new_name.len :0], editor.popups.layer_setup_index); 87 | } 88 | }, 89 | .duplicate => { 90 | const new_name = std.mem.trimRight(u8, pixi.editor.popups.layer_setup_name[0.. :0], "\u{0}"); 91 | if (std.mem.indexOf(u8, new_name, "\u{0}")) |index| { 92 | try file.duplicateLayer(editor.popups.layer_setup_name[0..index :0], editor.popups.layer_setup_index); 93 | } else { 94 | try file.duplicateLayer(editor.popups.layer_setup_name[0..new_name.len :0], editor.popups.layer_setup_index); 95 | } 96 | }, 97 | } 98 | editor.popups.layer_setup_state = .none; 99 | editor.popups.layer_setup = false; 100 | } 101 | 102 | imgui.popItemWidth(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/editor/popups/references.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../../pixi.zig"); 3 | const Editor = pixi.Editor; 4 | 5 | const core = @import("mach").core; 6 | const imgui = @import("zig-imgui"); 7 | 8 | var open: bool = false; 9 | 10 | pub fn draw(editor: *Editor) !void { 11 | if (!editor.popups.references) return; 12 | 13 | const popup_size = 200; 14 | 15 | const window_size = pixi.app.window_size; 16 | 17 | imgui.setNextWindowPos(.{ 18 | .x = window_size[0] - popup_size - 30.0, 19 | .y = 30.0, 20 | }, imgui.Cond_Appearing); 21 | imgui.setNextWindowSize(.{ 22 | .x = popup_size, 23 | .y = popup_size, 24 | }, imgui.Cond_Appearing); 25 | 26 | var popup_flags: imgui.WindowFlags = 0; 27 | popup_flags |= imgui.WindowFlags_None; 28 | 29 | var background_color = editor.theme.foreground; 30 | background_color.value[3] = editor.settings.reference_window_opacity / 100.0; 31 | 32 | imgui.pushStyleColorImVec4(imgui.Col_WindowBg, background_color.toImguiVec4()); 33 | defer imgui.popStyleColor(); 34 | 35 | if (imgui.begin( 36 | "References", 37 | &editor.popups.references, 38 | popup_flags, 39 | )) { 40 | var ref_flags: imgui.TabBarFlags = 0; 41 | ref_flags |= imgui.TabBarFlags_Reorderable; 42 | ref_flags |= imgui.TabBarFlags_AutoSelectNewTabs; 43 | 44 | if (imgui.beginTabBar("ReferencesTabBar", ref_flags)) { 45 | defer imgui.endTabBar(); 46 | 47 | for (editor.open_references.items, 0..) |*reference, i| { 48 | var tab_open: bool = true; 49 | 50 | const file_name = std.fs.path.basename(reference.path); 51 | 52 | imgui.pushIDInt(@as(c_int, @intCast(i))); 53 | defer imgui.popID(); 54 | 55 | const label = try std.fmt.allocPrintZ(pixi.app.allocator, " {s} {s} ", .{ pixi.fa.file_image, file_name }); 56 | defer pixi.app.allocator.free(label); 57 | 58 | var file_tab_flags: imgui.TabItemFlags = 0; 59 | file_tab_flags |= imgui.TabItemFlags_None; 60 | 61 | if (imgui.beginTabItem( 62 | label, 63 | &tab_open, 64 | file_tab_flags, 65 | )) { 66 | imgui.endTabItem(); 67 | } 68 | 69 | if (!tab_open) { 70 | try editor.closeReference(i); 71 | break; 72 | } 73 | 74 | if (imgui.isItemClickedEx(imgui.MouseButton_Left)) { 75 | editor.setActiveReference(i); 76 | } 77 | 78 | if (imgui.beginPopupContextItem()) { 79 | defer imgui.endPopup(); 80 | imgui.text("Opacity"); 81 | _ = imgui.sliderFloatEx("Background", &editor.settings.reference_window_opacity, 0.0, 100.0, "%.0f", imgui.SliderFlags_AlwaysClamp); 82 | _ = imgui.sliderFloatEx("Reference", &reference.opacity, 0.0, 100.0, "%.0f", imgui.SliderFlags_AlwaysClamp); 83 | } 84 | } 85 | 86 | var canvas_flags: imgui.WindowFlags = 0; 87 | canvas_flags |= imgui.WindowFlags_ChildWindow; 88 | 89 | if (editor.getReference(editor.open_reference_index)) |reference| { 90 | if (imgui.beginChild( 91 | reference.path, 92 | .{ .x = 0.0, .y = 0.0 }, 93 | imgui.ChildFlags_None, 94 | canvas_flags, 95 | )) { 96 | const window_width = imgui.getWindowWidth(); 97 | const window_height = imgui.getWindowHeight(); 98 | 99 | const file_width: f32 = @floatFromInt(reference.texture.width); 100 | const file_height: f32 = @floatFromInt(reference.texture.height); 101 | 102 | const canvas_center_offset = reference.canvasCenterOffset(); 103 | 104 | { // Handle reference camera 105 | var camera: pixi.gfx.Camera = .{ 106 | .zoom = @min(window_width / file_width, window_height / file_height), 107 | }; 108 | camera.setNearestZoomFloor(); 109 | if (!reference.camera.zoom_initialized) { 110 | reference.camera.zoom_initialized = true; 111 | reference.camera.zoom = camera.zoom; 112 | } 113 | camera.setNearestZoomFloor(); 114 | reference.camera.min_zoom = @min(camera.zoom, pixi.editor.settings.zoom_steps[0]); 115 | 116 | reference.camera.processPanZoom(.reference); 117 | } 118 | 119 | { // Draw reference texture 120 | const color = pixi.math.Color.initFloats(1.0, 1.0, 1.0, reference.opacity / 100.0); 121 | reference.camera.drawTexture( 122 | &reference.texture, 123 | .{ canvas_center_offset[0], canvas_center_offset[1], @floatFromInt(reference.texture.width), @floatFromInt(reference.texture.height) }, 124 | color.toU32(), 125 | ); 126 | } 127 | 128 | { // Allow dropper support 129 | if (imgui.isWindowHovered(imgui.HoveredFlags_None)) { 130 | reference.processSampleTool(); 131 | } 132 | } 133 | } 134 | imgui.endChild(); 135 | } 136 | } 137 | } 138 | imgui.end(); 139 | } 140 | -------------------------------------------------------------------------------- /src/editor/popups/rename.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../../pixi.zig"); 3 | 4 | const App = pixi.App; 5 | 6 | const imgui = @import("zig-imgui"); 7 | 8 | const Popups = @import("Popups.zig"); 9 | const Editor = pixi.Editor; 10 | 11 | pub fn draw(popups: *Popups, app: *App, editor: *Editor) !void { 12 | const dialog_name = switch (popups.rename_state) { 13 | .none => "None...", 14 | .rename => "Rename...", 15 | .duplicate => "Duplicate...", 16 | }; 17 | 18 | if (popups.rename) { 19 | imgui.openPopup(dialog_name, imgui.PopupFlags_None); 20 | } else return; 21 | 22 | const popup_width: f32 = 350; 23 | const popup_height: f32 = 115; 24 | 25 | const window_size = app.window_size; 26 | const window_center: [2]f32 = .{ window_size[0] / 2.0, window_size[1] / 2.0 }; 27 | 28 | imgui.setNextWindowPos(.{ 29 | .x = window_center[0] - popup_width / 2.0, 30 | .y = window_center[1] - popup_height / 2.0, 31 | }, imgui.Cond_None); 32 | imgui.setNextWindowSize(.{ 33 | .x = popup_width, 34 | .y = 0.0, 35 | }, imgui.Cond_None); 36 | 37 | var modal_flags: imgui.WindowFlags = 0; 38 | modal_flags |= imgui.WindowFlags_NoResize; 39 | modal_flags |= imgui.WindowFlags_NoCollapse; 40 | 41 | if (imgui.beginPopupModal(dialog_name, &popups.rename, modal_flags)) { 42 | defer imgui.endPopup(); 43 | imgui.spacing(); 44 | 45 | const style = imgui.getStyle(); 46 | const spacing = style.item_spacing.x; 47 | const full_width = popup_width - (style.frame_padding.x * 2.0) - imgui.calcTextSize("Name").x; 48 | const half_width = (popup_width - (style.frame_padding.x * 2.0) - spacing) / 2.0; 49 | 50 | const base_name = std.fs.path.basename(popups.rename_path[0..]); 51 | var base_index: usize = 0; 52 | if (std.mem.indexOf(u8, popups.rename_path[0..], base_name)) |index| { 53 | base_index = index; 54 | } 55 | imgui.pushItemWidth(full_width); 56 | 57 | var input_text_flags: imgui.InputTextFlags = 0; 58 | input_text_flags |= imgui.InputTextFlags_AutoSelectAll; 59 | input_text_flags |= imgui.InputTextFlags_EnterReturnsTrue; 60 | 61 | const enter = imgui.inputText( 62 | "Name", 63 | popups.rename_path[base_index.. :0], 64 | popups.rename_path[base_index.. :0].len, 65 | input_text_flags, 66 | ); 67 | 68 | if (imgui.buttonEx("Cancel", .{ .x = half_width, .y = 0.0 })) { 69 | popups.rename = false; 70 | } 71 | imgui.sameLine(); 72 | if (imgui.buttonEx("Ok", .{ .x = half_width, .y = 0.0 }) or enter) { 73 | switch (popups.rename_state) { 74 | .rename => { 75 | const old_path = std.mem.trimRight(u8, popups.rename_old_path[0..], "\u{0}"); 76 | const new_path = std.mem.trimRight(u8, popups.rename_path[0..], "\u{0}"); 77 | 78 | try std.fs.renameAbsolute(old_path[0..], new_path[0..]); 79 | 80 | const old_path_z = popups.rename_old_path[0..old_path.len :0]; 81 | for (editor.open_files.items) |*open_file| { 82 | if (std.mem.eql(u8, open_file.path, old_path_z)) { 83 | app.allocator.free(open_file.path); 84 | open_file.path = try app.allocator.dupeZ(u8, new_path); 85 | } 86 | } 87 | }, 88 | .duplicate => { 89 | const original_path = std.mem.trimRight(u8, popups.rename_old_path[0..], "\u{0}"); 90 | const new_path = std.mem.trimRight(u8, popups.rename_path[0..], "\u{0}"); 91 | 92 | try std.fs.copyFileAbsolute(original_path, new_path, .{}); 93 | }, 94 | else => unreachable, 95 | } 96 | popups.rename_state = .none; 97 | popups.rename = false; 98 | } 99 | 100 | imgui.popItemWidth(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/generated/atlas.zig: -------------------------------------------------------------------------------- 1 | // This is a generated file, do not edit. 2 | 3 | // Sprites 4 | 5 | pub const sprites = struct { 6 | pub const pencil_default = 0; 7 | pub const eraser_default = 1; 8 | pub const bucket_default = 2; 9 | pub const dropper_default = 3; 10 | pub const selection_default = 4; 11 | pub const selection_add_default = 5; 12 | pub const selection_rem_default = 6; 13 | pub const fox_default_0 = 7; 14 | pub const fox_default_1 = 8; 15 | pub const fox_default_2 = 9; 16 | pub const fox_default_3 = 10; 17 | pub const fox_default_4 = 11; 18 | pub const fox_default_5 = 12; 19 | pub const fox_default_6 = 13; 20 | pub const fox_default_7 = 14; 21 | pub const fox_default_8 = 15; 22 | pub const fox_default_9 = 16; 23 | pub const fox_default_10 = 17; 24 | pub const fox_default_11 = 18; 25 | pub const fox_default_12 = 19; 26 | pub const fox_default_13 = 20; 27 | pub const fox_default_14 = 21; 28 | pub const fox_default_15 = 22; 29 | pub const logo_default = 23; 30 | }; 31 | 32 | // Animations 33 | 34 | pub const animations = struct { 35 | pub var pencil_default = [_]usize { 36 | sprites.pencil_default, 37 | }; 38 | pub var eraser_default = [_]usize { 39 | sprites.eraser_default, 40 | }; 41 | pub var bucket_default = [_]usize { 42 | sprites.bucket_default, 43 | }; 44 | pub var dropper_default = [_]usize { 45 | sprites.dropper_default, 46 | }; 47 | pub var selection_default = [_]usize { 48 | sprites.selection_default, 49 | }; 50 | pub var selection_add_default = [_]usize { 51 | sprites.selection_add_default, 52 | }; 53 | pub var selection_rem_default = [_]usize { 54 | sprites.selection_rem_default, 55 | }; 56 | pub var fox_default = [_]usize { 57 | sprites.fox_default_0, 58 | sprites.fox_default_1, 59 | sprites.fox_default_2, 60 | sprites.fox_default_3, 61 | sprites.fox_default_4, 62 | sprites.fox_default_5, 63 | sprites.fox_default_6, 64 | sprites.fox_default_7, 65 | sprites.fox_default_8, 66 | sprites.fox_default_9, 67 | sprites.fox_default_10, 68 | sprites.fox_default_11, 69 | sprites.fox_default_12, 70 | sprites.fox_default_13, 71 | sprites.fox_default_14, 72 | sprites.fox_default_15, 73 | }; 74 | pub var logo_default = [_]usize { 75 | sprites.logo_default, 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /src/generated/paths.zig: -------------------------------------------------------------------------------- 1 | // This is a generated file, do not edit. 2 | // Paths 3 | 4 | pub const root = "assets/"; 5 | 6 | pub const palettes = "assets/palettes/"; 7 | 8 | pub const themes = "assets/themes/"; 9 | 10 | pub const @"CozetteVector.ttf" = "assets/fonts/CozetteVector.ttf"; 11 | pub const @"apollo.hex" = "assets/palettes/apollo.hex"; 12 | pub const @"cursors.pixi" = "assets/src/cursors.pixi"; 13 | pub const @"endesga-16.hex" = "assets/palettes/endesga-16.hex"; 14 | pub const @"endesga-32.hex" = "assets/palettes/endesga-32.hex"; 15 | pub const @"fa-regular-400.ttf" = "assets/fonts/fa-regular-400.ttf"; 16 | pub const @"fa-solid-900.ttf" = "assets/fonts/fa-solid-900.ttf"; 17 | pub const @"fox.png" = "assets/fox.png"; 18 | pub const @"fox_bg.png" = "assets/fox_bg.png"; 19 | pub const @"journey.hex" = "assets/palettes/journey.hex"; 20 | pub const @"lospec500.hex" = "assets/palettes/lospec500.hex"; 21 | pub const @"misc.pixi" = "assets/src/misc.pixi"; 22 | pub const @"pear36.hex" = "assets/palettes/pear36.hex"; 23 | pub const @"pico-8.hex" = "assets/palettes/pico-8.hex"; 24 | pub const @"pixi.atlas" = "assets/pixi.atlas"; 25 | pub const @"pixi.png" = "assets/pixi.png"; 26 | pub const @"pixi_dark.json" = "assets/themes/pixi_dark.json"; 27 | pub const @"pixi_light.json" = "assets/themes/pixi_light.json"; 28 | pub const @"resurrect-64.hex" = "assets/palettes/resurrect-64.hex"; 29 | -------------------------------------------------------------------------------- /src/gfx/Quad.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zm = @import("zmath"); 3 | const pixi = @import("../pixi.zig"); 4 | const gfx = pixi.gfx; 5 | 6 | pub const Quad = @This(); 7 | vertices: [5]gfx.Vertex, 8 | 9 | pub fn setHeight(self: *Quad, height: f32) void { 10 | for (self.vertices, 0..) |_, i| { 11 | self.vertices[i].position[2] = height; 12 | } 13 | } 14 | 15 | pub fn setColor(self: *Quad, color: [4]f32) void { 16 | for (self.vertices, 0..) |_, i| { 17 | self.vertices[i].color = color; 18 | } 19 | } 20 | 21 | pub fn setViewport(self: *Quad, x: f32, y: f32, width: f32, height: f32, tex_width: f32, tex_height: f32) void { 22 | // squeeze texcoords in by 128th of a pixel to avoid bleed 23 | const w_tol = (1.0 / tex_width) / 128.0; 24 | const h_tol = (1.0 / tex_height) / 128.0; 25 | 26 | const inv_w = 1.0 / tex_width; 27 | const inv_h = 1.0 / tex_height; 28 | 29 | self.vertices[0].uv = [_]f32{ x * inv_w + w_tol, y * inv_h + h_tol }; 30 | self.vertices[1].uv = [_]f32{ (x + width) * inv_w - w_tol, y * inv_h + h_tol }; 31 | self.vertices[2].uv = [_]f32{ (x + width) * inv_w - w_tol, (y + height) * inv_h - h_tol }; 32 | self.vertices[3].uv = [_]f32{ x * inv_w + w_tol, (y + height) * inv_h - h_tol }; 33 | self.vertices[4].uv = [_]f32{ (self.vertices[0].uv[0] + self.vertices[1].uv[0]) / 2.0, (self.vertices[0].uv[1] + self.vertices[2].uv[1]) / 2.0 }; 34 | } 35 | 36 | pub fn flipHorizontally(self: *Quad) void { 37 | const bl_uv = self.vertices[0].uv; 38 | self.vertices[0].uv = self.vertices[1].uv; 39 | self.vertices[1].uv = bl_uv; 40 | 41 | const tr_uv = self.vertices[2].uv; 42 | self.vertices[2].uv = self.vertices[3].uv; 43 | self.vertices[3].uv = tr_uv; 44 | } 45 | 46 | pub fn flipVertically(self: *Quad) void { 47 | const bl_uv = self.vertices[0].uv; 48 | self.vertices[0].uv = self.vertices[1].uv; 49 | self.vertices[1].uv = bl_uv; 50 | 51 | const tr_uv = self.vertices[2].uv; 52 | self.vertices[2].uv = self.vertices[3].uv; 53 | self.vertices[3].uv = tr_uv; 54 | } 55 | 56 | pub fn scale(self: *Quad, s: [2]f32, pos_x: f32, pos_y: f32, origin_x: f32, origin_y: f32) void { 57 | for (self.vertices, 0..) |vert, i| { 58 | var position = zm.loadArr3(vert.position); 59 | const offset = zm.f32x4(pos_x, pos_y, 0, 0); 60 | 61 | const translation_matrix = zm.translation(origin_x, origin_y, 0); 62 | const scale_matrix = zm.scaling(s[0], s[1], 0); 63 | 64 | position -= offset; 65 | position = zm.mul(position, zm.mul(translation_matrix, scale_matrix)); 66 | position += offset; 67 | 68 | zm.storeArr3(&self.vertices[i].position, position); 69 | } 70 | } 71 | 72 | pub fn rotate(self: *Quad, rotation: f32, pivot: zm.F32x4) void { 73 | for (0..5) |i| { 74 | const vert = self.vertices[i]; 75 | var position = zm.loadArr3(vert.position); 76 | const radians = std.math.degreesToRadians(rotation); 77 | 78 | const rotation_matrix = zm.rotationZ(radians); 79 | 80 | position -= pivot; 81 | position = zm.mul(position, rotation_matrix); 82 | position += pivot; 83 | 84 | zm.storeArr3(&self.vertices[i].position, position); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/gfx/gfx.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zm = @import("zmath"); 3 | const pixi = @import("../pixi.zig"); 4 | const App = @import("../App.zig"); 5 | const zstbi = @import("zstbi"); 6 | 7 | const build_options = @import("build-options"); 8 | 9 | const mach = @import("mach"); 10 | const Core = mach.Core; 11 | const gpu = mach.gpu; 12 | 13 | pub const Quad = @import("Quad.zig"); 14 | pub const Batcher = @import("Batcher.zig"); 15 | pub const Texture = @import("Texture.zig"); 16 | pub const Camera = @import("Camera.zig"); 17 | 18 | pub const Vertex = struct { 19 | position: [3]f32 = [_]f32{ 0.0, 0.0, 0.0 }, 20 | uv: [2]f32 = [_]f32{ 0.0, 0.0 }, 21 | color: [4]f32 = [_]f32{ 1.0, 1.0, 1.0, 1.0 }, 22 | data: [3]f32 = [_]f32{ 0.0, 0.0, 0.0 }, 23 | }; 24 | 25 | pub const UniformBufferObject = struct { 26 | mvp: zm.Mat, 27 | }; 28 | 29 | pub fn init(app: *App) !void { 30 | const device: *gpu.Device = pixi.core.windows.get(app.window, .device); 31 | 32 | const default_shader = @embedFile("../shaders/default.wgsl"); 33 | const default_shader_module = device.createShaderModuleWGSL("default.wgsl", default_shader); 34 | 35 | defer default_shader_module.release(); 36 | 37 | const compute_shader = @embedFile("../shaders/compute.wgsl"); 38 | const compute_shader_module = device.createShaderModuleWGSL("compute.wgsl", compute_shader); 39 | 40 | defer compute_shader_module.release(); 41 | 42 | const vertex_attributes = [_]gpu.VertexAttribute{ 43 | .{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 }, 44 | .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, 45 | .{ .format = .float32x4, .offset = @offsetOf(Vertex, "color"), .shader_location = 2 }, 46 | .{ .format = .float32x3, .offset = @offsetOf(Vertex, "data"), .shader_location = 3 }, 47 | }; 48 | const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ 49 | .array_stride = @sizeOf(Vertex), 50 | .step_mode = .vertex, 51 | .attributes = &vertex_attributes, 52 | }); 53 | 54 | const blend = gpu.BlendState{ 55 | .color = .{ 56 | .operation = .add, 57 | .src_factor = .src_alpha, 58 | .dst_factor = .one_minus_src_alpha, 59 | }, 60 | .alpha = .{ 61 | .operation = .add, 62 | .src_factor = .src_alpha, 63 | .dst_factor = .one_minus_src_alpha, 64 | }, 65 | }; 66 | 67 | const color_target = gpu.ColorTargetState{ 68 | .format = .rgba8_unorm, 69 | .blend = &blend, 70 | .write_mask = gpu.ColorWriteMaskFlags.all, 71 | }; 72 | 73 | const default_fragment = gpu.FragmentState.init(.{ 74 | .module = default_shader_module, 75 | .entry_point = "frag_main", 76 | .targets = &.{color_target}, 77 | }); 78 | 79 | const default_vertex = gpu.VertexState.init(.{ 80 | .module = default_shader_module, 81 | .entry_point = "vert_main", 82 | .buffers = &.{vertex_buffer_layout}, 83 | }); 84 | 85 | const default_pipeline_descriptor = gpu.RenderPipeline.Descriptor{ 86 | .fragment = &default_fragment, 87 | .vertex = default_vertex, 88 | }; 89 | 90 | app.pipeline_default = device.createRenderPipeline(&default_pipeline_descriptor); 91 | 92 | app.uniform_buffer_default = device.createBuffer(&.{ 93 | .usage = .{ .copy_dst = true, .uniform = true }, 94 | .size = @sizeOf(UniformBufferObject), 95 | .mapped_at_creation = .false, 96 | }); 97 | 98 | const compute_pipeline_descriptor = gpu.ComputePipeline.Descriptor{ 99 | .compute = gpu.ProgrammableStageDescriptor{ 100 | .module = compute_shader_module, 101 | .entry_point = "copyTextureToBuffer", 102 | }, 103 | }; 104 | 105 | app.pipeline_compute = device.createComputePipeline(&compute_pipeline_descriptor); 106 | } 107 | -------------------------------------------------------------------------------- /src/input/Mouse.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zm = @import("zmath"); 3 | const math = @import("../math/math.zig"); 4 | const pixi = @import("../pixi.zig"); 5 | const Core = @import("mach").Core; 6 | 7 | const builtin = @import("builtin"); 8 | 9 | const Mods = Core.KeyMods; 10 | const MouseButton = Core.MouseButton; 11 | 12 | const Self = @This(); 13 | 14 | pub const ButtonState = enum { 15 | press, 16 | release, 17 | }; 18 | 19 | pub const Button = struct { 20 | button: MouseButton, 21 | mods: ?Mods = null, 22 | action: Action, 23 | state: bool = false, 24 | previous_state: bool = false, 25 | 26 | /// Returns true the frame the key was pressed. 27 | pub fn pressed(self: Button) bool { 28 | return (self.state == true and self.state != self.previous_state); 29 | } 30 | 31 | /// Returns true while the key is pressed down. 32 | pub fn down(self: Button) bool { 33 | return self.state == true; 34 | } 35 | 36 | /// Returns true the frame the key was released. 37 | pub fn released(self: Button) bool { 38 | return (self.state == false and self.state != self.previous_state); 39 | } 40 | 41 | /// Returns true while the key is released. 42 | pub fn up(self: Button) bool { 43 | return self.state == false; 44 | } 45 | }; 46 | 47 | pub const Action = enum { 48 | primary, 49 | secondary, 50 | sample, 51 | }; 52 | 53 | buttons: []Button, 54 | position: [2]f32 = .{ 0.0, 0.0 }, 55 | previous_position: [2]f32 = .{ 0.0, 0.0 }, 56 | magnify: ?f32 = null, 57 | scroll_x: ?f32 = null, 58 | scroll_y: ?f32 = null, 59 | 60 | pub fn button(self: *Self, action: Action) ?*Button { 61 | for (self.buttons) |*bt| { 62 | if (bt.action == action) 63 | return bt; 64 | } 65 | return null; 66 | } 67 | 68 | pub fn anyButtonDown(self: *Self) bool { 69 | for (self.buttons) |bt| { 70 | if (bt.down()) { 71 | return true; 72 | } 73 | } 74 | 75 | return false; 76 | } 77 | 78 | pub fn setButtonState(self: *Self, b: MouseButton, mods: Mods, state: ButtonState) void { 79 | for (self.buttons) |*bt| { 80 | if (bt.button == b) { 81 | if (state == .release or bt.mods == null) { 82 | bt.previous_state = bt.state; 83 | bt.state = switch (state) { 84 | .press => true, 85 | else => false, 86 | }; 87 | } else if (bt.mods) |md| { 88 | if (@as(u8, @bitCast(md)) == @as(u8, @bitCast(mods))) { 89 | bt.previous_state = bt.state; 90 | bt.state = switch (state) { 91 | .press => true, 92 | else => false, 93 | }; 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | pub fn initDefault(allocator: std.mem.Allocator) !Self { 101 | var buttons = std.ArrayList(Button).init(allocator); 102 | 103 | const os = builtin.target.os.tag; 104 | const windows = os == .windows; 105 | _ = windows; 106 | const macos = os == .macos; 107 | _ = macos; 108 | 109 | { 110 | try buttons.append(.{ 111 | .button = MouseButton.left, 112 | .action = Action.primary, 113 | }); 114 | 115 | try buttons.append(.{ 116 | .button = MouseButton.right, 117 | .action = Action.secondary, 118 | }); 119 | 120 | try buttons.append(.{ 121 | .button = MouseButton.right, 122 | .action = Action.sample, 123 | }); 124 | } 125 | 126 | return .{ .buttons = try buttons.toOwnedSlice() }; 127 | } 128 | -------------------------------------------------------------------------------- /src/input/input.zig: -------------------------------------------------------------------------------- 1 | const pixi = @import("../pixi.zig"); 2 | 3 | pub const Mouse = @import("Mouse.zig"); 4 | pub const Hotkeys = @import("Hotkeys.zig"); 5 | 6 | pub fn process() !void { 7 | if (!pixi.editor.popups.anyPopupOpen()) { 8 | try pixi.editor.hotkeys.process(pixi.editor); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/internal/Animation.zig: -------------------------------------------------------------------------------- 1 | name: [:0]const u8, 2 | start: usize, 3 | length: usize, 4 | fps: usize, 5 | -------------------------------------------------------------------------------- /src/internal/Atlas.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | const Atlas = @This(); 5 | const ExternalAtlas = @import("../Atlas.zig"); 6 | 7 | /// The packed atlas texture 8 | texture: ?pixi.gfx.Texture = null, 9 | 10 | /// The packed atlas heightmap 11 | heightmap: ?pixi.gfx.Texture = null, 12 | 13 | /// The actual atlas, which contains the sprites and animations data 14 | data: ?ExternalAtlas = undefined, 15 | 16 | pub const Selector = enum { 17 | texture, 18 | heightmap, 19 | data, 20 | }; 21 | 22 | pub fn save(atlas: Atlas, path: []const u8, selector: Selector) !void { 23 | switch (selector) { 24 | .texture, .heightmap => { 25 | if (!std.mem.eql(u8, ".png", std.fs.path.extension(path))) { 26 | std.log.debug("File name must end with .png extension!", .{}); 27 | return; 28 | } 29 | const write_path = std.fmt.allocPrintZ(pixi.editor.arena.allocator(), "{s}", .{path}) catch unreachable; 30 | 31 | switch (selector) { 32 | .texture => if (atlas.texture) |*texture| try texture.stbi_image().writeToFile(write_path, .png), 33 | .heightmap => if (atlas.heightmap) |*heightmap| try heightmap.stbi_image().writeToFile(write_path, .png), 34 | else => unreachable, 35 | } 36 | }, 37 | .data => { 38 | if (!std.mem.eql(u8, ".atlas", std.fs.path.extension(path))) { 39 | std.log.debug("File name must end with .atlas extension!", .{}); 40 | return; 41 | } 42 | if (atlas.data) |data| { 43 | var handle = try std.fs.cwd().createFile(path, .{}); 44 | defer handle.close(); 45 | 46 | const out_stream = handle.writer(); 47 | const options: std.json.StringifyOptions = .{}; 48 | 49 | try std.json.stringify(data, options, out_stream); 50 | } 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/internal/Buffers.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | const History = @import("History.zig"); 5 | const Buffers = @This(); 6 | 7 | stroke: Stroke, 8 | temporary_stroke: Stroke, 9 | 10 | pub const Stroke = struct { 11 | indices: std.ArrayList(usize), 12 | values: std.ArrayList([4]u8), 13 | canvas: pixi.Internal.File.Canvas = .primary, 14 | 15 | pub fn init(allocator: std.mem.Allocator) Stroke { 16 | return .{ 17 | .indices = std.ArrayList(usize).init(allocator), 18 | .values = std.ArrayList([4]u8).init(allocator), 19 | }; 20 | } 21 | 22 | pub fn append(stroke: *Stroke, index: usize, value: [4]u8, canvas: pixi.Internal.File.Canvas) !void { 23 | try stroke.indices.append(index); 24 | try stroke.values.append(value); 25 | stroke.canvas = canvas; 26 | } 27 | 28 | pub fn appendSlice(stroke: *Stroke, indices: []usize, values: [][4]u8, canvas: pixi.Internal.File.Canvas) !void { 29 | try stroke.indices.appendSlice(indices); 30 | try stroke.values.appendSlice(values); 31 | stroke.canvas = canvas; 32 | } 33 | 34 | pub fn toChange(stroke: *Stroke, layer: i32) !History.Change { 35 | return .{ .pixels = .{ 36 | .layer = layer, 37 | .indices = try stroke.indices.toOwnedSlice(), 38 | .values = try stroke.values.toOwnedSlice(), 39 | } }; 40 | } 41 | 42 | pub fn clearAndFree(stroke: *Stroke) void { 43 | stroke.indices.clearAndFree(); 44 | stroke.values.clearAndFree(); 45 | } 46 | 47 | pub fn deinit(stroke: *Stroke) void { 48 | stroke.clearAndFree(); 49 | stroke.indices.deinit(); 50 | stroke.values.deinit(); 51 | } 52 | }; 53 | 54 | pub fn init(allocator: std.mem.Allocator) Buffers { 55 | return .{ 56 | .stroke = Stroke.init(allocator), 57 | .temporary_stroke = Stroke.init(allocator), 58 | }; 59 | } 60 | 61 | pub fn clearAndFree(buffers: *Buffers) void { 62 | buffers.stroke.clearAndFree(); 63 | buffers.temporary_stroke.clearAndFree(); 64 | } 65 | 66 | pub fn deinit(buffers: *Buffers) void { 67 | buffers.clearAndFree(); 68 | buffers.stroke.deinit(); 69 | buffers.temporary_stroke.deinit(); 70 | } 71 | -------------------------------------------------------------------------------- /src/internal/Frame.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | /// A frame is the necessary data to create a transformation frame control around a sprite 5 | /// and move/scale/rotate it within the editor. 6 | /// 7 | /// The vertices correspond to each corner where a stretch control exists, 8 | /// and a pivot, which is moveable and changes the rotation control pivot. 9 | const Frame = @This(); 10 | 11 | const File = @import("File.zig"); 12 | 13 | vertices: [4]File.TransformVertex, 14 | pivot: File.TransformVertex, 15 | rotation: f32 = 0.0, 16 | id: u32, 17 | sprite_index: usize, 18 | layer_id: u32, 19 | parent_id: ?u32 = null, 20 | visible: bool = true, 21 | tween_id: ?u32 = null, 22 | tween: pixi.math.Tween = .none, 23 | -------------------------------------------------------------------------------- /src/internal/Keyframe.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const pixi = @import("../pixi.zig"); 4 | 5 | const Frame = @import("Frame.zig"); 6 | 7 | const Keyframe = @This(); 8 | 9 | frames: std.ArrayList(Frame), 10 | time: f32 = 0.0, 11 | id: u32, 12 | active_frame_id: u32, 13 | 14 | pub fn frame(self: Keyframe, id: u32) ?*Frame { 15 | for (self.frames.items) |*fr| { 16 | if (fr.id == id) 17 | return fr; 18 | } 19 | return null; 20 | } 21 | 22 | pub fn frameIndex(self: Keyframe, id: u32) ?usize { 23 | for (self.frames.items, 0..) |*fr, i| { 24 | if (fr.id == id) 25 | return i; 26 | } 27 | return null; 28 | } 29 | -------------------------------------------------------------------------------- /src/internal/KeyframeAnimation.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | const Keyframe = @import("Keyframe.zig"); 5 | const Frame = @import("Frame.zig"); 6 | 7 | const KeyframeAnimation = @This(); 8 | 9 | name: [:0]const u8, 10 | id: u32, 11 | keyframes: std.ArrayList(Keyframe), 12 | elapsed_time: f32 = 0.0, 13 | active_keyframe_id: u32, 14 | 15 | pub fn keyframe(self: KeyframeAnimation, id: u32) ?*Keyframe { 16 | for (self.keyframes.items) |*fr| { 17 | if (fr.id == id) 18 | return fr; 19 | } 20 | return null; 21 | } 22 | 23 | pub fn keyframeIndex(self: KeyframeAnimation, id: u32) ?usize { 24 | for (self.keyframes.items, 0..) |fr, i| { 25 | if (fr.id == id) 26 | return i; 27 | } 28 | return null; 29 | } 30 | 31 | pub fn getKeyframeMilliseconds(self: KeyframeAnimation, ms: usize) ?*Keyframe { 32 | for (self.keyframes.items) |*kf| { 33 | const kf_ms: usize = @intFromFloat(kf.time * 1000.0); 34 | if (ms == kf_ms) 35 | return kf; 36 | } 37 | 38 | return null; 39 | } 40 | 41 | pub fn getKeyframeFromFrame(self: KeyframeAnimation, frame_id: u32) ?*Keyframe { 42 | for (self.keyframes.items) |*kf| { 43 | if (kf.frame(frame_id) != null) { 44 | return kf; 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | 51 | pub fn getFrameNodeColor(self: KeyframeAnimation, frame_id: u32) u32 { 52 | var color_index: usize = @mod(frame_id * 2, 35); 53 | 54 | if (self.getTweenStartFrame(frame_id)) |tween_start_frame| { 55 | var last_frame = tween_start_frame; 56 | while (true) { 57 | if (self.getTweenStartFrame(last_frame.id)) |fr| { 58 | last_frame = fr; 59 | } else { 60 | break; 61 | } 62 | } 63 | 64 | color_index = @mod(last_frame.id * 2, 35); 65 | } 66 | 67 | return if (pixi.editor.colors.keyframe_palette) |palette| pixi.math.Color.initBytes( 68 | palette.colors[color_index][0], 69 | palette.colors[color_index][1], 70 | palette.colors[color_index][2], 71 | palette.colors[color_index][3], 72 | ).toU32() else pixi.editor.theme.text.toU32(); 73 | } 74 | 75 | pub fn getTweenStartFrame(self: KeyframeAnimation, frame_id: u32) ?*Frame { 76 | for (self.keyframes.items) |kf| { 77 | for (kf.frames.items) |*fr| { 78 | if (fr.tween_id) |tween_id| { 79 | if (tween_id == frame_id) { 80 | return fr; 81 | } 82 | } 83 | } 84 | } 85 | return null; 86 | } 87 | 88 | /// Returns the length of the animation in seconds 89 | pub fn length(self: KeyframeAnimation) f32 { 90 | var len: f32 = 0.0; 91 | for (self.keyframes.items) |kf| { 92 | if (kf.time > len) 93 | len = kf.time; 94 | } 95 | return len; 96 | } 97 | 98 | /// Returns the number of frames in the largest keyframe 99 | pub fn maxNodes(self: KeyframeAnimation) usize { 100 | var nodes: usize = 0; 101 | for (self.keyframes.items) |kf| { 102 | if (kf.frames.items.len > nodes) 103 | nodes = kf.frames.items.len; 104 | } 105 | return nodes; 106 | } 107 | -------------------------------------------------------------------------------- /src/internal/Layer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gpu = @import("mach").gpu; 3 | const pixi = @import("../pixi.zig"); 4 | 5 | const Layer = @This(); 6 | 7 | name: [:0]const u8, 8 | texture: pixi.gfx.Texture, 9 | visible: bool = true, 10 | collapse: bool = false, 11 | id: u32 = 0, 12 | transform_bindgroup: ?*gpu.BindGroup = null, 13 | 14 | pub fn pixels(self: *const Layer) [][4]u8 { 15 | return @as([*][4]u8, @ptrCast(self.texture.pixels.ptr))[0 .. self.texture.pixels.len / 4]; 16 | } 17 | 18 | pub fn getPixelIndex(self: Layer, pixel: [2]usize) usize { 19 | return pixel[0] + pixel[1] * @as(usize, @intCast(self.texture.width)); 20 | } 21 | 22 | pub fn getPixel(self: Layer, pixel: [2]usize) [4]u8 { 23 | const index = self.getPixelIndex(pixel); 24 | const p = @as([*][4]u8, @ptrCast(self.texture.pixels.ptr))[0 .. self.texture.pixels.len / 4]; 25 | return p[index]; 26 | } 27 | 28 | pub fn setPixel(self: *Layer, pixel: [2]usize, color: [4]u8, update: bool) void { 29 | const index = self.getPixelIndex(pixel); 30 | var p = self.pixels(); 31 | p[index] = color; 32 | if (update) 33 | self.texture.update(pixi.core.windows.get(pixi.app.window, .device)); 34 | } 35 | 36 | pub fn setPixelIndex(self: *Layer, index: usize, color: [4]u8, update: bool) void { 37 | var p = self.pixels(); 38 | p[index] = color; 39 | if (update) 40 | self.texture.update(pixi.core.windows.get(pixi.app.window, .device)); 41 | } 42 | 43 | pub const ShapeOffsetResult = struct { 44 | index: usize, 45 | color: [4]u8, 46 | }; 47 | 48 | /// Only used for handling getting the pixels surrounding the origin 49 | /// for stroke sizes larger than 1 50 | pub fn getIndexShapeOffset(self: Layer, origin: [2]usize, current_index: usize) ?ShapeOffsetResult { 51 | const shape = pixi.editor.tools.stroke_shape; 52 | const size: i32 = @intCast(pixi.editor.tools.stroke_size); 53 | 54 | if (size == 1) { 55 | if (current_index != 0) 56 | return null; 57 | 58 | return .{ 59 | .index = self.getPixelIndex(origin), 60 | .color = self.getPixel(origin), 61 | }; 62 | } 63 | 64 | const size_center_offset: i32 = -@divFloor(@as(i32, @intCast(size)), 2); 65 | const index_i32: i32 = @as(i32, @intCast(current_index)); 66 | const pixel_offset: [2]i32 = .{ @mod(index_i32, size) + size_center_offset, @divFloor(index_i32, size) + size_center_offset }; 67 | 68 | if (shape == .circle) { 69 | const extra_pixel_offset_circle: [2]i32 = if (@mod(size, 2) == 0) .{ 1, 1 } else .{ 0, 0 }; 70 | const pixel_offset_circle: [2]i32 = .{ pixel_offset[0] * 2 + extra_pixel_offset_circle[0], pixel_offset[1] * 2 + extra_pixel_offset_circle[1] }; 71 | const sqr_magnitude = pixel_offset_circle[0] * pixel_offset_circle[0] + pixel_offset_circle[1] * pixel_offset_circle[1]; 72 | 73 | // adjust radius check for nicer looking circles 74 | const radius_check_mult: f32 = (if (size == 3 or size > 10) 0.7 else 0.8); 75 | 76 | if (@as(f32, @floatFromInt(sqr_magnitude)) > @as(f32, @floatFromInt(size * size)) * radius_check_mult) { 77 | return null; 78 | } 79 | } 80 | 81 | const pixel_i32: [2]i32 = .{ @as(i32, @intCast(origin[0])) + pixel_offset[0], @as(i32, @intCast(origin[1])) + pixel_offset[1] }; 82 | 83 | if (pixel_i32[0] < 0 or pixel_i32[1] < 0 or pixel_i32[0] >= self.texture.width or pixel_i32[1] >= self.texture.height) { 84 | return null; 85 | } 86 | 87 | const pixel: [2]usize = .{ @intCast(pixel_i32[0]), @intCast(pixel_i32[1]) }; 88 | 89 | return .{ 90 | .index = getPixelIndex(self, pixel), 91 | .color = getPixel(self, pixel), 92 | }; 93 | } 94 | 95 | pub fn clear(self: *Layer, update: bool) void { 96 | const p = self.pixels(); 97 | @memset(p, .{ 0, 0, 0, 0 }); 98 | 99 | if (update) 100 | self.texture.update(pixi.core.windows.get(pixi.app.window, .device)); 101 | } 102 | -------------------------------------------------------------------------------- /src/internal/Palette.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | 4 | const PackedColor = packed struct(u32) { r: u8, g: u8, b: u8, a: u8 }; 5 | 6 | pub const Palette = @This(); 7 | 8 | name: [:0]const u8, 9 | colors: [][4]u8, 10 | 11 | pub fn loadFromFile(file: [:0]const u8) !Palette { 12 | var colors = std.ArrayList([4]u8).init(pixi.app.allocator); 13 | const base_name = std.fs.path.basename(file); 14 | const ext = std.fs.path.extension(file); 15 | if (std.mem.eql(u8, ext, ".hex")) { 16 | var contents = try std.fs.cwd().openFile(file, .{}); 17 | defer contents.close(); 18 | 19 | while (try contents.reader().readUntilDelimiterOrEofAlloc(pixi.app.allocator, '\n', 200000)) |line| { 20 | const color_u32 = try std.fmt.parseInt(u32, line[0 .. line.len - 1], 16); 21 | const color_packed: PackedColor = @as(PackedColor, @bitCast(color_u32)); 22 | try colors.append(.{ color_packed.b, color_packed.g, color_packed.r, 255 }); 23 | pixi.app.allocator.free(line); 24 | } 25 | } else { 26 | return error.WrongFileType; 27 | } 28 | 29 | return .{ 30 | .name = try pixi.app.allocator.dupeZ(u8, base_name), 31 | .colors = try colors.toOwnedSlice(), 32 | }; 33 | } 34 | 35 | pub fn deinit(self: *Palette) void { 36 | pixi.app.allocator.free(self.name); 37 | pixi.app.allocator.free(self.colors); 38 | } 39 | -------------------------------------------------------------------------------- /src/internal/Reference.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | const imgui = @import("zig-imgui"); 4 | 5 | const File = @import("File.zig"); 6 | 7 | const Reference = @This(); 8 | 9 | path: [:0]const u8, 10 | texture: pixi.gfx.Texture, 11 | camera: pixi.gfx.Camera = .{}, 12 | opacity: f32 = 100.0, 13 | 14 | pub fn deinit(reference: *Reference) void { 15 | reference.texture.deinit(); 16 | pixi.app.allocator.free(reference.path); 17 | } 18 | 19 | pub fn canvasCenterOffset(reference: *Reference) [2]f32 { 20 | const width: f32 = @floatFromInt(reference.texture.width); 21 | const height: f32 = @floatFromInt(reference.texture.height); 22 | 23 | return .{ -width / 2.0, -height / 2.0 }; 24 | } 25 | 26 | pub fn getPixelIndex(reference: Reference, pixel: [2]usize) usize { 27 | return pixel[0] + pixel[1] * @as(usize, @intCast(reference.texture.width)); 28 | } 29 | 30 | pub fn getPixel(self: Reference, pixel: [2]usize) [4]u8 { 31 | const index = self.getPixelIndex(pixel); 32 | const pixels = @as([*][4]u8, @ptrCast(self.texture.pixels.ptr))[0 .. self.texture.pixels.len / 4]; 33 | return pixels[index]; 34 | } 35 | 36 | pub fn processSampleTool(reference: *Reference) void { 37 | const sample_key = if (pixi.editor.hotkeys.hotkey(.{ .procedure = .sample })) |hotkey| hotkey.down() else false; 38 | const sample_button = if (pixi.editor.mouse.button(.sample)) |sample| sample.down() else false; 39 | 40 | if (!sample_key and !sample_button) return; 41 | 42 | imgui.setMouseCursor(imgui.MouseCursor_None); 43 | reference.camera.drawCursor(pixi.atlas.sprites.dropper_default, 0xFFFFFFFF); 44 | 45 | const mouse_position = pixi.editor.mouse.position; 46 | var camera = reference.camera; 47 | 48 | const pixel_coord_opt = camera.pixelCoordinates(.{ 49 | .texture_position = canvasCenterOffset(reference), 50 | .position = mouse_position, 51 | .width = reference.texture.width, 52 | .height = reference.texture.height, 53 | }); 54 | 55 | if (pixel_coord_opt) |pixel_coord| { 56 | const pixel = .{ @as(usize, @intFromFloat(pixel_coord[0])), @as(usize, @intFromFloat(pixel_coord[1])) }; 57 | 58 | const color = reference.getPixel(pixel); 59 | 60 | try camera.drawColorTooltip(color); 61 | 62 | if (color[3] != 0) 63 | pixi.editor.colors.primary = color; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/internal/Sprite.zig: -------------------------------------------------------------------------------- 1 | origin: [2]f32 = .{ 0.0, 0.0 }, 2 | -------------------------------------------------------------------------------- /src/math/color.zig: -------------------------------------------------------------------------------- 1 | const zm = @import("zmath"); 2 | const imgui = @import("zig-imgui"); 3 | 4 | pub const Color = struct { 5 | value: zm.F32x4, 6 | 7 | pub fn initFloats(r: f32, g: f32, b: f32, a: f32) Color { 8 | return .{ 9 | .value = zm.f32x4(r, g, b, a), 10 | }; 11 | } 12 | 13 | pub fn initBytes(r: u8, g: u8, b: u8, a: u8) Color { 14 | return .{ 15 | .value = zm.f32x4(@as(f32, @floatFromInt(r)) / 255, @as(f32, @floatFromInt(g)) / 255, @as(f32, @floatFromInt(b)) / 255, @as(f32, @floatFromInt(a)) / 255), 16 | }; 17 | } 18 | 19 | pub fn bytes(self: Color) [4]u8 { 20 | return .{ 21 | @as(u8, @intFromFloat(self.value[0] * 255.0)), 22 | @as(u8, @intFromFloat(self.value[1] * 255.0)), 23 | @as(u8, @intFromFloat(self.value[2] * 255.0)), 24 | @as(u8, @intFromFloat(self.value[3] * 255.0)), 25 | }; 26 | } 27 | 28 | pub fn lerp(self: Color, other: Color, t: f32) Color { 29 | return .{ .value = zm.lerp(self.value, other.value, t) }; 30 | } 31 | 32 | pub fn toSlice(self: Color) [4]f32 { 33 | var slice: [4]f32 = undefined; 34 | zm.storeArr4(&slice, self.value); 35 | return slice; 36 | } 37 | 38 | pub fn toImguiVec4(self: Color) imgui.Vec4 { 39 | return .{ 40 | .x = self.value[0], 41 | .y = self.value[1], 42 | .z = self.value[2], 43 | .w = self.value[3], 44 | }; 45 | } 46 | 47 | pub fn toU32(self: Color) u32 { 48 | const Packed = packed struct(u32) { 49 | r: u8, 50 | g: u8, 51 | b: u8, 52 | a: u8, 53 | }; 54 | 55 | const p = Packed{ 56 | .r = @as(u8, @intFromFloat(self.value[0] * 255.0)), 57 | .g = @as(u8, @intFromFloat(self.value[1] * 255.0)), 58 | .b = @as(u8, @intFromFloat(self.value[2] * 255.0)), 59 | .a = @as(u8, @intFromFloat(self.value[3] * 255.0)), 60 | }; 61 | 62 | return @as(u32, @bitCast(p)); 63 | } 64 | }; 65 | 66 | pub const Colors = struct { 67 | pub const white = Color.initFloats(1, 1, 1, 1); 68 | pub const black = Color.initFloats(0, 0, 0, 1); 69 | pub const red = Color.initFloats(1, 0, 0, 1); 70 | pub const green = Color.initFloats(0, 1, 0, 1); 71 | pub const blue = Color.initFloats(0, 0, 1, 1); 72 | pub const grass = Color.initBytes(110, 138, 92, 255); 73 | pub const background = Color.initBytes(42, 44, 53, 255); 74 | pub const background_dark = Color.initBytes(30, 31, 38, 255); 75 | pub const text = Color.initBytes(222, 177, 142, 255); 76 | }; 77 | -------------------------------------------------------------------------------- /src/math/direction.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zm = @import("zmath"); 3 | 4 | const sqrt = 0.70710678118654752440084436210485; 5 | const sqrt2 = 1.4142135623730950488016887242097; 6 | 7 | pub const Direction = enum(u8) { 8 | none = 0, 9 | 10 | n = 0b0000_0001, // 1 11 | e = 0b0000_0100, // 4 12 | s = 0b0000_0011, // 3 13 | w = 0b0000_1100, // 12 14 | 15 | se = 0b0000_0111, // 5 16 | ne = 0b0000_0101, // 7 17 | nw = 0b0000_1101, // 15 18 | sw = 0b0000_1111, // 13 19 | 20 | /// Returns closest direction of size to the supplied vector. 21 | pub fn find(comptime size: usize, vx: f32, vy: f32) Direction { 22 | return switch (size) { 23 | 4 => { 24 | var d: u8 = 0; 25 | 26 | const absx = @abs(vx); 27 | const absy = @abs(vy); 28 | 29 | if (absy < absx * sqrt2) { 30 | //x 31 | if (vx > 0) d = 0b0000_0100 else if (vx < 0) d = 0b0000_1100; 32 | } else { 33 | //y 34 | if (vy > 0) d = 0b0000_0001 else if (vy < 0) d = 0b0000_0011; 35 | } 36 | 37 | return @as(Direction, @enumFromInt(d)); 38 | }, 39 | 40 | 8 => { 41 | var d: u8 = 0; 42 | 43 | const absx = @abs(vx); 44 | const absy = @abs(vy); 45 | 46 | if (absy < absx * (sqrt2 + 1.0)) { 47 | // x 48 | if (vx > 0) d = 0b0000_0100 else if (vx < 0) d = 0b0000_1100; 49 | } 50 | if (absy > absx * (sqrt2 - 1.0)) { 51 | // y 52 | if (vy > 0) d = d | 0b0000_0001 else if (vy < 0) d = d | 0b0000_0011; 53 | } 54 | 55 | return @as(Direction, @enumFromInt(d)); 56 | }, 57 | else => @compileError("Direction size is unsupported"), 58 | }; 59 | } 60 | 61 | /// Writes the actual bits of the direction. 62 | /// Useful for converting input to directions. 63 | pub fn write(n: bool, s: bool, e: bool, w: bool) Direction { 64 | var d: u8 = 0; 65 | if (w) { 66 | d = d | 0b0000_1100; 67 | } 68 | if (e) { 69 | d = d | 0b0000_0100; 70 | } 71 | if (n) { 72 | d = d | 0b0000_0001; 73 | } 74 | if (s) { 75 | d = d | 0b0000_0011; 76 | } 77 | 78 | return @as(Direction, @enumFromInt(d)); 79 | } 80 | 81 | /// Returns horizontal axis of the direction. 82 | pub fn x(self: Direction) f32 { 83 | return @as(f32, @floatFromInt(@as(i8, @bitCast(@intFromEnum(self))) << 4 >> 6)); 84 | } 85 | 86 | /// Returns vertical axis of the direction. 87 | pub fn y(self: Direction) f32 { 88 | return @as(f32, @floatFromInt(@as(i8, @bitCast(@intFromEnum(self))) << 6 >> 6)); 89 | } 90 | 91 | /// Returns direction as a F32x4. 92 | pub fn f32x4(self: Direction) zm.F32x4 { 93 | return zm.f32x4(self.x(), self.y(), 0, 0); 94 | } 95 | 96 | /// Returns direction as a normalized F32x4. 97 | pub fn normalized(self: Direction) zm.F32x4 { 98 | return switch (self) { 99 | .none => zm.f32x4s(0), 100 | .s => zm.f32x4(0, -1, 0, 0), 101 | .se => zm.f32x4(sqrt, -sqrt, 0, 0), 102 | .e => zm.f32x4(1, 0, 0, 0), 103 | .ne => zm.f32x4(sqrt, sqrt, 0, 0), 104 | .n => zm.f32x4(0, 1, 0, 0), 105 | .nw => zm.f32x4(-sqrt, sqrt, 0, 0), 106 | .w => zm.f32x4(-1, 0, 0, 0), 107 | .sw => zm.f32x4(-1, -1, 0, 0), 108 | }; 109 | } 110 | 111 | /// Returns true if direction is flipped to face west. 112 | pub fn flippedHorizontally(self: Direction) bool { 113 | return switch (self) { 114 | .nw, .w, .sw => true, 115 | else => false, 116 | }; 117 | } 118 | 119 | /// Returns true if direction is flipped to face north. 120 | pub fn flippedVertically(self: Direction) bool { 121 | return switch (self) { 122 | .nw, .n, .ne => true, 123 | else => false, 124 | }; 125 | } 126 | 127 | pub fn rotateCW(self: Direction) Direction { 128 | return switch (self) { 129 | .s => .sw, 130 | .se => .s, 131 | .e => .se, 132 | .ne => .e, 133 | .n => .ne, 134 | .nw => .n, 135 | .w => .nw, 136 | .sw => .w, 137 | .none => .none, 138 | }; 139 | } 140 | 141 | pub fn rotateCCW(self: Direction) Direction { 142 | return switch (self) { 143 | .s => .se, 144 | .se => .e, 145 | .e => .ne, 146 | .ne => .n, 147 | .n => .nw, 148 | .nw => .w, 149 | .w => .sw, 150 | .sw => .s, 151 | .none => .none, 152 | }; 153 | } 154 | 155 | pub fn fmt(self: Direction) [:0]const u8 { 156 | return switch (self) { 157 | .s => "south", 158 | .se => "southeast", 159 | .e => "east", 160 | .ne => "northeast", 161 | .n => "north", 162 | .nw => "northwest", 163 | .w => "west", 164 | .sw => "southwest", 165 | .none => "none", 166 | }; 167 | } 168 | }; 169 | 170 | test "Direction" { 171 | var direction: Direction = .none; 172 | 173 | direction = Direction.find(8, 1, 1); 174 | std.testing.expect(direction == .se); 175 | std.testing.expectEqual(zm.f32x4(1, 1, 0, 0), direction.f32x4()); 176 | std.testing.expectEqual(zm.f32x4(sqrt, sqrt, 0, 0), direction.normalized()); 177 | 178 | direction = Direction.find(8, 0, 1); 179 | std.testing.expect(direction == .s); 180 | 181 | direction = Direction.find(8, -1, -1); 182 | std.testing.expect(direction == .nw); 183 | std.testing.expect(direction.flippedHorizontally() == true); 184 | 185 | direction = Direction.find(4, 1, 1); 186 | std.testing.expect(direction == .e); 187 | std.testing.expect(direction.flippedHorizontally() == false); 188 | } 189 | -------------------------------------------------------------------------------- /src/math/math.zig: -------------------------------------------------------------------------------- 1 | const zm = @import("zmath"); 2 | const game = @import("game"); 3 | const std = @import("std"); 4 | 5 | pub const sqrt2: f32 = 1.414213562373095; 6 | 7 | pub const Direction = @import("direction.zig").Direction; 8 | const rect = @import("rect.zig"); 9 | pub const Rect = rect.Rect; 10 | pub const RectF = rect.RectF; 11 | const color = @import("color.zig"); 12 | pub const Color = color.Color; 13 | pub const Colors = color.Colors; 14 | pub const Tween = @import("tween.zig").Tween; 15 | 16 | pub const Point = struct { x: i32, y: i32 }; 17 | 18 | pub fn lerp(a: f32, b: f32, t: f32) f32 { 19 | return a + (b - a) * t; 20 | } 21 | 22 | pub fn ease(a: f32, b: f32, t: f32, ease_type: EaseType) f32 { 23 | return switch (ease_type) { 24 | .linear => lerp(a, b, t), 25 | .ease_in => lerp(a, b, square(t)), 26 | .ease_out => lerp(a, b, flip(square(flip(t)))), 27 | .ease_in_out => lerp(a, b, -(std.math.cos(std.math.pi * t) - 1.0) / 2.0), 28 | }; 29 | } 30 | 31 | fn square(t: f32) f32 { 32 | return t * t; 33 | } 34 | 35 | fn flip(t: f32) f32 { 36 | return 1.0 - t; 37 | } 38 | 39 | pub const EaseType = enum { 40 | linear, 41 | ease_in, 42 | ease_out, 43 | ease_in_out, 44 | }; 45 | -------------------------------------------------------------------------------- /src/math/rect.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const math = @import("math.zig"); 3 | 4 | pub const Rect = struct { 5 | x: i32 = 0, 6 | y: i32 = 0, 7 | width: i32 = 0, 8 | height: i32 = 0, 9 | 10 | pub fn init(x: i32, y: i32, width: i32, height: i32) Rect { 11 | return .{ .x = x, .y = y, .width = width, .height = height }; 12 | } 13 | 14 | pub fn top(self: Rect) i32 { 15 | return self.y; 16 | } 17 | 18 | pub fn bottom(self: Rect) i32 { 19 | return self.y + self.height; 20 | } 21 | 22 | pub fn left(self: Rect) i32 { 23 | return self.x; 24 | } 25 | 26 | pub fn right(self: Rect) i32 { 27 | return self.x + self.width; 28 | } 29 | 30 | pub fn containsPoint(self: Rect, point: math.Point) bool { 31 | return self.x <= point.x and point.x < self.right() and self.y <= point.y and point.y < self.bottom(); 32 | } 33 | 34 | // pub fn containsVector2 (self: Rect, vector: math.Vector2) bool { 35 | 36 | // return self.x <= vector.x and vector.x < self.right() and self.y <= vector.y and vector.y < self.bottom(); 37 | // } 38 | 39 | pub fn rectF(self: Rect) RectF { 40 | return .{ 41 | .x = @as(f32, @floatFromInt(self.x)), 42 | .y = @as(f32, @floatFromInt(self.y)), 43 | .width = @as(f32, @floatFromInt(self.width)), 44 | .height = @as(f32, @floatFromInt(self.height)), 45 | }; 46 | } 47 | }; 48 | 49 | pub const RectF = struct { 50 | x: f32 = 0, 51 | y: f32 = 0, 52 | width: f32 = 0, 53 | height: f32 = 0, 54 | 55 | pub fn init(x: f32, y: f32, width: f32, height: f32) Rect { 56 | return .{ .x = x, .y = y, .width = width, .height = height }; 57 | } 58 | 59 | pub fn top(self: RectF) f32 { 60 | return self.y; 61 | } 62 | 63 | pub fn bottom(self: RectF) f32 { 64 | return self.y + self.height; 65 | } 66 | 67 | pub fn left(self: RectF) f32 { 68 | return self.x; 69 | } 70 | 71 | pub fn right(self: RectF) f32 { 72 | return self.x + self.width; 73 | } 74 | 75 | pub fn rect(self: RectF) Rect { 76 | return .{ 77 | .x = @as(i32, @intFromFloat(self.x)), 78 | .y = @as(i32, @intFromFloat(self.y)), 79 | .width = @as(i32, @intFromFloat(self.width)), 80 | .height = @as(i32, @intFromFloat(self.height)), 81 | }; 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/math/tween.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | const zmath = @import("zmath"); 4 | 5 | pub const Tween = enum { 6 | none, 7 | linear, 8 | }; 9 | -------------------------------------------------------------------------------- /src/pixi.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mach = @import("mach"); 3 | const Core = mach.Core; 4 | 5 | pub const version: std.SemanticVersion = .{ 6 | .major = 0, 7 | .minor = 2, 8 | .patch = 0, 9 | }; 10 | 11 | // Generated files, these contain helpers for autocomplete 12 | // So you can get a named index into atlas.sprites 13 | pub const paths = @import("generated/paths.zig"); 14 | pub const atlas = @import("generated/atlas.zig"); 15 | 16 | // Other helpers and namespaces 17 | pub const algorithms = @import("algorithms/algorithms.zig"); 18 | pub const fa = @import("tools/font_awesome.zig"); 19 | pub const fs = @import("tools/fs.zig"); 20 | pub const gfx = @import("gfx/gfx.zig"); 21 | pub const input = @import("input/input.zig"); 22 | pub const math = @import("math/math.zig"); 23 | pub const shaders = @import("shaders/shaders.zig"); 24 | 25 | // Modules 26 | 27 | /// App contains the main schedule, which is run by the mach entrypoint 28 | pub const App = @import("App.zig"); 29 | pub const Artboard = @import("editor/artboard/Artboard.zig"); 30 | pub const Assets = @import("Assets.zig"); 31 | pub const Editor = @import("editor/Editor.zig"); 32 | pub const Explorer = @import("editor/explorer/Explorer.zig"); 33 | pub const Packer = @import("tools/Packer.zig"); 34 | pub const Popups = @import("editor/popups/Popups.zig"); 35 | pub const Sidebar = @import("editor/Sidebar.zig"); 36 | 37 | // The set of Mach modules our application may use. 38 | pub const Modules = mach.Modules(.{ 39 | App, 40 | Artboard, 41 | Assets, 42 | Core, 43 | Editor, 44 | Explorer, 45 | Packer, 46 | Popups, 47 | Sidebar, 48 | }); 49 | 50 | // Global pointers 51 | pub var core: *Core = undefined; 52 | pub var app: *App = undefined; 53 | pub var editor: *Editor = undefined; 54 | pub var packer: *Packer = undefined; 55 | pub var assets: *Assets = undefined; 56 | 57 | /// Internal types 58 | /// These types contain additional data to support the editor 59 | /// An example of this is File. pixi.File matches the file type to read from JSON, 60 | /// while the pixi.Internal.File contains cameras, timers, file-specific editor fields. 61 | pub const Internal = struct { 62 | pub const Animation = @import("internal/Animation.zig"); 63 | pub const Atlas = @import("internal/Atlas.zig"); 64 | pub const Buffers = @import("internal/Buffers.zig"); 65 | pub const File = @import("internal/File.zig"); 66 | pub const Frame = @import("internal/Frame.zig"); 67 | pub const History = @import("internal/History.zig"); 68 | pub const Keyframe = @import("internal/Keyframe.zig"); 69 | pub const KeyframeAnimation = @import("internal/KeyframeAnimation.zig"); 70 | pub const Layer = @import("internal/Layer.zig"); 71 | pub const Palette = @import("internal/Palette.zig"); 72 | pub const Reference = @import("internal/Reference.zig"); 73 | pub const Sprite = @import("internal/Sprite.zig"); 74 | }; 75 | 76 | /// Frame-by-frame sprite animation 77 | pub const Animation = Internal.Animation; 78 | 79 | /// Contains lists of sprites and animations 80 | pub const Atlas = @import("Atlas.zig"); 81 | 82 | /// The data that gets written to disk in a .pixi file and read back into this type 83 | pub const File = @import("File.zig"); 84 | 85 | /// Contains information such as the name, visibility and collapse settings of a texture layer 86 | pub const Layer = @import("Layer.zig"); 87 | 88 | /// Source location within the atlas texture and origin location 89 | pub const Sprite = @import("Sprite.zig"); 90 | -------------------------------------------------------------------------------- /src/shaders/compute.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) var tex: texture_2d; 2 | @group(0) @binding(1) var buf: array>; 3 | 4 | @compute @workgroup_size(1) 5 | fn copyTextureToBuffer(@builtin(global_invocation_id) id: vec3) { 6 | let size = textureDimensions(tex); 7 | buf[id.y * size.x + id.x] = textureLoad(tex, id.xy, 0); 8 | } -------------------------------------------------------------------------------- /src/shaders/default.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | mvp: mat4x4, 3 | } 4 | @group(0) @binding(0) var uniforms: Uniforms; 5 | 6 | struct VertexOut { 7 | @builtin(position) position_clip: vec4, 8 | @location(0) position: vec3, 9 | @location(1) uv: vec2, 10 | @location(2) color: vec4, 11 | @location(3) data: vec3 12 | } 13 | @vertex fn vert_main( 14 | @location(0) position: vec3, 15 | @location(1) uv: vec2, 16 | @location(2) color: vec4, 17 | @location(3) data: vec3 18 | ) -> VertexOut { 19 | var output: VertexOut; 20 | output.position_clip = vec4(position.xy, 0.0, 1.0) * uniforms.mvp; 21 | output.uv = uv; 22 | output.color = color; 23 | output.data = data; 24 | return output; 25 | } 26 | 27 | @group(0) @binding(1) var diffuse: texture_2d; 28 | @group(0) @binding(2) var diffuse_sampler: sampler; 29 | @fragment fn frag_main( 30 | @location(0) position: vec3, 31 | @location(1) uv: vec2, 32 | @location(2) color: vec4, 33 | @location(3) data: vec3, 34 | ) -> @location(0) vec4 { 35 | var sample = textureSample(diffuse, diffuse_sampler, uv); 36 | return sample * color; 37 | } -------------------------------------------------------------------------------- /src/shaders/shaders.zig: -------------------------------------------------------------------------------- 1 | pub const default_vs = @embedFile("default_vs.wgsl"); 2 | pub const default_fs = @embedFile("default_fs.wgsl"); 3 | -------------------------------------------------------------------------------- /src/tools/LDTKTileset.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pixi = @import("../pixi.zig"); 3 | const core = @import("mach").core; 4 | 5 | pub const LDTKCompatibility = struct { 6 | tilesets: []LDTKTileset, 7 | }; 8 | 9 | const LDTKTileset = @This(); 10 | 11 | pub const LDTKSprite = struct { 12 | src: [2]u32, 13 | }; 14 | 15 | layer_paths: [][:0]const u8, 16 | sprite_size: [2]u32, 17 | sprites: []LDTKSprite, 18 | -------------------------------------------------------------------------------- /src/tools/fs.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// reads the contents of a file. Returned value is owned by the caller and must be freed! 4 | pub fn read(allocator: std.mem.Allocator, filename: []const u8) ![]u8 { 5 | const file = try std.fs.cwd().openFile(filename, .{}); 6 | 7 | defer file.close(); 8 | const file_size = try file.getEndPos(); 9 | 10 | var buffer = try allocator.alloc(u8, file_size); 11 | 12 | _ = try file.read(buffer[0..buffer.len]); 13 | 14 | return buffer; 15 | } 16 | 17 | /// reads the contents of a file. Returned value is owned by the caller and must be freed! 18 | pub fn readZ(allocator: std.mem.Allocator, filename: []const u8) ![:0]u8 { 19 | const file = try std.fs.cwd().openFile(filename, .{}); 20 | defer file.close(); 21 | 22 | const file_size = try file.getEndPos(); 23 | var buffer = try allocator.alloc(u8, file_size + 1); 24 | _ = try file.read(buffer[0..file_size]); 25 | buffer[file_size] = 0; 26 | 27 | return buffer[0..file_size :0]; 28 | } 29 | -------------------------------------------------------------------------------- /src/tools/gif.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cgif = @cImport(@cInclude("cgif.h")); 3 | const quant = @import("quantize/quantize.zig"); 4 | const zstbi = @import("zstbi"); 5 | 6 | const Allocator = std.mem.Allocator; 7 | 8 | const GifError = error{ 9 | gif_make_failed, 10 | gif_open_failed, 11 | gif_close_failed, 12 | malloc_failed, 13 | gif_write_failed, 14 | invalid_index, 15 | cgif_pending, 16 | cgif_unknown_error, 17 | 18 | gif_uninitialized, 19 | 20 | unknown_error_pls_report_bug, 21 | }; 22 | 23 | pub const GifFrame = struct { 24 | bgra_buf: []const u8, 25 | duration_ms: u64, 26 | }; 27 | 28 | pub const GifConfig = struct { 29 | transparent: bool = false, 30 | use_dithering: bool = true, 31 | use_local_palette: bool = true, 32 | path: [:0]const u8, 33 | width: usize, 34 | height: usize, 35 | }; 36 | 37 | pub const Gif = struct { 38 | const Self = @This(); 39 | 40 | allocator: Allocator, 41 | 42 | cgif_config: *cgif.CGIF_Config, 43 | cgif_frame_config: *cgif.CGIF_FrameConfig, 44 | gif: ?*cgif.CGIF, 45 | path: [:0]const u8, 46 | 47 | config: GifConfig, 48 | 49 | pub fn init(allocator: Allocator, config: GifConfig) !Self { 50 | // Configure CGIF's config object 51 | const cgif_config = try allocator.create(cgif.CGIF_Config); 52 | initCGifConfig(cgif_config, config.path, config.width, config.height); 53 | cgif_config.attrFlags = cgif.CGIF_ATTR_IS_ANIMATED; 54 | if (config.transparent) 55 | cgif_config.attrFlags |= cgif.CGIF_ATTR_HAS_TRANSPARENCY; 56 | 57 | const cgif_frame_config = try allocator.create(cgif.CGIF_FrameConfig); 58 | initFrameConfig(cgif_frame_config); 59 | 60 | cgif_frame_config.transIndex = 0; 61 | cgif_frame_config.genFlags = cgif.CGIF_FRAME_GEN_USE_DIFF_WINDOW; 62 | 63 | if (config.transparent) 64 | cgif_frame_config.genFlags |= cgif.CGIF_FRAME_GEN_USE_TRANSPARENCY; 65 | 66 | var gif: ?*cgif.CGIF = null; 67 | if (config.use_local_palette) { 68 | cgif_config.attrFlags |= @intCast(cgif.CGIF_ATTR_NO_GLOBAL_TABLE); 69 | cgif_frame_config.attrFlags |= @intCast(cgif.CGIF_FRAME_ATTR_USE_LOCAL_TABLE); 70 | 71 | // If we're using a local palette, we can create a GIF object right away. 72 | // For global palettes, we need to wait for the frames first. 73 | // We cannot create a palette object for CGIF before seeing all the frames. 74 | gif = cgif.cgif_newgif(cgif_config) orelse { 75 | return GifError.gif_make_failed; 76 | }; 77 | } 78 | 79 | return .{ 80 | .allocator = allocator, 81 | .cgif_config = cgif_config, 82 | .cgif_frame_config = cgif_frame_config, 83 | .gif = gif, 84 | .path = config.path, 85 | .config = config, 86 | }; 87 | } 88 | 89 | pub fn addFrames(self: *Self, frames: []zstbi.Image, fps: u32) !void { 90 | for (frames) |frame| { 91 | const duration: u64 = @as(u64, @intFromFloat((1.0 / @as(f32, @floatFromInt(fps))) * 1000.0)); 92 | try self.addFrame(frame, duration); 93 | } 94 | } 95 | 96 | pub fn addFrame(self: *Self, frame: zstbi.Image, duration_ms: u64) !void { 97 | if (!self.config.use_local_palette) { 98 | std.debug.panic("Unimplemented!", .{}); 99 | } 100 | 101 | const gif = self.gif orelse return GifError.gif_uninitialized; 102 | 103 | const quantized = try quant.quantizeBgraImage( 104 | self.allocator, 105 | frame.data, 106 | self.config.width, 107 | self.config.height, 108 | quant.Quantize.median_cut, 109 | self.config.use_dithering, 110 | ); 111 | 112 | // CGIF uses units of 0.01s for frame delay. 113 | const duration = @as(f64, @floatFromInt(duration_ms)) / 10.0; 114 | const duration_int: u64 = @intFromFloat(@round(duration)); 115 | 116 | self.cgif_frame_config.delay = @truncate(duration_int); 117 | self.cgif_frame_config.pImageData = quantized.image_buffer.ptr; 118 | self.cgif_frame_config.pLocalPalette = quantized.color_table.ptr; 119 | self.cgif_frame_config.numLocalPaletteEntries = @intCast(quantized.color_table.len / 3); 120 | 121 | const err_code = cgif.cgif_addframe(gif, self.cgif_frame_config); 122 | if (err_code != 0) { 123 | return cgifError(err_code); 124 | } 125 | } 126 | 127 | pub fn close(self: *Self) GifError!void { 128 | if (self.gif == null) { 129 | return GifError.gif_uninitialized; 130 | } 131 | 132 | const err_code = cgif.cgif_close(self.gif); 133 | self.gif = null; 134 | if (err_code != 0) { 135 | return cgifError(err_code); 136 | } 137 | } 138 | 139 | /// Convert a CGIF error to a GifError. 140 | fn cgifError(err_code: c_int) GifError { 141 | return switch (err_code) { 142 | cgif.CGIF_ERROR => GifError.cgif_unknown_error, 143 | cgif.CGIF_EOPEN => GifError.gif_open_failed, 144 | cgif.CGIF_EWRITE => GifError.gif_write_failed, 145 | cgif.CGIF_ECLOSE => GifError.gif_close_failed, 146 | cgif.CGIF_EALLOC => GifError.malloc_failed, 147 | cgif.CGIF_EINDEX => GifError.invalid_index, 148 | else => GifError.unknown_error_pls_report_bug, 149 | }; 150 | } 151 | 152 | pub fn deinit(self: *const Self) void { 153 | self.allocator.destroy(self.cgif_config); 154 | self.allocator.destroy(self.cgif_frame_config); 155 | } 156 | }; 157 | 158 | /// Intialize a cgif gif config struct. 159 | fn initCGifConfig( 160 | gif_config: *cgif.CGIF_Config, 161 | path: [:0]const u8, 162 | width: usize, 163 | height: usize, 164 | ) void { 165 | // in a c program, this would be a memset(gif_config, 0), but we can't do that in Zig 166 | gif_config.pGlobalPalette = null; 167 | gif_config.pContext = null; 168 | gif_config.pWriteFn = null; 169 | gif_config.attrFlags = 0; 170 | gif_config.genFlags = 0; 171 | gif_config.numGlobalPaletteEntries = 0; 172 | gif_config.numLoops = 0; 173 | 174 | gif_config.path = path.ptr; 175 | gif_config.width = @intCast(width); 176 | gif_config.height = @intCast(height); 177 | } 178 | 179 | /// Intialize a cgif frame config struct. 180 | pub fn initFrameConfig(conf: *cgif.CGIF_FrameConfig) void { 181 | conf.pLocalPalette = null; 182 | conf.pImageData = null; 183 | 184 | conf.numLocalPaletteEntries = 0; 185 | conf.attrFlags = 0; 186 | conf.genFlags = 0; 187 | conf.transIndex = 0; 188 | 189 | conf.delay = 0; 190 | } 191 | -------------------------------------------------------------------------------- /src/tools/quantize/fixed-stack.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn FixedStack(comptime T: type, comptime capacity: usize) type { 4 | return struct { 5 | const Self = @This(); 6 | 7 | data: [capacity]T = undefined, 8 | ptr: usize = 0, 9 | 10 | pub inline fn push(self: *Self, value: T) void { 11 | std.debug.assert(self.ptr < capacity); 12 | self.data[self.ptr] = value; 13 | self.ptr += 1; 14 | } 15 | 16 | pub inline fn isEmpty(self: *const Self) bool { 17 | return self.ptr == 0; 18 | } 19 | 20 | pub inline fn pop(self: *Self) T { 21 | std.debug.assert(self.ptr >= 0); 22 | self.ptr -= 1; 23 | return self.data[self.ptr]; 24 | } 25 | 26 | pub inline fn top(self: *Self) T { 27 | std.debug.assert(self.ptr >= 0); 28 | return self.data[self.ptr - 1]; 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/tools/quantize/kdtree-benchmark.zig: -------------------------------------------------------------------------------- 1 | const Kd = @import("kd-tree.zig"); 2 | const std = @import("std"); 3 | const Timer = @import("timer"); 4 | 5 | pub fn main() !void { 6 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 7 | const allocator = gpa.allocator(); 8 | 9 | const ncolors = 255; 10 | var color_table: [ncolors]u8 = undefined; 11 | 12 | var gen = std.rand.DefaultPrng.init(@abs(std.time.milliTimestamp())); 13 | for (0..color_table.len) |i| { 14 | color_table[i] = gen.random().int(u8); 15 | } 16 | 17 | var kdtree = try Kd.KDTree.init(allocator, &color_table); 18 | 19 | var t = Timer{}; 20 | var total_time: i64 = 0; 21 | const ntimes = 10000; 22 | for (0..ntimes) |_| { 23 | const qcolor = [3]u8{ 24 | gen.random().int(u8), 25 | gen.random().int(u8), 26 | gen.random().int(u8), 27 | }; 28 | 29 | t.start(); 30 | _ = kdtree.findNearestColor(qcolor); 31 | total_time += t.end(); 32 | } 33 | 34 | const avg_time = @as(f64, @floatFromInt(total_time)) / ntimes; 35 | std.debug.print("Average lookup time: {d}ms\n", .{avg_time}); 36 | } 37 | -------------------------------------------------------------------------------- /src/tools/quantize/quantize.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const median_cut = @import("median-cut.zig"); 3 | 4 | pub const QuantizerConfig = struct { 5 | width: usize, 6 | height: usize, 7 | use_dithering: bool, 8 | allocator: std.mem.Allocator, 9 | ncolors: u16 = 256, 10 | }; 11 | 12 | /// A single RGB image represented as a list of indices 13 | /// into a color table. 14 | pub const QuantizedImage = struct { 15 | const Self = @This(); 16 | /// RGBRGBRGB... 17 | color_table: []u8, 18 | /// indices into the color table 19 | image_buffer: []u8, 20 | 21 | pub fn init(color_table: []u8, image_buffer: []u8) Self { 22 | return .{ .color_table = color_table, .image_buffer = image_buffer }; 23 | } 24 | 25 | pub fn deinit(self: *const Self, allocator: std.mem.Allocator) void { 26 | allocator.free(self.color_table); 27 | allocator.free(self.image_buffer); 28 | } 29 | }; 30 | 31 | /// A list of frames that are represented as arrays of indices into 32 | /// a common global color table. 33 | pub const QuantizedFrames = struct { 34 | const Self = @This(); 35 | /// RGBRGBRGB... * 256 36 | color_table: []u8, 37 | /// A list of frames where each frame is a 38 | /// list of indices into the color table. 39 | frames: [][]u8, 40 | 41 | /// The allocator used to allocate the color table and the frames. 42 | allocator: std.mem.Allocator, 43 | 44 | pub fn init(allocator: std.mem.Allocator, table: []u8, frames: [][]u8) !Self { 45 | return Self{ 46 | .allocator = allocator, 47 | .color_table = table, 48 | .frames = frames, 49 | }; 50 | } 51 | 52 | pub fn deinit(self: *const Self) void { 53 | self.allocator.free(self.color_table); 54 | for (self.frames) |frame| { 55 | self.allocator.free(frame); 56 | } 57 | self.allocator.free(self.frames); 58 | } 59 | }; 60 | 61 | pub const Quantize = enum { median_cut, kd_tree }; 62 | 63 | pub fn quantizeBgraFrames( 64 | allocator: std.mem.Allocator, 65 | bgra_bufs: []const []const u8, 66 | width: usize, 67 | height: usize, 68 | method: Quantize, 69 | use_dithering: bool, 70 | ) !QuantizedFrames { 71 | const config = QuantizerConfig{ 72 | .width = width, 73 | .height = height, 74 | .use_dithering = use_dithering, 75 | .allocator = allocator, 76 | }; 77 | 78 | switch (method) { 79 | Quantize.median_cut => { 80 | return try median_cut.quantizeBgraFrames(config, bgra_bufs); 81 | }, 82 | else => std.debug.panic("not implemented!", .{}), 83 | } 84 | } 85 | 86 | pub fn quantizeBgraImage( 87 | allocator: std.mem.Allocator, 88 | bgra_buf: []const u8, 89 | width: usize, 90 | height: usize, 91 | method: Quantize, 92 | use_dithering: bool, 93 | ) !QuantizedImage { 94 | const config = QuantizerConfig{ 95 | .width = width, 96 | .height = height, 97 | .use_dithering = use_dithering, 98 | .allocator = allocator, 99 | }; 100 | 101 | switch (method) { 102 | Quantize.median_cut => { 103 | return try median_cut.quantizeBgraImage(config, bgra_buf); 104 | }, 105 | else => std.debug.panic("not implemented!", .{}), 106 | } 107 | } 108 | 109 | /// Reduce the number of colors in an image down to a specific number. 110 | pub fn reduceColors( 111 | allocator: std.mem.Allocator, 112 | bgra_buf: []const u8, 113 | width: usize, 114 | height: usize, 115 | colors: u16, 116 | dither: bool, 117 | ) !QuantizedImage { 118 | const config = QuantizerConfig{ 119 | .width = width, 120 | .height = height, 121 | .use_dithering = dither, 122 | .allocator = allocator, 123 | .ncolors = colors, 124 | }; 125 | 126 | return try median_cut.quantizeBgraImage(config, bgra_buf); 127 | } 128 | -------------------------------------------------------------------------------- /src/tools/timer.zig: -------------------------------------------------------------------------------- 1 | // A simple timer utility for benchmarking. 2 | const std = @import("std"); 3 | 4 | const Self = @This(); 5 | start_time: i64 = -1, 6 | done: bool = false, 7 | 8 | pub fn start(self: *Self) void { 9 | self.start_time = std.time.milliTimestamp(); 10 | self.done = false; 11 | } 12 | 13 | pub fn end(self: *Self) i64 { 14 | if (self.start_time == -1 or self.done) { 15 | std.debug.panic("Timer already ended", .{}); 16 | return -1; 17 | } 18 | self.done = true; 19 | 20 | const end_time = std.time.milliTimestamp(); 21 | const elapsed = end_time - self.start_time; 22 | return elapsed; 23 | } 24 | -------------------------------------------------------------------------------- /src/tools/watcher/MacosWatcher.zig: -------------------------------------------------------------------------------- 1 | const MacosWatcher = @This(); 2 | 3 | const std = @import("std"); 4 | const Assets = @import("../../Assets.zig"); 5 | const c = @cImport({ 6 | @cInclude("CoreServices/CoreServices.h"); 7 | }); 8 | 9 | const log = std.log.scoped(.watcher); 10 | 11 | pub fn init( 12 | allocator: std.mem.Allocator, 13 | ) !MacosWatcher { 14 | _ = allocator; 15 | 16 | return .{}; 17 | } 18 | 19 | pub fn callback( 20 | streamRef: c.ConstFSEventStreamRef, 21 | clientCallBackInfo: ?*anyopaque, 22 | numEvents: usize, 23 | eventPaths: ?*anyopaque, 24 | eventFlags: ?[*]const c.FSEventStreamEventFlags, 25 | eventIds: ?[*]const c.FSEventStreamEventId, 26 | ) callconv(.C) void { 27 | _ = eventIds; 28 | _ = eventFlags; 29 | _ = streamRef; 30 | const ctx: *Context = @alignCast(@ptrCast(clientCallBackInfo)); 31 | 32 | const paths: [*][*:0]u8 = @alignCast(@ptrCast(eventPaths)); 33 | for (paths[0..numEvents]) |p| { 34 | const path = std.mem.span(p); 35 | 36 | const basename = std.fs.path.basename(path); 37 | var base_path = path[0 .. path.len - basename.len]; 38 | if (std.mem.endsWith(u8, base_path, "/")) 39 | base_path = base_path[0 .. base_path.len - 1]; 40 | 41 | ctx.assets.onAssetChange(base_path, basename); 42 | } 43 | } 44 | 45 | pub fn stop(_: *MacosWatcher) void { 46 | c.CFRunLoopStop(c.CFRunLoopGetCurrent()); 47 | } 48 | 49 | const Context = struct { 50 | assets: *Assets, 51 | }; 52 | pub fn listen( 53 | _: *MacosWatcher, 54 | assets: *Assets, 55 | ) !void { 56 | const in_paths = try assets.getWatchPaths(assets.allocator); 57 | var macos_paths = try assets.allocator.alloc(c.CFStringRef, in_paths.len); 58 | 59 | for (in_paths, macos_paths[0..]) |str, *ref| { 60 | ref.* = c.CFStringCreateWithCString( 61 | null, 62 | str.ptr, 63 | c.kCFStringEncodingUTF8, 64 | ); 65 | } 66 | 67 | const paths_to_watch: c.CFArrayRef = c.CFArrayCreate( 68 | null, 69 | @ptrCast(macos_paths.ptr), 70 | @intCast(macos_paths.len), 71 | null, 72 | ); 73 | 74 | var ctx: Context = .{ 75 | .assets = assets, 76 | }; 77 | 78 | var stream_context: c.FSEventStreamContext = .{ .info = &ctx }; 79 | const stream: c.FSEventStreamRef = c.FSEventStreamCreate( 80 | null, 81 | &callback, 82 | &stream_context, 83 | paths_to_watch, 84 | c.kFSEventStreamEventIdSinceNow, 85 | 0.05, 86 | c.kFSEventStreamCreateFlagFileEvents, 87 | ); 88 | 89 | c.FSEventStreamScheduleWithRunLoop( 90 | stream, 91 | c.CFRunLoopGetCurrent(), 92 | c.kCFRunLoopDefaultMode, 93 | ); 94 | 95 | if (c.FSEventStreamStart(stream) == 0) { 96 | @panic("failed to start the event stream"); 97 | } 98 | 99 | // Free allocations before entering the run loop, it will not return 100 | assets.allocator.free(macos_paths); 101 | assets.allocator.free(in_paths); 102 | 103 | c.CFRunLoopRun(); 104 | 105 | c.FSEventStreamStop(stream); 106 | c.FSEventStreamInvalidate(stream); 107 | c.FSEventStreamRelease(stream); 108 | 109 | c.CFRelease(paths_to_watch); 110 | } 111 | --------------------------------------------------------------------------------