#9970 reopened Image > Adjustments > Photo Filter... at the filter, colour,
density and Preserve Luminosity of its last OK. Measured in the reference
application: after OK at 60, 70 and 74 %, with a preset or with Color and a
custom colour, it reopens every time at Warming Filter (85), the orange
swatch and 25 %; only Preserve Luminosity carries over (off after an OK with
it off, on after an OK with it on). Opening now starts from the neutral
settings and takes just that one value from the last OK; Alt still reopens
at all the last settings (#8900). Tests and the ui-parity row follow the
measured behaviour.
Co-Authored-By: ljkx <177671075+ljkx@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#10797 landed the Gradient Map drag-off and a shared off_strip() on main.
Conflicts resolved to main's Gradient Map code and main's off_strip(): a
stop comes off the bar dragged up or down, as measured in the reference
application, so the PR's directional stop_dragged_off() is dropped and
Layer Style's Gradient Overlay uses off_strip() too.
Co-Authored-By: ljkx <ljkx@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main added preferences since the PR's last merge, so the toolColors field now
makes it 10 of 195 settings that do nothing (was 10 of 194 on main); the
ui-parity preference row follows the scorecard.
Co-Authored-By: GyoJoong Kum <david32123kr@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`docs/ui-parity.md`'s status line conflicted: the branch and main each
prepended a clause at the head of the change list, and the branch also
carried the newer `Last updated` date. The merged line keeps main's
toolbar tool-memory clause (#3569) word for word after our Reset
Transform clause (#11662), with one `Change:` header and the later date.
Review finding on this PR (#11675, discussion_r4499346688): with the warp now cleared as part of
the reset, a smart object whose stack holds a visible filter this build cannot apply took the
Free Transform path's forgiving branch - `refresh_layer` returns false for that stack,
`refresh_text` drops it on the floor, and `restore_unrendered` put the OLD cache back warped by
the relative map. The step recorded a placement at 100 % with no warp whose pixels still showed
the warp.
The step now insists on the guarded refresh the other smart-object edits use:
- `unapplied_filters` is checked before anything changes, so an unsupported Photoshop filter kept
verbatim from a PSD names itself in the error and the document is never touched.
- `refresh_or_fail(doc, id)?` runs at the reset boundary, right after `transform_by_map` and
before the exact map is pinned. It is a cache-warm call in the normal case (same document state
as the render `transform_by_map` just did), and it refuses an unreadable source or a render that
fails outright. Because `Session::edit` runs its closure on a copy and returns early on `Err`,
a refused reset leaves the document, the placement, the warp and the history list exactly as
they were - the fallback pixels are never accepted.
Regression test `reset_transform_refuses_a_stack_it_cannot_re_render` (red before this commit:
the reset returned Ok) asserts the error names the filter, that transform, warp and filter stack
are unchanged, that no history state was added, and that the same object resets normally once the
unsupported filter is off the stack. The four existing reset tests still pass, so the success
path and its warp clearing are unchanged.
No new dependency; photocraft_io's UNSUPPORTED_FILTER was already re-exported by this module.
The ui-parity row, its revision entry and the status line said the Warp survives Reset Transform.
It does not: measured against the reference application 2026-11-06 (report in #11675), the warp is
cleared with the rest of the transform, and the object centres on the average of the placement's
four corners. A 2026-11-07 revision row records the correction; the 2026-11-06 entry stays as the
history of what landed when.
cargo xtask parity and scorecard re-run: unchanged (661 of 661 live), so nothing to regenerate.
cargo xtask parity and cargo xtask scorecard regenerated: 661 of 661 menu items
live, Layer 162 of 162. A ui-parity row for the command and the Layers panel
row-menu item, with the revision entry and the status line (#11662).
* Channels: the panel menu has the channel commands, New Spot Channel among them (#11451)
The Channels panel's menu had no channel items, so New Spot Channel... and
the rest could only be reached from a row's right-click menu. It now lists
New Channel..., Duplicate Channel, Delete Channel, New Spot Channel...,
Merge Spot Channel, Channel Options..., Split Channels and Merge
Channels..., each acting on the targeted channel and greyed where it can't:
Duplicate takes a single colour, alpha or spot channel, the composite of a
one-channel image, the Quick Mask or a targeted layer mask. Channel
Options... in Quick Mask mode now opens the targeted alpha or spot
channel's options, not Quick Mask Options. The panel menus run their
commands through the menu bar's enabled rules, so the Layers panel menu
greys its dialog items the same way too. The labels are in every catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Channels: New Channel... and Merge Channels... ask first, from the panel and row menus (#11451)
The panel menu's Merge Channels... ran channel.merge with no parameters, and
the engine merged the first grayscale documents into RGB and closed them with
no choice; New Channel... added a channel straight away. Both now open a
dialog when called without parameters, as New Spot Channel... and Channel
Options... already did: New Channel... reuses the Channel Options dialog
(name, colour indicates, colour and opacity), and a new Merge Channels
dialog picks the mode and the open grayscale document for each channel. The
channel rows' menu had the same two items and goes through the same path.
With parameters (scripts, the New button) both still run at once.
The Merge Spot Channel test now checks pixels, so a delete couldn't pass it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Channels: Merge Channels waits for three grayscale documents and the dialog offers only modes they fill (#11451)
With two grayscale documents open, Merge Channels... was enabled and its
dialog repeated a document for the third channel; OK then failed in the
engine and the dialog closed. The engine's rule now needs three documents
of one size (no mode merges fewer), so the menus grey the item below that.
The dialog offers only the modes the open documents can fill (RGB and Lab
from three, CMYK from four), greys OK with the reason while the picks
repeat, keeps itself open when OK is refused, and comes back with the
engine's message if the merge itself fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Channels: the Merge Channels dialog keeps its picks by document id (#11451)
The dialog kept the picked source documents by tab index, and background
work can still run while it is open, so a document moved or closed in the
meantime could leave another one at a picked index; OK would then merge,
and close, the wrong document. The picks are now document ids, resolved to
the current tabs when OK is pressed: a closed pick, or one that is no
longer a grayscale document of the same size, is refused with the reason
and the dialog stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Image > Adjustments > Selective Color used the generic schema form: one
set of sliders shared by all nine colours, with OK applying them only to
the colour selected at that moment. Switching colour looked like the
values had spread, and edits to other colours were lost.
Make it a dedicated adjustment kind with one editor shared by the dialog
and the Properties panel. Each colour now keeps its own row, and OK and
Preview apply all nine. The older single-colour params from actions and
smart filters fold into the rows.
* Toolbar: Alt/Option-click cycles the flyout tool slot.
Matches Photoshop: plain click keeps the shown tool; Alt-click advances through the slot without opening the menu (including tools Shift+letter skips).
* Toolbar: don't fit-on-screen on rapid Alt-clicks through Hand.
Alt-click only cycles the flyout slot; exclude Alt from the Hand/Zoom double-click handlers and cover inactive-slot cycling.
* Toolbar: ignore Hand/Zoom double-click after an Alt-click.
egui pairs a plain click with a prior Alt-click as a double-click; remember the previous click's Alt so fit-on-screen and actual-pixels only run for two plain clicks.
* Toolbar: read click modifiers from the slot's pointer release.
Match DesignCraft/EffectCraft: bind release mods to the tool rect, treat any modifier as blocking Hand/Zoom double-click, and harden the Alt-cycle test by clearing i.modifiers before release.
* Toolbar: cover 1/2/3-tool Alt-click cycles and hidden-tool slots.
Addresses ReviewerA on #10301: next_slot_tool wrap cases for short flyouts, and visible_sections so Edit › Toolbar hidden tools shrink the Alt-click cycle.
* style(ui): rustfmt after rebase onto main
* Toolbar: next_slot_tool never panics on an empty slot
Index with checked arithmetic and get() (house rule: no panics on
input-derived indices); an empty slot returns the current tool. Test
added.
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Resolve the parity-doc conflict: keep main's status line and entries, re-add this PR's entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the parity-doc conflict: keep main's status line and entries, re-add this PR's entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): Overscroll off keeps an image smaller than the window centred (#10488)
With Tools › Overscroll off, an image smaller than the window stayed
wherever zooming around the pointer left it, inside the window, so
zooming out drifted it off centre. #1718 measured that on Photoshop 25.4,
but two reports of Photoshop itself (#10488, #4799) say it stays centred
with Overscroll off. The owner chose the centred behaviour:
scrollbars::clamp_axis now centres an axis on which the image is no
larger than the view; a larger image still can't leave its edges, and
Overscroll on keeps the free view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ui): Overscroll off centres a fitting rotated image (#10488)
CodeRabbit on #11085: the rotated clamp path was only tested from an
already-centred view. A 600 px square at 45 degrees, started off centre
in a 900-point view, now ends with its screen bounds centred.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* style(ui): keep the rotated centring test within rustfmt's width
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Edit › Transform › Distort ignored its mid-edge handles: mode_allows
rejected Hit::Edge in Distort, so a press there armed no gesture. As in
Photoshop, a Distort side handle now moves its edge freely: both of that
edge's corners move by the drag offset and the opposite edge stays put,
stopping before the box folds flat (keep_convex), as a Distort corner
does. Like the corner, it ignores Shift and Alt. The reference point
keeps its place in the box. Preview and commit share the session quad,
and the commit is one history step.
Perspective mode's side handles now skew along their edge, as in
Photoshop, by reusing the Skew-mode edge arm. Before, they scaled.
The handle pointer follows: the distort pointer on Distort side handles
and the skew pointer on Perspective side handles. Free Transform's plain
side handles (scale) and its Cmd-drag (free edge move) are unchanged.
Tests cover each edge's exact corner offsets, modifier independence, the
collapse stop, a real pointer drag committed in one step with the pixels
inside the shown quad, and Perspective edge slides. docs/ui-parity.md is
updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): Ctrl+Alt + wheel zooms the canvas like Alt + wheel (#10488)
Ctrl+Alt+wheel over the canvas did nothing useful because two conditions in
wheel_nav.rs excluded it from the Alt gesture:
let latest_alt = wheel.last().map(|(_, _, m, _)| m.alt && !m.command && !m.ctrl);
and, in the raw-event accumulator, every branch was filtered with
`!m.command && !m.ctrl`. Ctrl+Alt+wheel therefore never set the tracked `alt`
and never contributed a notch to `raw`: with Preferences > General > Zoom with
Scroll Wheel ON it produced nothing at all (raw stayed zero, so the zoom read an
eased ~0), and with it OFF it fell through to classify's `_` arm and merely
scrolled vertically, because egui keeps vertical_scroll_modifier = ALT and does
not fold the delta onto x when both scroll patterns match. The combination is a
dead/incorrect gesture today, so no working behaviour is being replaced.
The new `alt_gesture` helper forgives `command` only when `ctrl` is set as well.
winit mirrors a held Ctrl on Windows (and Linux) into BOTH ctrl and command, so
a bare `!command` test can never match what a Windows reporter typed; macOS
Cmd+Alt (command set, ctrl not) is left out of the zoom gesture (egui 0.37 does not fold it sideways either: with Alt held both scroll patterns match, so its delta stays vertical). `latest_shift` stays strict, so
Ctrl+Alt+Shift+wheel zooms continuously instead of snapping to the ladder, and
the Shift-only and plain branches are untouched, so plain Ctrl+wheel still
scrolls sideways (#635). `legacy_pinch_to_zoom` is untouched and cannot
double-count a fractional Ctrl+Alt pinch step: it matches `modifiers.ctrl`
whatever else is held, but it *replaces* the wheel event with `Event::Zoom`, so
`wheel_events` never sees that step a second time.
Deliberately out of scope: the other half of the report, the centred-zoom /
overscroll complaint. The per-frame view clamp at canvas.rs:1737-1741 runs only
when Preferences > Tools > Overscroll is off (it is on by default), so with the
defaults nothing pulls the view centre back, and what the reporter is seeing is
more likely the deliberate wheel_anchor nearest-image-point clamp
(view_xform.rs:239, #3720) or Zoom Clicked Point to Center. That needs its own
reproduction and issue, and is noted as still open in the dev log.
Tests, fail-first: each_modifier_maps_like_photoshop gained Windows Ctrl+Alt
rows (preference off and on, instant and eased, a fractional pinch step counted
once, and a macOS Cmd+Alt guard row) while keeping its Ctrl-only sideways-scroll
rows; new full-app kittest test
input_tests::ctrl_alt_wheel_zooms_the_canvas_like_alt_wheel zooms five notches
around the pointer and holds the document point under it. Both failed before the
production change and pass after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ui): Ctrl + Alt + Shift + wheel is dead today, not a zoom (#10488)
CodeRabbit 4432102938 asked for an assertion on the Ctrl + Alt + Shift combination instead of
only a comment, and for the reason: the filter rejects the wheel event. Both are right. With ctrl
held, latest_shift (wheel_nav.rs:381) is false because it excludes ctrl and command, so the alt
branch of the filter (wheel_nav.rs:413-422) runs and its "!m.shift" drops the notch out of raw;
shift stays false, so the snap branch never takes it either.
The new row in each_modifier_maps_like_photoshop first asserted what my own comment claimed - a
continuous zoom, with Zoom with Scroll Wheel off and Animated Zoom off - and failed exactly that
way: "Ctrl + Alt + Shift + wheel zooms 10%, at once: [0.0 0.0] 1 snaps 0". Pan zero, zoom 1.0,
snaps 0, and the same numbers with Animated Zoom on, while plain Alt + Shift still snaps. The
gesture is dead, and only the ctrl half of it kills it.
So the three places that asserted a continuous zoom are corrected here instead of left to rot: the
module doc (wheel_nav.rs:8-11), the comment above the filter (wheel_nav.rs:407-413) and the parity
row (docs/ui-parity.md:193). They now state that the gesture is dead, point at the test that pins
it, and name the open question. No behaviour change: the repo has no measured Photoshop evidence
for this combination (the issue names only Ctrl + Alt), so the choice - (a) accept Shift in the alt
branch when Ctrl is held, making it zoom like Alt + wheel, or (b) treat it as exactly Alt + Shift
and snap - goes to a maintainer rather than being invented. Option (b) has a measured cost worth
knowing before anyone picks it: plain Ctrl + Shift + wheel pans sideways today ([40,0], zoom 1.0,
snaps 0, and no test covers it), and under (b) that becomes a no-op with Zoom with Scroll Wheel on.
Gates: cargo test -p photocraft-ui-egui --lib -- wheel = 102 passed, 0 failed (with the promised
assertion first: 101 passed, 1 failed); cargo clippy -p photocraft-ui-egui --all-targets
-- -D warnings clean; cargo fmt -p photocraft-ui-egui -- --check clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ui): pin Ctrl + Alt + Shift + wheel with Zoom with Scroll Wheel on (#10488)
Review 4433950859: both Ctrl + Alt + Shift + wheel assertions ran with Zoom with
Scroll Wheel off, while the test comment, the wheel_nav.rs filter comment and the
ui-parity.md Wheel row stated the no-action result unqualified. Measured, not
copied: a temporary probe test ran one Ctrl + Alt + Shift notch (Line unit,
gesture_with, 60 frames summed) in all four preference combinations and printed
(pan, zoom, snaps) through an assert that was made to fail:
zww=off anim=off ([0.0 0.0], 1.0, 0) | zww=off anim=on ([0.0 0.0], 1.0, 0)
zww=on anim=off ([0.0 40.0], 1.0, 0) | zww=on anim=on ([0.0 40.0], 1.0, 0)
test result: FAILED. 102 passed; 1 failed; 0 ignored; 0 measured; 4526 filtered out; finished in 2.63s
The suggestion's premise holds: the dead-gesture claim is true only with Zoom
with Scroll Wheel off. With the preference on, alt_gesture keeps the notch out of
the branch's raw, but `alt != zoom_with_wheel` is false so the zoom arms are
skipped, and egui's own smoothed scroll delta (which the raw filter never gates)
pans the notch through the ordinary scroll path: one whole notch, 40 points
vertical, no zoom, no snap, with Animated Zoom off AND on (it only spreads the
same 40 points over later frames).
Probe deleted; the qualified version landed in all three artefacts:
- wheel_nav/tests.rs: the two dead assertions now say "with Zoom with Scroll
Wheel off", two new assertions pin the preference-on pan (near [0.0 40.0],
zoom 1.0, snaps 0, both Animated Zoom modes), and the comment and the
"not even a scroll" sentence carry the preference condition and the numbers.
- wheel_nav.rs classification comment: the dead-gesture sentence is qualified
the same way.
- docs/ui-parity.md Wheel row: same qualification plus the measured numbers.
Final gates: cargo test -p photocraft-ui-egui --lib -- wheel ->
test result: ok. 102 passed; 0 failed (also re-run after cargo fmt re-wrapped
one over-long assert, same result); clippy -D warnings clean; fmt --check exit
0; cargo xtask layers: OK, 31 crates, no layering violations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep Cmd+Ctrl+Alt + wheel off the Alt zoom path, pin AltGr (#10488)
alt_gesture now also requires !mac_cmd when forgiving command, so macOS
Cmd+Ctrl+Alt (command, ctrl, mac_cmd, alt) is not the zoom gesture. Tests pin
the macOS and Windows/Linux modifier shapes and that AltGr reaches egui as
neither ctrl nor alt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep Ctrl+Alt+Shift + wheel as before, correct the AltGr note, pin the Alt key fallback (#10488)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- CLI batch: recursive inputs keep the engine's tree order for {serial};
--format picks the encoder whatever extension --naming writes; the
summary prints before --error-log is written.
- Batch: a jobs.cancel during a write keeps that file in `files`;
Opened Files refuses a default-named result that replaces another open
document's file; a recorded Save leaves a document changed when Batch
writes nothing for it.
- Recording: folded Move-tool nudges add whole-pixel steps; a null
Check Spelling `action` is recorded as Check Spelling.
- Linked files: a document without links lists none missing; a look
whose links changed while it ran is dropped.
- Save for Web: a null or empty destination opens the picker; a rename
while the picker is open writes the picked file.
- Actions: a Check Spelling step fails instead of replacing a dialog
opened by hand.
- Docs: desktop Batch cancel flow, Check Spelling step shape, filter row
history count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The filter-type dropdown row now reads Kind, Name, Mode and Color, the other five missing; status
line and revision history updated. No percentage changed; the scorecard has no row for it
(`cargo xtask scorecard` leaves docs/scorecard.md as it was).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Gradient Editor/Properties strip and Layer Style's Gradient Overlay
ramp ignored the release frame. When the last pointer move came in the
same frame as the release, the stop stayed in the state the previous
frame left it in. A stop flicked off the bar was kept, and one brought
back onto the bar at release was deleted. Both now apply the same
drag-off rule on that frame, as Gradient Map and the shape strip
already did.
The editor strip also counted opacity stops from its preview, which
already leaves a dragged-off stop out. So while an opacity stop was off
the bar, the preview flipped between deleting and moving it every
frame. It now counts the committed stops, like colour stops.
Tests: a quick release off the bar and back onto it (both editors);
colour stops dragged up and opacity stops dragged down are kept; a
cancelled Gradient Map drag adds no history step. The parity row names
Gradient Map's control "Remove stop".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a colour stop down, or an opacity stop up, by about a marker's
height off the gradient bar now removes it on release in every stop
editor, as in Photoshop's Gradient Editor. At least two stops of that
kind always stay. While the stop is off the bar the ramp and the
preview show it gone, and dragging it back onto the bar cancels.
The Gradient Editor, Properties and shape gradient strips already did
this. Gradient Map had no drag-off at all, and Layer Style's Gradient
Overlay only deleted after about 44 pt and showed nothing until
release. All four now share stop_dragged_off() in gradient_ui.rs
(26 pt past the bar edge, by kind). Gradient Map and Gradient Overlay
take the stop out live and put it back if it returns.
Adds harness tests for drag-off removal, the two-stop minimum,
opacity stops dragged up and drag-back cancel, and updates
docs/ui-parity.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: adjust_dialog.rs body() keeps main's captured target and curve_mode_for_fields, then the once-per-opening Photo Filter mode reset; tests.rs keeps both sides' new tests; docs/ui-parity.md keeps main's status line with the #9879 entry prepended (dated 2026-11-02) and both sides' rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>