* fix(ui): show uppercase RGB hex values in shared color picker
* test(text): expect uppercase hex in text style color picker
* fix(ui): uppercase displayed hex without changing color storage
* test(text): keep color serialization independent of picker display casing
* fix(input): translate Linux precision Ctrl-wheel pinch gestures
* fix(input): decode Wayland high-resolution point pinch events
* docs(input): describe precision pinch support on Windows and Linux
* rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(export): offer ICO in Export As with format-size guidance
* fix(export): use theme warning token for oversized icons
* i18n: translate the Export As ICO size warning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(type): leave keyboard input to focused font search
* test(type): focused font search cannot alter the edited type layer
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(batch): ignore recorded Quick Export and use batch output
* fix(batch): ignore recorded Quick Export and use batch output
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf: reuse sampled composite across channel visibility toggles
* test: borrow full composite for sampled-grid oracle
* test: compare the entire channel view after invalidation
* style: keep channel cache construction readable
* fix: format channel test and share band renderer
* fix: invalidate sampled channel composite after edits and previews
* refactor: keep channel rendering optimization isolated from compositor
* test: cover both partial-damage and full channel redraw paths
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(algo): reject oversized Magic Eraser masks before multiplying dimensions
* Format the oversized Magic Eraser mask test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Use checked mask indexing so incomplete or extreme regions return zero coverage instead of panicking or wrapping.
Validated with the full algo suite, independent native and Wasm regressions, and green PR checks.
* feat(codecs): import XPM3 images with transparency (#3692)
* Correct XPM test expectation for green palette name
* feat(codecs): decode XPM2 plain-text image files
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(codecs): import XPM3 images with transparency (#3692)
* Correct XPM test expectation for green palette name
* feat(codecs): resolve full X11 rgb.txt color names in XPM
* XPM: fix Format::ALL length after DPX, reject '+' in hex colours, fix non-ASCII byte-string test, fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(io): open Krita .kra composites as editable raster documents (#3692)
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): offer label colors from the layer eye icon
* fix(ui): offer label colors from the layer eye icon
* test(ui): cover layer eye color context menu
* style(ui): keep extracted color menu indentation consistent
* fix(selection): keep Color Range inside active selection and reject out-of-bounds samples
* fix(ui): scope Color Range dialog and canvas previews to the active selection
* test(selection): cover Color Range scoping, sampling, invert, Add and undo
* test(ui): ensure Color Range preview and pipette honor existing selection
* test(selection): avoid Debug bound when asserting prepared-query error
* test(selection): validate malformed Color Range sample orders without selection interference
* Color Range: format, and scope the OK undo test's prior selection (#3313)
The existing ok_applies_one_undo_step test picked a prior selection that
excluded the red it then expected Color Range to select; with Color Range
scoped to the active selection it now selects the red inside it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(type): preserve formatting for trailing empty paragraphs
Represent an explicitly styled terminal empty paragraph with a zero-byte paragraph run, keep it through normalization, style only the caret's paragraph, and transfer that style when text is inserted at EOF. Cover line geometry, typing, and PSD text serialization.
* fix(type): retain imported terminal paragraph marker style
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(shapes): preserve transformed outline on unchanged live geometry edit
A nonuniform scale stores an anisotropic outline but live rounded rectangles only store scalar corner radii. Avoid regenerating the path when all live values are unchanged, preserving transformed corners and PSD vector source. Cover no-op edits, real radius edits and undo at all depths.
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(video): retain original footage for restore and comparison (#3059, #3060)
* Clamp original-frame inserts to the original stack; cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(vector): keep scaled live-line weight after editing endpoints (#3063)
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(io): preserve group fill opacity when exporting OpenRaster (#3048)
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(type): use the next visual line at shared wrap boundaries
At soft-wrap and forced-break character offsets, resolve the caret's visual line from the last matching line rather than the preceding one. Make line-edge navigation share that choice and add regression tests for wrapped, forced and paragraph breaks.
* test(type): use actual break characters in caret boundary regressions
* fix(type): preserve the visual line when hit testing a shared wrap offset
Keep caret index preference on the following line, but use pointer geometry for click-hit line identity at an adjacent line's end.
* Update the caret round-trip test for wrap-boundary lines; cargo fmt
The existing round-trip helper expected the first line containing a
shared soft-wrap offset; the caret now reports the following line, where
it is drawn and where hit testing lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(type): skip text in hidden ancestor groups during automatic hit testing
Honor the effective visibility of every ancestor when locating a type layer by point. Explicit layer targeting remains available, with a regression test for nested hidden groups.
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(shortcuts): reject malformed key bindings and format Plus
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(canvas): show Move transform control cursors before first interaction (#2871)
* Fix the Move transform-controls cursor test after merging main
ViewXform gained an aspect field on main, and the rotation-band probe sat
within the top-centre handle's grab radius; probe between handles instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(prefs): persist an optional canvas dot pattern setting (#2264)
* feat(canvas): respect interface canvas dot preference (#2264)
* test(prefs): cover canvas dot toggle persistence and reset (#2264)
* Translate the Show canvas dots preference and regenerate the scorecard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(prefs): search visible settings across sections (#2593)
Filter the Preferences sidebar by section and editable field labels,
switch to the first match, and show matching settings in the editor.
Keep the draft intact while searching and hide Reset Section during filtering.
* Preferences search: translate the field hint and empty state; rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge fixups: pass the new search argument in a main test; search test uses a visible Type setting
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): fit Layers opacity controls within narrow docks (#2309)
* Layers Opacity row: keep the label at the default dock width
Show the label while the blend dropdown keeps 80 px, so the default
Layers dock looks as before and only long translated labels on narrow
docks give way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): expose painting symmetry for all themes (#2659)
Render the existing symmetry control for Brush, Pencil and Eraser
independent of palette. Add a per-theme, per-tool UI visibility test.
* rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(prefs): add 80–95% UI scale presets (#2492)
Offer finer five-percent steps between 75% and 100% for small screens.
Exercise the new values through the live preference and egui scaling test.
* Label the 80-95% UI scale choices as percentages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Translate the 80-95% UI scale labels in every catalog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(layers): keep Background semantics across merge, duplication and canvas size
Preserve destination locks in Merge Down, share the normal Background-copy rule with multi-layer duplication, and convert a Background to a paintable layer when extending its canvas transparently.
Add depth-aware regression coverage and ordinary-layer controls for #2380, #2381 and #2384.
* refactor(layers): copy lower-layer locks without cloning Copy data
* Format background_layer_consistency test
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(layers): scrub opacity and fill by dragging labels (#2361)
* test(layers): compare translated scrub labels as text
* test(layers): query egui label roles for scrub gestures
* cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layer label scrub test: find the text label by its value on the popup arrow's row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(document): keep layer comps valid across document duplication
Remap saved comp and Last Document State references when cloned layers receive new IDs, including historical states and nested groups. Drop obsolete comp metadata in merged-only copies instead of carrying references to removed layers.
Add focused engine regressions for #2382 and related copy workflows.
* style(test): apply rustfmt layout to layer-comp regression tests
* fix(selection): constrain Polygonal Lasso segments with Shift (#2352)
* Polygonal Lasso: make the Shift constraint helper visible to its tests; cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ci(web): build browser release and enforce wasm gate on PRs
* docs(web): describe PR browser build and size checks
* CI web gate: run only when web-relevant files change (main always)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Color Lookup properties' Load 3D LUT… entry opens the platform file picker (filtered to .cube, .3dl and .look, on desktop and web) and embeds the chosen LUT into that adjustment layer, replacing the typed-path field. A cancelled or invalid pick leaves the layer unchanged (#1749).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Levels and Curves composed their master and per-channel records in opposite orders (#975). The
Photoshop-authored corpus decides it: channel first, then master. Levels now does that, bringing
psd-tools levels_rgb.psd from 0.447 to 0.012 max error; Curves (curves_rgb.psd passes) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-clicking a dock tab offers Close (hides just that tab; the last one hides its group) and Close Tab Group, as in Photoshop. Hidden tabs are kept by name in the dock layout, so they survive theme switches and workspace saves, and the Window menu brings a tab back (#1753).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A schema-built filter dialog reopens with the values it was last applied with, as in Photoshop. Only schema-valid numbers, choices and booleans are kept (no paths, document indices or free text), and a failed or cancelled dialog saves nothing (#1757).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Actions panel's Play button falls back to the first action when no row (or a stale row) is selected, as Photoshop does, and a failed playback request now shows in the status bar instead of being dropped (#1951).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dialog's drag handle spans the full title row plus a few pixels of padding around the title, so grabs slightly beside, above or below the title text move the window (#1921). The title keeps its height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Just outside a corner the pointer becomes a curved rotate arrow, and while rotating the angle is shown
next to it. Distort and Warp keep their cursors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proximity Match copied its translated patch within the buffer it was writing, so overlapping offsets
re-copied fresh output and left streaks. It now reads from the pre-heal pixels, with checked bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A literal tab in PSD text shaped as .notdef. Layout now shapes it as a space, keeping byte, style
and caret offsets intact. Tab stops are still to come. Part of #1881.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A release that egui reports as both a drag stop and a click no longer replays the click over a live
stroke (which painted a second full-pressure dab), and the click fallback now uses the pen's pressure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The embedded document built by layer_to_smart now carries the document's patterns, so pattern fills
and pattern effects keep rendering inside the smart object and after Edit Contents. Fixes#1867.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
best_offset clamps the search radius and ring to the image extent and rejects zero or negative radii,
so hostile values no longer overflow i32 loops or search for billions of iterations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A percentage grid now spaces vertical lines by the document width and horizontal lines by its height,
so rectangular canvases get proportional cells, and grid snapping uses the same per-axis steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the save prompt reads Save / Don't Save / Cancel without printed key letters, like the native alert; D, C, S, Return and Escape still answer it. Windows and Linux keep their mnemonics. Shortcut test from #1963 (arthurdeka).
Co-Authored-By: arthurdeka <arthurdeka@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
file.export.colorLookupTables takes scope all|selected or an explicit layers list and bakes only those top-level, visible adjustment layers, always in document stacking order; the dialog defaults to the selection when it is all exportable adjustments. Invalid selections and sizes are errors rather than silent fallbacks; LUT sizes outside 2-129 (the reader's limit) are now refused instead of clamped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > Stroke… from the menus opens a Photoshop-style dialog (Width, Color, Location, Blending:
Mode, Opacity, Preserve Transparency) remembering its last values; called with params it runs
directly. edit.stroke gains mode and preserveTransparency, blending through Fill's compositing
(blend_color_mask), and validates them. The Blending section is shared with Fill
(dialog_blend_ui.rs). Fixes#1463.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a Pen path is being drawn, Undo (its customisable shortcut or Edit > Undo) and Backspace/
Delete remove the last uncommitted anchor instead of undoing an earlier document step, as in
Photoshop; once the draft is empty, Undo works on the document again. Fixes#1466.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui.inspect now has brushPicker: null while closed, {pos, list} while the Brush
Preset picker is open (list is its collapsed/filter/view/renaming state), so
agents can see what a right-click with a painting tool opened. Fixes#1558.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A control-channel menu click could run an edit behind an open dialog (a rotation
under Image Size), which the native menu bar already refuses (#1354). The modal
gate moves to menus::modal_allows and ui.menu.invoke checks it; view navigation
stays live, engine.execute is unaffected. Fixes#1556.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking the gradient swatch in the options bar did nothing; as in Photoshop it now opens a
popup with the Gradients preset browser, so a preset is picked without opening Window › Gradients.
Fixes#1651.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in Photoshop, a secondary click on the Layers footer's Add a layer style and Create new fill or
adjustment layer buttons opens their menus too (a primary click still toggles them), and a small
arrow marks them as menu buttons. Fixes#1579.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #1494: Alt captured at stroke-down records the reversed tool, a
later stroke records the selected one, and the field rebuilt from the strokes
matches after undo and redo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A command dialog's preview is never shown in another tab, and the preview is computed even without a GPU texture, so headless tests can check it (90° Arbitrary rotation, Preview off, document untouched).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A long filter could occupy every worker in Rayon's global pool, and the marching-ants trace on
the UI thread then waited for those workers, so the job's Cancel control froze. While jobs run
the outline is traced serially; a test checks it matches the parallel trace. Fixes#1554.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PHOTOCRAFT_AUTOMATION_*_ROOT variables are read only by the desktop app, so
headless photocraft-cli mcp/serve need the --automation-*-root flags. Say so,
and pass explicit roots in the .mcp.json and claude mcp add examples.
Fixes#1551.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui.set gains rotation, gradientBlendMode and gradientClassic in its signature
and the stale duplicate ui.set line goes; ui.menu.invoke documents params,
ui.pointer tool, app.save an optional path, and serve's doc.save tiffLayers.
Fixes#1550.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>