├── .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 | 
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 | 
15 |
16 |
17 |
18 |
19 |
20 |
21 | [](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 |
--------------------------------------------------------------------------------