TriwarrandFredApps 7c64653d5c Add toolbar side toggle (#11193)
* Add a toolbar side toggle and dock-edge resizing

- Let the toolbox sit on the canvas' left or right, persisted in the panel
  state and switchable from the toolbar header and preferences.
- In the compact arrangement, stack dock, rail and toolbox on the dock's
  physical edge so the toolbox cannot cover the rail.
- Make the dock panel's inner edge the single resize handle: it drives the
  panel width and the dock total (split size and column widths) in the same
  frame, so the rail and dock content stay bound with no reserved room and
  no second handle. The docking layout's own column-edge handle is covered;
  the bar is the theme separator at rest and the accent on hover/drag.
- Persist the layout, keep the split size in sync, handle RTL, and test the
  toolbar side state.

* Toolbar side: dock-edge only with a lone canvas; test every theme

- Use the between-dock-and-canvas layout only when nothing is docked beside the canvas: that view drops the canvas from craft-ui's split, so with a panel there the standard layout keeps it visible and dockable.
- Add canvas_alone_layout/canvas_alone and a unit test for a lone canvas versus a split canvas side.
- Add a kittest that toggles the toolbar Left/Right across every ThemeKind::ALL entry, asserting the panel lands on the requested physical side.
- No dependency changes: Cargo.toml and Cargo.lock are identical to main, so the repository's exact pins are untouched.

* Toolbar side: keep the canvas a drop target during dock drags

While a panel is being dragged, draw the standard layout even with the toolbar at the dock edge. It keeps the canvas in craft-ui's split, so a panel can still be dropped beside it; after the drop the canvas is no longer alone and the standard layout stays. The between-dock-and-canvas layout resumes for a lone canvas once the drag ends.

* Toolbar side: stable drag layout, unwind-safe dock flag, clippy

- Restore the dock-only thread-local through a drop guard, so an unwinding show does not leave a later standard layout canvas-less.
- Do not switch the dock layout mid-drag: craft-ui records its areas on the press, and changing the shape cancels the drag. The layout stays stable; a panel beside the canvas still selects the standard layout before the drag begins.
- Fix the CI clippy errors: explicit auto-deref in end_dock_node/resize_end_dock and field reassignment in the toolbar_side test.

* Toolbar side: anchor the Edit Toolbar flyout; borrow the canvas layout

- more_slot_flyout takes the resolved ToolbarSide instead of the text direction, so a right-hand toolbar in LTR (and a left-hand toolbar in RTL) anchors the flyout away from the canvas like the tool flyouts.
- canvas_alone borrows the saved layout and treats the native-dock legacy layout as canvas-only, avoiding a per-frame clone and traversal.

* Toolbar side: only a horizontal canvas split uses the dock-edge layout

A saved workspace can put the canvas above another panel in a vertical root split; canvas_alone_layout now requires SplitAxis::Horizontal so that shape keeps the standard layout. The guard test covers the vertical case.

* Toolbar side: document the functions this PR adds

Covers ToolbarSide::resolve/other, canvas_alone_layout, the DockOnlyGuard drop, and the two new tests. Docstring coverage of the PR's own additions is now complete; no behaviour change.

* docs: refresh the scorecard after merging main

The merge left docs/scorecard.md stale against the combined settings table; regenerated with cargo xtask scorecard, which is what the gate's scorecard step checks.

* Toolbar side: honor the workspace lock, empty docks and the end-dock shape

- The dock-edge resize handle is inert while the workspace is locked (hover sense, no highlight) and set_end_dock_total refuses to write then; the drag test covers a locked workspace.
- An empty dock takes only the rail's width instead of a clamped blank column; dock_is_empty reports it and a test empties the dock.
- canvas_alone_layout now requires the second child to be the Dock itself, and end_dock_node/resize_end_dock follow horizontal splits only, so a layout the handle cannot resize keeps the standard layout instead of snapping back.
- Tests: DockOnlyGuard restores the flag on drop and unwinding; the theme test clicks the toolbar's move button instead of setting state; a dock-edge harness drags the edge, checks it persists and that a locked workspace refuses it; axis tests and a two-column ui.set {dockWidth} test.
- The flyout indicator follows the resolved toolbar side, the rail keeps its chrome fill and 4/8 inset with the separator inside it, ATTRIBUTION counts the new icon (116), and docs/control-protocol.md describes dockWidth as the whole end dock.

* Toolbar side: floating windows on empty docks, iconic widths, dockWidth columns

- An empty dock still runs the docking pass on the full remaining area, so Studio's floating Properties window keeps rendering with or without the rail; the dock column itself takes no width. A Studio regression builds an empty dock with a floating Properties group and asserts the pass runs and the window is drawn.
- Dock width now uses each column's displayed width (iconic_width while collapsed to icons), and resizing/scaling keeps iconic widths in step. A collapse-to-icons regression checks a 36-point column not reserving 290.
- ui.set {dockWidth} scales every column to the requested total directly instead of resizing only the first column and then finding the sum already equal. The two-column test also covers a target smaller than the other column's expanded width (250 from [300, 180]) and asserts per-column widths.

* Toolbar side: iconic widths, edge release and modal handling, side persistence

- An iconic-only dock keeps its narrow displayed width instead of being raised to the expanded
  minimum, and the dock-edge panel records its laid-out width for a geometry regression.
- The release frame applies its final pointer position before dropping the drag, so the dock keeps
  the width the user released at.
- The dock-edge handle is inert (and any stored drag is dropped) while a modal dialog is open.
- set_display_width changes iconic_width while iconic and leaves the columns expanded width for
  expanding again; the regression resizes an iconic column and checks the restore.
- Clicking the toolbar side persists the panels snapshot immediately through persist_panels, even
  when the dock is not drawn, while Remember Workspace Changes is on.
- The locked-drag test compares against the pre-drag width instead of only settled equality.

* Toolbar side: defer persistence while moving, translate the side labels

- persist_panels defers while the dock persistence pass sees a native window move/resize (the
  same native_moved signal), so a click during an OS resize cannot save intermediate geometry;
  it requests the usual 100 ms repaint and leaves the dock pass to save when the geometry is
  stable.
- The toolbar-side tooltip/accessible labels now pass both literals directly to tl!, and all 34
  registered catalogs carry translations for Move Toolbar to the Right/Left.

* Toolbar side: keep in-progress dock geometry out of toolbar saves

- native_moved caches its answer for the frame, so the dock pass and persist_panels see the same
  movement result even though only the first call observes the change.
- persist_panels defers while a dock-edge drag is stored, a pointer is down or a native window
  moved; it stores a pending flag and requests the usual 100 ms repaint.
- persist_pending runs in the frame tick (before preferences save) and retries the deferred save
  once geometry is stable, so the toolbar side is not lost even when the dock was not drawn.
- The dock persistence pass also skips while the dock-edge drag start is stored, covering a
  synthetic/accessibility click with no pointer button down.

* Toolbar side: keep a same-width dockWidth request a no-op

The dockWidth rewrite scaled and committed the legacy layout even when the requested width
already matched, so a later `ui.set {dockTabs}` took the customized-workspace path and expanded
the Properties group. Restore the early return for an unchanged displayed width and cover it:
setting the current dock width keeps `app.ui.docking` None.

Fixes the `a_new_adjustment_layer_brings_properties_to_the_front` failure on CI.

* Toolbar side: recheck native bounds, expire stale dock-edge drags

- native_moved keeps movement already seen in the frame but compares the bounds again on every
  call, so a later dock save is not fooled by the early persist_pending check caching false
  before the docking UI updates a floating window.
- dock_panel records the frame it rendered; persist_pending drops a stored dock-edge drag start
  once the panel stopped rendering (Shift+Tab hides the dock mid-drag), so deferred saves resume.
- The same-width dockWidth test also selects a tab in a collapsed group and asserts the tab,
  the collapsed state and that no workspace was customized.

* Toolbar side: draw the release width, clear drags when the dock empties

- The release frame draws the panel at the final pointer width just stored, instead of the width
  read before the drag ended, so the panel and toolbar match the saved dock width.
- The empty-dock path clears a stored dock-edge drag start, so a dock that empties mid-drag cannot
  leave deferred saves blocked.
- Tests: nested DockOnlyGuard (flag found set stays set), and a queued `ui.set {dockWidth}`
  through the rendered dock pass checking a two-column dock totals 420 points.

* Toolbar side: cancel the dock-edge drag when resizing is unavailable

- A disabled parent UI (screen picking) never delivers drag_stopped, so dock_panel now clears a
  stored dock-edge drag whenever resizing is unavailable: locked, modal, empty dock or disabled
  UI. The same condition makes the handle hover-only, so it cannot start a new drag while
  disabled.
- Regression: start a resize, disable the UI, release, re-enable and move the pointer; the drag
  is cleared, the dock does not jump and persistence can resume.

* Toolbar side: bound the dock-edge layout and enforce column minima

- The dock-edge panel leaves the toolbar and the 200-point canvas minimum room, so a narrow
  window shrinks the dock instead of the canvas.
- The empty dock runs the docking pass in a child ui that does not consume the parent layout, so
  the toolbar and canvas keep their space.
- Column scaling keeps the 220-point panel minimum: minima are allocated first, the rest is shared
  proportionally, and requests below the sum of minima stop there.
- The dock metrics (empty, iconic-only, width) come from one layout snapshot per frame.
- Removed dead empty-dock branches, narrowed the content-edge interceptor to one point either
  side, corrected the toolbar target comment, used the drag press origin, and documented the
  Side::End assumption.
- Tests: narrow-window canvas minimum, empty dock composed with toolbar and canvas, minimum-aware
  two-column widths, and the empty compose regression.

* Toolbar side: bound the dock-edge layout, enforce panel minima, match iconic widths

- The dock-edge panel leaves the toolbar and the 200-point canvas minimum room, so a narrow
  window shrinks the dock instead of the canvas.
- The empty dock runs the docking pass in a child ui that does not consume the parent layout, so
  the toolbar and canvas keep their space.
- Column scaling allocates each column's 220-point panel minimum first and shares the rest
  proportionally; requests below the sum of minima stop there.
- Iconic columns use craft-ui's renderer rule: iconic_width.unwrap_or(36).clamp(36, 240), including
  the real collapse case that leaves iconic_width unset.
- Dock metrics come from one layout snapshot per frame; dead empty-dock branches are gone; the
  content-edge interceptor is one point either side; the toolbar target comment is corrected and
  the Side::End assumption documented.
- Tests: narrow-window canvas minimum, empty dock composed with toolbar/canvas, minimum-aware
  two-column widths, collapse via Action::SetIconic leaving iconic_width unset, and the 36..240
  clamp. The press-origin change was reverted: Sense::drag starts immediately.

* Toolbar side: distribute refused width, draw actual widths, invalidate stale drags

- scale_columns moves the total toward the request in passes, freezing columns that hit a bound
  (iconic 36..240, expanded minimum 220), so width refused by the iconic cap reaches columns that
  can still grow.
- set_end_dock_total returns the width actually stored; the dock panel uses that width during the
  drag, on release and after, instead of forcing the requested or minimum width.
- The custom drag stores docking_generation; a workspace replacement clears the stale gesture
  before any width mutation.
- Clamp bounds are normalized against the available maximum (including iconic columns) and
  resizing is disabled when the window cannot fit the column minima, so narrow layouts cannot
  panic.
- Tests: iconic-cap surplus redistribution, actual-vs-requested panel geometry during and after a
  drag, workspace replacement during a drag, and a 300pt window resize without panic.

* Fix dock-edge resize bounds and cancellation

* Preserve explicit dock width across legacy migration

* Document dock code, dedupe widths, harden toolbar_side load

- Add doc comments to the functions and tests this PR touches (Docstring Coverage).
- Share the toolbar width (toolbar_width/toolbar_columns), the rail widths and
  CANVAS_MIN_WIDTH instead of repeating the values; no values change.
- Document dockWidth as the requested model width, which a narrow window may draw
  narrower in either layout, and test model 520 / drawn <= 364 / canvas >= 200.
- Document that a dockWidth change is shared equally among columns within their
  minimums and iconic caps; tie ICONIC_WIDTH/ICONIC_MAX_WIDTH to craft-ui's
  DockStyle defaults (36/240) in a test.
- Replace the DOCK_ONLY thread-local and its guard with show_layout(dock_only).
- Move the test-only probe writes behind panel_docking::record, with no-op stubs
  outside tests.
- Read an unknown or malformed saved toolbar_side as Start instead of dropping
  the whole panel state; ui.set still refuses it.

* Test visible dock chrome in narrow windows; name panel body minimum

- The narrow-window dockWidth test now checks the docking controls craft-ui drew:
  hit_rects are full widget rects (before clipping), so the visible part of each,
  intersected with the dock, must stay inside the dock and off the canvas and
  the toolbox, with at least one control visible.
- Name the 200-point guard for drawing a panel's body PANEL_BODY_MIN_WIDTH,
  separate from CANVAS_MIN_WIDTH; no value change.

* test(ui-egui): assert dock controls are clipped at the panel edge

The narrow-window test intersected each hit rect with the dock and then asserted the
intersection lies inside the dock, which always holds. Check each dock control's clipped
interact_rect against the dock instead, and add a squeezed-column test where a control's
full rect runs past the panel edge, so the clip is actually exercised.

* ci: retrigger pull_request workflows after resolving the merge conflict

No changes. GitHub did not queue Actions for 3652ede, the push that
resolved the conflict with main.

* test(ui-egui): assert saves resume after a disabled UI cancels a dock-edge drag

The test's doc says saves must resume once a disabled UI drops the held drag, but nothing
checked it. Assert that the held drag is not saved, that its width is saved as soon as the
drag is cancelled (while the UI is still disabled), and that a width change made after
re-enabling reaches the saved layout too.

* test(ui-egui): cancel a dock-edge drag on a window shrink and check the dockWidth column split

constrained_width_does_not_panic_while_resizing now starts the drag in a wide window and
shrinks the window below the column minima while it is held: the drag must be cancelled and
the dock must stay within the window. Both ui_set_dock_width tests now check how the change is
split between the columns, not only the total.

* test(ui-egui): tighten dock tests per CodeRabbit review 5824274070

- disabling_the_ui_cancels_a_dock_edge_drag: assert the drag is cancelled on the first disabled
  frame, before the pointer is released.
- all_iconic_totals_stop_at_both_bounds_without_losing_expanded_widths: check the stored column
  widths sum to the expected total after each request.
- explicit_width_request_preserves_queue_and_native_state_until_invalid_workspace_recovery: add
  a changed-width (520) case next to the no-op one.
- the_toolbar_side_toggle_works_in_every_interface_theme: with the toolbar on the right, a grouped
  tool's flyout opens to the left of its button.

* test(ui-egui): assert stored widths and native rect per CodeRabbit review 5824824938

- constrained_width_does_not_panic_while_resizing: the stored column widths captured before the
  window shrinks are unchanged after the cancel and after the release.
- explicit_width_request_preserves_queue_and_native_state_until_invalid_workspace_recovery: the
  native state keeps the exact rectangle the test saved until recovery, not just some state.

* Keep the toolbar side when a workspace saved none

A workspace saved before the toolbar could move has no panels.toolbar_side,
so applying it reset a right-hand toolbar to Start. Keep the current side
when the saved value is missing or unreadable; a valid saved side still wins.

* Cover a saved start side and the other panel flags on workspace apply

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:51:29 +01:00
2026-11-09 15:51:29 +01:00
2026-11-09 15:51:29 +01:00
2026-11-09 15:51:29 +01:00
…

ArtCraft

PhotoCraft icon: an engraved nine-tailed fox on a blue tile

PhotoCraft

Image editing; an open-source, clean-room reimplementation of Adobe Photoshop, rebuilt in pure Rust.
Layers, masks, adjustment layers, layer styles, type, vectors, brushes and real PSD files,
in a native app written entirely in Rust. Open source, offline, and yours.

100% Rust macOS · Windows · Linux · FreeBSD · Web License: MIT OR Apache-2.0 Status: early alpha
Scanned by SonarCloud Reviewed by CodeRabbit

Join the ArtCraft community on Discord

PhotoCraft on getartcraft.com · ArtCraft · All Crafting Apps


PhotoCraft editing Hokusai's The Great Wave: a caption card with a drop shadow, Title and Credit type layers, Vibrance and Curves adjustment layers, and the Curves editor drawn over the image's histogram
A caption card with a drop shadow, live type, and Vibrance and Curves adjustment layers, with the Curves editor open.
The Great Wave off Kanagawa, Katsushika Hokusai, c. 1831

Note

ArtCraft is a community of artists from all walks of life. Digital, generative, music, games — if you make things, you're one of us. Come say hi on Discord.

Features · Everything in the box · PSD · Agents · Under the hood · Get started · Downloads · Crafting Apps · Discord


🎛️ Familiar by design

The menus, shortcuts, panels and tools are where your hands expect them, from ⌘J to ⇧⌘D. If you know Photoshop, you already know PhotoCraft.

⚡ Native and fast

A GPU compositor on wgpu (Metal, Vulkan, DX12, WebGPU), copy-on-write tiles and multithreaded filters. No Electron, no web view, no waiting.

🗂️ Real PSD files

Open, edit and save layered Photoshop documents. Re-saving keeps the render of 307 of the 309 psd-tools test files.

🤖 Agent-ready

Every action is a command, so you can drive the same engine from the UI, the CLI, a JSON control channel or an MCP server.

Features

Every screenshot here is the real app at work on public-domain art, rendered offscreen through its control channel.

Monet's Impression, Sunrise with Levels and Vibrance adjustment layers; the Levels editor and the Histogram panel with mean, standard deviation and median are open on the right
Levels and Vibrance adjustment layers, with the live Histogram panel.
Impression, Sunrise, Claude Monet, 1872

Edit without regret

Adjustment layers keep every edit live. Stack Levels, Curves, Vibrance, Hue/Saturation and a dozen more, mask them to an area, reorder them, or turn them off, and your original pixels never change.

16 adjustment layers that also apply directly to pixels, including Curves with per-channel editing, Levels with a live histogram, Black & White, Channel Mixer, Gradient Map, Photo Filter, Selective Color and Color Lookup (.cube, .3dl, .look). Plus Shadows/Highlights, Replace Color, Match Color, HDR Toning, Desaturate and Equalize.
The Layer Style dialog editing an Outer Glow on the EARTHRISE type layer, with Stroke also enabled, over the Apollo 8 Earthrise photograph
Outer Glow and Stroke on a live type layer, in the Layer Style dialog.
Earthrise, William Anders / NASA, 1968

Styles that sell the shot

Drop Shadow, Inner Shadow, Outer and Inner Glow, Bevel & Emboss, Satin, Stroke, and Color, Gradient and Pattern Overlay, live on any layer, including type. Patterns come from a library (built-ins, Edit › Define Pattern, .pat import/export) and PSD Patt blocks.

Copy and paste styles between layers, hide all effects at once, and open styles straight from your PSDs, rendered to match Photoshop.
Vermeer's Girl with a Pearl Earring: an elliptical selection around the face, and a Hue/Saturation layer with an elliptical mask that turns everything outside the face gray
An elliptical selection becomes the mask of a Hue/Saturation layer, so only the face keeps its color.
Girl with a Pearl Earring, Johannes Vermeer, c. 1665

Selections that understand your image

Marquees, lassos and the Magic Wand for precision; Quick Selection, Object Selection and Select Subject when you want the computer to do the tracing; Select and Mask to refine hair-fine edges.

Feather, expand, contract, smooth, grow, reselect. Turn any selection into a layer mask, a vector path or a shape. Smart selection runs on your machine, with no cloud and no account.
Bierstadt's Among the Sierra Nevada with the headline SIERRA NEVADA being edited on the canvas in Georgia, a byline, and a paragraph caption; the Character and Paragraph controls are open
A headline edited in place, with a byline and a paragraph of body text.
Among the Sierra Nevada, California, Albert Bierstadt, 1868

Type that sets beautifully

Point and paragraph text, edited right on the canvas, with full Character and Paragraph controls: font, weight, size, leading, tracking, alignment and colour. The full Type Color Picker also supports screen sampling with a pixel loupe.

Type layers stay editable, take layer styles, and round-trip through PSD.
A lotus badge built from shape layers (Lotus, Water, Sun, Badge and Dotted Ring) over Monet's Water Lilies, with the lotus path's anchor points selected
A badge made of shape layers: a gradient-filled lotus, a star and a dotted ring.
Water Lilies, Claude Monet, 1906

Pixel-perfect vectors

Rectangle, Ellipse, Triangle, Polygon, Line and the Pen, Freeform Pen and Curvature Pen tools, with resolution-independent shape layers, gradient fills, and dashed, aligned strokes.

Combine shapes (unite, subtract, intersect, exclude), keep paths in the Paths panel, use them as vector masks, or stroke and fill them. 116 of 116 shape layers in our PSD corpus match Photoshop's pixels.
The Twirl filter dialog at a 320 degree angle, previewing live inside an elliptical selection on Van Gogh's The Starry Night
Twirl previews live on the canvas, only inside the selection.
The Starry Night, Vincent van Gogh, 1889

See it before you commit

Every filter dialog previews live on the canvas, through your selection. Blurs (Gaussian, Box, Motion, Radial, Surface, Smart, Lens, Shape, and the Blur Gallery: Tilt-Shift, Iris, Field, Spin, Path), sharpening, Reduce Noise, distortions (Twirl, Wave, Ripple, Displace, Shear, Zig Zag…), Pixelate, Stylize (Oil Paint, Wind, Extrude…), Render (Clouds, Fibers, Lens Flare, Lighting Effects) and more.

Run filters on a smart object and they stay editable: change, hide, reorder or mask them at any time.

Large-radius blurs use running-sum box passes across all cores: a radius-180 Gaussian on 3.6 MP takes under a second.
Free Transform handles around a rotated copy of Ansel Adams' The Tetons and the Snake River, with the History panel listing Open, Duplicate Layer, Free Transform and other steps
Free Transform on a rotated print, with every step listed in History.
The Tetons and the Snake River, Ansel Adams, 1942

Shape it any way you like

Free Transform with scale, rotate, skew, distort and perspective; exact 90° and 180° rotations and flips; Transform Again. Layers, type, shapes, masks and selections all transform together.

Full history, Toggle Last State and the History Brush mean every step can be undone, even one brush stroke at a time.
The Export As dialog in the light theme over Klimt's The Kiss: JPG at quality 90, scaled to 50 percent, with a preview and an estimated size of about 684K
Export As in the light theme, with a preview and a file-size estimate.
The Kiss, Gustav Klimt, 1907–1908

Ship it anywhere

Export As with format, quality, transparency and scale, plus a preview and an instant file-size estimate. Quick Export to PNG in one click.

Choose a dark Pro theme, the airy Studio themes, or a Classic look.

Everything in the box

🧰 69 tools

Move · Artboard · Rectangular and Elliptical Marquee · Single Row Marquee · Single Column Marquee · Lasso · Polygonal Lasso · Magnetic Lasso · Object Selection · Quick Selection · Magic Wand · Crop · Perspective Crop · Slice · Slice Select · Frame · Eyedropper · Color Sampler · Ruler · Note · Count · Remove · Spot Healing Brush · Healing Brush · Patch · Content-Aware Move · Red Eye · Brush · Pencil · Mixer Brush · Color Replacement · Clone Stamp · Pattern Stamp · History Brush · Art History Brush · Eraser · Background Eraser · Magic Eraser · Gradient · Paint Bucket · Blur · Sharpen · Smudge · Dodge · Burn · Sponge · Pen · Freeform Pen · Curvature Pen · Add Anchor Point · Delete Anchor Point · Convert Point · Horizontal and Vertical Type · Horizontal and Vertical Type Mask · Path Selection · Direct Selection · Rectangle · Ellipse · Triangle · Polygon · Star · Line · Custom Shape · Hand · Rotate View · Zoom

🖌️ A real brush engine

Shape Dynamics, Scattering, Texture, Dual Brush, Color Dynamics, Transfer, Brush Pose, Wet Edges, Build-up and Smoothing (including Pulled String), driven by pen pressure, tilt, rotation and direction. Brush presets, Define Brush from Selection, and deterministic, replayable strokes.

🗃️ Layers, done properly

Groups, clipping masks, pixel and vector masks, fill layers (solid, gradient and pattern), adjustment layers, live smart objects with smart filters and lossless transforms and warps, multi-layer selection with align, distribute and link, alpha channels and Quick Mask, 27 Photoshop blend modes plus six Paint.NET variants, opacity and fill, locks, colour labels, a layer kind filter, merge, flatten, rasterize, Layer via Copy/Cut, Paste Into.

🎨 Any colour, any depth

RGB, Grayscale, CMYK and Lab documents at 8, 16 and 32 bits per channel. Bit depth and colour model are runtime data, so every tool works at every depth.

Real ICC colour management in pure Rust: embedded profiles, Assign and Convert to Profile with all four rendering intents and black point compensation, soft proofing (⌘Y) and Gamut Warning (⇧⌘Y) on the GPU.

🗂️ Formats

PSD and PSB, layered TIFF (Photoshop's layer data in the TIFF, read and written in either byte order), Paint.NET PDN3 (read, with editable layers and all 14 blend modes), OpenRaster (read and write, with layers, groups and blend modes, for Krita, MyPaint and GIMP), GIMP XCF (read, with layers, groups, masks, blend modes, channels and the selection), Krita .kra (read, with paint layers, groups, blend modes and transparency masks), SVG (opens as shape layers, places as a vector Smart Object), plus flat PNG, JPEG, TIFF, WebP (lossy and lossless), GIF, BMP, TGA, ICO, QOI, PNM, OpenEXR, Radiance HDR and FITS (astronomy images: 8/16-bit and 32-bit float, gray or RGB, header kept), and the native .pcraft format. Flat formats retain the depths they support: PNG keeps 8/16-bit samples, TIFF keeps 8/16-bit and 32-bit float samples, and OpenEXR keeps half/float samples; JPEG, WebP and the other 8-bit formats reduce higher-depth samples with an export warning. See the codec capability matrix for each format. HEIC photos from iPhone and Mac are read-only (included in official builds; optional --features heif for source builds). AVIF is write-only with the optional avif build feature; AVIF decoding and JPEG XL are unavailable. Affinity documents (.af, .afdesign, .afphoto, .afpub) open natively, read only, with artboards, layers, vectors, text, images and masks; effects, adjustments and other unsupported parts are listed in a warning, and Affinity export is unavailable.

🪄 The everyday essentials

Auto Tone, Contrast and Color · Equalize · Image and Canvas Size · Crop and Trim · Reveal All · Edit › Fill and Stroke · Copy Merged · Paste in Place · guides, rulers, grid and snapping · Actions record and replay · a command palette (⌘F).

PSD without compromise

PhotoCraft's PSD support is a standalone crate written from Adobe's public specification and tested against a corpus of real-world files.

  • Faithful round trips: opening and re-saving a document renders the same for 307 of the 309 files in the psd-tools test set and 169 of 170 in our mixed ag-psd/psd-tools set (crates/io/tests/it/corpus.rs; fetch the psd-tools set with cargo xtask corpus --psd-tools), and anything we don't model yet (raw blocks, descriptors, extras) is carried over instead of being dropped. A re-saved file is not byte-identical to its source: PhotoCraft rewrites image resources, layer records and the composite. Only the standalone photocraft-psd crate, parsing and writing a file without the document model, reproduces every parseable corpus file byte for byte (crates/psd/tests/it/corpus.rs).
  • Pixels that match: a composite oracle compares our render with Photoshop's own merged image, covering gradient interpolation (Classic, Perceptual and Linear), layer effects, shape strokes, clipping and fill opacity.
  • Large documents: PSB, 16 and 32-bit files, and CMYK and Lab documents open natively.

Built for agents

Every menu item, tool and dialog runs a command from one registry of 500+ commands. The UI, the CLI, the JSON control channel and the MCP server all call the same commands, so anything you can click, a script or an AI agent can do too.

# Headless: open, edit, save
photocraft-cli run wave.psd \
  --cmd filter.sharpen.smartSharpen     --params '{"amount":80}' \
  --cmd layer.newAdjustmentLayer.curves --params '{"points":[[0,0],[64,48],[192,212],[255,255]]}' \
  --out wave-final.png

# Apply one action list to a folder of images
photocraft-cli batch --actions grade.json --in ./raw --out ./graded

# Every subcommand explains itself
photocraft-cli batch --help

# Let an agent drive it over MCP (headless, or bridged to the running app)
photocraft-cli mcp

The desktop app also offers an authenticated, loopback-only control channel (photocraft --control) for inspecting UI state, driving tools with pointer events, and taking offscreen screenshots. Every image in this README was rendered that way. See docs/control-protocol.md.

Under the hood

  • Engine first: a pure-data document model and a command engine, with a thin egui UI on top. Layering is enforced at build time across 24 crates.
  • Two compositors: a CPU compositor serves as the reference oracle, and a wgpu compositor puts the canvas on the GPU. They are tested against each other.
  • Copy-on-write tiles: 256² sparse tiles make undo cheap and huge canvases light, and effect maps are cached per layer state.
  • Runs in the browser: the whole engine and UI compile to WebAssembly.
  • Clean-room: implemented from public specs and observed behaviour only. No proprietary code, shaders or assets.
  • Tested: more than 1,700 tests, including PSD round trips, synthetic generators, compositor oracles and multi-depth checks.

Get started

git clone https://github.com/storytold/photocraft
cd photocraft
cargo run --release -p photocraft -- image.psd   # the desktop app
cargo test --workspace                           # the test suite

Japanese fonts for the UI and Type tool come from craft-fonts, an optional build input (desktop release builds always include it). Without it PhotoCraft uses your system's CJK fonts:

git clone https://github.com/storytold/craft-fonts ../craft-fonts
CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run --release -p photocraft

New contributors and AI agents: start with AGENTS.md, then docs/.

Installers for macOS, Windows, Linux, FreeBSD and the web are attached to each GitHub release. On Linux you can pick an AppImage, a .deb, an .rpm, a tarball or a Flatpak bundle. The bundle needs the freedesktop runtime from Flathub, which flatpak offers to install along with it:

flatpak install --user photocraft-<version>-linux-x86_64.flatpak   # or -linux-aarch64
flatpak run ai.storyteller.photocraft

The Flatpak prints through the host's CUPS socket (File › Print talks IPP to it directly, no lp needed); without CUPS, or without a printer, File › Print opens the system print dialog through the print portal. See printing.

The AppImage needs no install: the first run registers its launcher icon and menu entry in ~/.local/share so the dock shows PhotoCraft's icon on Wayland. Set PHOTOCRAFT_NO_DESKTOP_INTEGRATION=1 to skip that, and see docs/releasing.md › Linux to undo it.

To use the bundled CLI, run ./photocraft-<version>-linux-x86_64.AppImage --cli --help, or --cli mcp to start the MCP server on stdio. CLI launches skip desktop integration. A symlink named photocraft-cli also selects the CLI; see docs/development.md.

On a Wayland session, files dropped on the window don't open yet: winit 0.30, the windowing library under egui, has no Wayland drag and drop (#386). Use File › Open, or copy the image in your file manager and paste it with Ctrl+V. To drag and drop, start PhotoCraft under XWayland: WAYLAND_DISPLAY= photocraft, WAYLAND_DISPLAY= ./photocraft-<version>-linux-x86_64.AppImage, or flatpak run --nosocket=wayland --socket=x11 ai.storyteller.photocraft. On Wayland, PhotoCraft shows the command for your install in a notice until you dismiss it. Preferences › Performance › Linux display server = X11 does the same on every launch. With a fractional display scale (125%, 150%, 175%) an XWayland window is drawn at a lower resolution and enlarged by the compositor, so the interface is blurred there; native Wayland stays sharp (#1072).

On macOS, the command-line tool comes as photocraft-cli-<version>-macos-universal.zip. The binary is signed with the same Developer ID as the app and notarized by Apple. A bare binary can't carry a stapled notarization ticket the way the DMG does, so the first time you run it macOS checks the notarization online. You can confirm it yourself:

ditto -x -k photocraft-cli-<version>-macos-universal.zip .
spctl --assess --type install -vv photocraft-cli-<version>-macos-universal/photocraft-cli
# ... accepted, source=Notarized Developer ID

On FreeBSD 14 (x86_64), the release has a tarball laid out like /usr/local. Install the runtime libraries, then unpack it there:

pkg install libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib
tar -xzf photocraft-<version>-freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local
photocraft

Maintainers: docs/releasing.md explains how releases are built, signed and published.

Important

Status: PhotoCraft is in early alpha, and we want to be straight about where it stands: much of Photoshop's feature surface exists in some form, but it is not yet a Photoshop replacement for daily professional work. The biggest gaps are AI/generative features, 15 missing tools, depth in typography and pro workflows, and plug-in compatibility. Every Photoshop menu item is wired to a command (docs/parity-checklist.md), but that measures wiring, not behaviour. The honest, dimension-by-dimension picture and where we're going next are in the roadmap and parity assessment. Expect rough edges, and please file issues (include your OS, document size, layer count and a screenshot). You can also tell us what broke on Discord.

Community packages

These installers are maintained by community members, not by the PhotoCraft team.

On Gentoo, the ::snakebyte overlay packages the Linux release as media-gfx/photocraft-bin:

eselect repository add snakebyte git https://github.com/switch87/snakebyte-overlay.git
emaint sync -r snakebyte
echo 'media-gfx/photocraft-bin ~amd64' >> /etc/portage/package.accept_keywords/photocraft
emerge --ask media-gfx/photocraft-bin

Documentation

Developer, architecture, automation, format, and security documentation is maintained in the PhotoCraft documentation book.

To self-host the browser version with Docker, see Docker web hosting.

Security

Security architecture, threat modeling, parser hardening, fuzzing, and vulnerability reporting are covered in the security documentation and the repository security policy.

Test corpora

PhotoCraft is tested against real files: our own Photoshop-authored oracle PSDs in photocraft-corpus plus the psd-tools, ag-psd and PngSuite sets, pinned and sha256-verified. Fetch them with cargo xtask corpus --all and run the tests with cargo xtask test-corpus (details in docs/development.md).

Downloads

New to PhotoCraft? Download it from the PhotoCraft page on getartcraft.com. That's the easiest way to install it.

Want a specific build or format? On GitHub, the latest release has every build listed below, and all releases have earlier versions and their notes. <ver> in the file names is the version number, and SHA256SUMS.txt lists a checksum for every file.

Windows

Build Installer Portable
x64 (64-bit Intel/AMD) photocraft-<ver>-windows-x64.msi photocraft-<ver>-windows-x64-portable.zip
arm64 (Snapdragon and other ARM PCs) photocraft-<ver>-windows-arm64.msi photocraft-<ver>-windows-arm64-portable.zip
x86 (32-bit) photocraft-<ver>-windows-x86.msi photocraft-<ver>-windows-x86-portable.zip

Installers and executables are code-signed.

macOS

Build File Notes
App, universal (Apple silicon + Intel) photocraft-<ver>-macos-universal.dmg Signed and notarized
Command-line tool, universal photocraft-cli-<ver>-macos-universal.zip Signed and notarized

Linux

Format x86_64 aarch64 (ARM64) Notes
AppImage photocraft-<ver>-linux-x86_64.AppImage photocraft-<ver>-linux-aarch64.AppImage Runs anywhere; updates itself with AppImageUpdate (.zsync files)
Flatpak photocraft-<ver>-linux-x86_64.flatpak photocraft-<ver>-linux-aarch64.flatpak Sandboxed; flatpak install --user <file>
Debian/Ubuntu photocraft-<ver>-linux-x86_64.deb photocraft-<ver>-linux-aarch64.deb
Fedora/RHEL/openSUSE photocraft-<ver>-linux-x86_64.rpm photocraft-<ver>-linux-aarch64.rpm
Tarball photocraft-<ver>-linux-x86_64.tar.gz photocraft-<ver>-linux-aarch64.tar.gz Unpack anywhere

RISC-V (riscv64): photocraft-<ver>-linux-riscv64.tar.gz only, built against Ubuntu 24.04 (needs glibc 2.39+).

FreeBSD

Build File
x86_64 photocraft-<ver>-freebsd-x86_64.tar.gz

Web (WebAssembly)

Build File Notes
Static site photocraft-web-<ver>.zip Runs in a modern browser; host it on any static server

The Crafting Apps

PhotoCraft is one of the Crafting Apps: free, open-source creative tools from the ArtCraft team, each written from scratch in Rust and each able to stand on its own.

App What it's for Code Learn more
PhotoCraft Image editing: layers, masks, type and real PSD files GitHub Website
VectorCraft Vector illustration GitHub Website
FilmCraft Video editing, color and sound GitHub Website
LightCraft Photo library and raw development GitHub Website
PdfCraft Reading, organizing and protecting PDFs GitHub Website
EffectCraft Motion graphics and visual effects GitHub Website
DesignCraft Page layout and publishing GitHub Website
FigCraft Interface design and prototyping GitHub Website
WordCraft Word processing and documents GitHub Website
SoundCraft Audio recording and editing GitHub Website
DeckCraft Presentations and slide decks GitHub Website
CADCraft Computer-aided design GitHub Website
GridCraft Spreadsheets and data GitHub Website
TextureCraft 3D texture painting GitHub Website
Craft Anything See how any software works, then build your own GitHub Website
SolidCraft Parametric 3D CAD by Tangency Labs (community app) GitHub

And ArtCraft itself, our AI image and video studio for artists who want real control.

The Crafting Apps share the same conventions: clean-room and pure Rust, native on macOS, Windows and Linux, in the browser via WebAssembly, and fully drivable by agents.


Join the ArtCraft community on Discord

Come make things with us

Our Discord is where artists of every kind hang out: people who paint, shoot, draw, cut film, set type, and people still figuring out what they like to make. Share what you're working on, ask for help, tell us what's broken, or tell us what you wish these tools could do. Whatever your medium and however long you've been at it, you're welcome here.

discord.gg/artcraft · getartcraft.com · The Crafting Apps · PhotoCraft


License and credits

PhotoCraft is dual-licensed under MIT or Apache-2.0, at your option. Copyright (c) 2026 ArtCraft Team and the PhotoCraft contributors. Required notices are in NOTICE.

Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed with its author, source and license in ATTRIBUTION.md.

Every artwork shown is in the public domain (Wikimedia Commons, NASA, U.S. National Archives); sources are listed in docs/images/SOURCES.md.

The ArtCraft name, wordmark and logos in docs/brand/ are trademarks of the ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as part of this repository and PhotoCraft, under docs/brand/LICENSE-brand.txt. Forks and modified versions must remove them.

Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. PhotoCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Adobe Inc.; these names are used only to describe the workflows it is compatible with.

ArtCraft
Made by the ArtCraft team and community.

Star history

Star History Chart

Languages
Rust 99.5%
WGSL 0.2%
Shell 0.2%
PowerShell 0.1%