Files
photocraft/docs/development.md
T
2026-09-30 05:02:08 -04:00

5.8 KiB

Development guide

Prerequisites

  • Rust stable (1.90+). Add the web target with rustup target add wasm32-unknown-unknown.
  • macOS, Windows or Linux. Linux needs libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libgtk-3-dev.

Build and run

cargo run --release -p photocraft -- path/to/image.psd      # desktop app
cargo run --release -p photocraft -- --control 7878 img.jpg # with the JSON control server
cargo test --workspace                                     # everything
cargo xtask ci                                             # fmt + clippy + tests + layers + wasm
cargo xtask stats                                          # tests and lines per crate

Image code is slow at opt-level 0, so the workspace profile builds dependencies at opt-level 2. Use --release for anything interactive.

Environment variables

Variable Effect
PHOTOCRAFT_CONTROL_PORT Same as --control <port>
PHOTOCRAFT_CPU_CANVAS=1 Force the CPU canvas path instead of the wgpu shader canvas
PHOTOCRAFT_GPU_TILE=2048 Force GPU canvas tiling (tests tile seams)
PHOTOCRAFT_THEME_FILE=tokens.json Debug builds only: live design-token overrides, re-read on change

Live design tokens

{ "card": "#323232", "tab_strip": "#262626", "accent": "#378ef0", "radius": 4 }

Keys are the field names of theme::Tokens (crates/ui-egui/src/theme.rs). Edit and save the file while the app runs to see changes immediately, with no recompile. This is compiled out of release builds.

Driving the app programmatically

Start the app with --control 7878. It then accepts JSON lines on 127.0.0.1:7878:

printf '%s\n' '{"id":1,"method":"engine.execute","params":{"command":"layer.newAdjustmentLayer.hueSaturation","params":{"hue":30}}}' \
              '{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}' | nc 127.0.0.1 7878

See docs/control-protocol.md for every method. Tips:

  • macOS does not render occluded windows. ui.screenshot raises the window first (focus: true by default). Use ui.focus before other visual checks.
  • Capture only our own window. For native captures (to see the real title bar and traffic lights), capture by window id (screencapture -l <CGWindowID>), never a screen region, which can grab other apps.
  • Pointer gestures: ui.pointer takes events in document coordinates and runs them through the same tool state machine as the mouse.

Testing strategy

  • Unit and property tests in every crate. proptest is used for tile COW, regions and codecs.
  • Format crates:
    • synthetic generators (psd::testgen)
    • byte-exact round trips
    • malformed-input sweeps (truncate at every offset)
    • fuzz targets (crates/*/fuzz)
  • Real-file corpus in corpus/ (gitignored). corpus/psd holds MIT-licensed test PSDs, listed with their sources in corpus/psd/SOURCES.md, and cargo xtask corpus --download fetches PngSuite. Tests skip silently when a corpus is absent.
  • Composite oracle: a PSD's embedded merged image is compared with our compositor's output. The pass rate is tracked in the roadmap.
  • UI: unit tests for widgets and state, plus screenshot checks through the control channel.

Performance notes

  • The canvas is presented by a custom WGSL shader (ui-egui/src/gpu_canvas.rs): mip-mapped/nearest sampling, procedural checkerboard, pixel grid, tiling. Brush strokes upload only their damage rect.
  • Compositing is still CPU (photocraft-compose). Live adjustment previews on large documents use a downsampled proxy (ui-egui/src/proxy.rs). The GPU compositor is milestone M5.
  • ui.inspect returns perf timings (UI ms per frame, composite ms, upload ms).
  • Never scan full surfaces per frame. Cache per document revision (PhotocraftApp::cached_bounds). An uncached content_bounds() on a 36 MP layer once cost 77 ms per frame.

Web build

apps/photocraft-web runs the same PhotocraftApp in the browser through eframe's web runner. The renderer is wgpu: WebGPU where the browser has it, WebGL2 otherwise. It is Rust only. The only JavaScript is the glue that wasm-bindgen generates.

brew install trunk                 # or: cargo install trunk --locked
cd apps/photocraft-web
trunk build --release              # writes ../../dist/web (index.html, .js glue, .wasm)
trunk serve --release              # dev server on http://127.0.0.1:8765

Any static file server works for dist/web, for example python3 -m http.server 8765 run inside that directory. Trunk downloads the matching wasm-bindgen and wasm-opt itself. The release .wasm is about 12.7 MB, or 5.0 MB gzipped. Serve it with compression.

URL flags: ?webgl forces the WebGL2 backend, and ?cpu forces the CPU canvas path.

How the web shell (apps/photocraft-web/src/web.rs) differs from desktop:

  • Open uses rfd::AsyncFileDialog. The bytes arrive asynchronously in Services::inbox, which the app drains every frame.
  • Save / Save As / Export trigger a browser download of the encoded bytes. The shell does this with a Blob, an object URL and a temporary <a download>, all created from Rust. There is no save dialog, so the suggested name becomes the download name.
  • Drag-and-drop: WebShell takes the frame's dropped_files before the app sees them. It reads each file with DroppedFile::bytes_async and pushes the bytes into the inbox.
  • No control server: browsers can't listen on TCP. To automate the web build, drive headless Chrome with --remote-debugging-port. Page.setInterceptFileChooserDialog plus DOM.setFileInputFiles covers Open, Input.dispatchDragEvent with files covers drops, and Browser.setDownloadBehavior captures downloads.
  • Headless Chrome on macOS (--headless=new --enable-unsafe-webgpu) gets a real WebGPU adapter.