* Clear the clippy 1.100 lints that fail the gate on main (#13531)
Clippy 1.100.0 flags needless_range_loop in cms synth.rs (the gate
failure on main), algo photo_util.rs and an engine multichannel test,
and, on Windows only, needless_bool in format noreplace.rs, where the
cfg(unix) branch compiles away. Each loop now iterates the slice it
indexed, with the same values in the same order.
* Clear the Linux-only clippy 1.100 needless_bool lints (#13531)
paths_match in ui-egui and is_redirect in the app ended with a
cfg(not(windows)) branch that is just `false`, which clippy 1.100
flags on Linux. Each platform's branch now returns the whole
comparison: the same results, in the same order.
* Clone Stamp and Healing Brush clone from a source set in another open document
Alt-clicking a clone source in one document and painting in another sampled the
painted document at the source point: a clone source slot held a point and a layer
but not the document. Slots now record the document (and its layer) they were set
in, and painting elsewhere samples that document, as in Photoshop.
- Engine: CloneSource gains `document` (serde default, so older slots read as
"the document being painted"). Clone Stamp, Healing Brush, the live stroke, the
Pattern Preview wrap and the brush-tip preview read the source document. Pixels
are converted to the painted document's colour mode and depth like Edit > Paste
(no ICC conversion, same as Paste). Current Layer / Current & Below use the
layer the source was set on (that document's active layer if it was deleted);
All Layers uses its composite.
- A source whose document was closed is an error asking for a new source, and
stores no aligned anchor, instead of silently sampling the wrong document.
- cloneSource.set takes an optional `"document": index` (with `source`);
cloneSource.list and set report each slot's document index.
- UI: the Clone Source panel names the source document (or says it was closed),
the source crosshair isn't drawn over another document, and the brush preview
cache includes the source document's revision.
Tests: engine tests for 8/16/32-bit with and without wrap, colour-mode conversion
(Gray > RGB16, RGB > CMYK), sampled layer, Healing Brush, live stroke vs commit,
preview, closed/invalid documents and cloneSource.set/list; a UI test that
Alt-clicks in one document and paints in another.
* Clone test: the aligned stroke past the smaller source document's edge leaves the destination unchanged
Review follow-up: with "aligned": false the stroke sampled inside the source, so it didn't test the edge its comment named, and nothing was asserted.
* Clone Stamp / Healing Brush: an empty Clone Source slot refuses to paint
clone_params fell back to ui.clone_source / ui.clone_offset when the active
slot was empty. Those coordinates carry no document, so after Alt-clicking in
one document and picking an empty slot in another, the stroke read the first
document's coordinates from the second one's pixels.
* fix: reject stale cross-document clone previews
* Clone Source: a copy opened after the source document closed isn't its source
A native copy keeps its document's ID, and a reopened document may keep its ID. A slot set in
a document that was then closed resolved that ID again once a copy was open, and painted from
the copy instead of reporting the closed source (CodeRabbit, clone_source.rs).
Session::close now marks the slots set in the closing document closed (CloneSources::forget_doc,
next to the other per-feature forget_doc calls). A closed slot refuses the stroke and its live
preview with the closed-source error, shows "Source: document closed" in the panel, reports no
`document` in cloneSource.list/set, and draws no overlay or crosshair, whatever document has the
ID now. Setting a new source clears it.
* Object Selection: New, Add, Subtract and Intersect mode buttons (#12853)
The options bar shows the four mode buttons before Object Finder. Clicks and
rectangle drags combine in the bar's mode, with Shift adding, Alt subtracting
and Shift+Alt intersecting (rectangle drags used to subtract with Shift+Alt).
In New mode a click inside the selection deselects and a drag inside moves
the outline, as the marquees do.
* Object Selection: Cmd stays the Move tool inside the selection; update tests for New-mode Deselect
* Object Selection mode tests: a Cmd-drag floats the pixels and lands as Move on the drop
* Object Selection: keep Object Finder off's Quick Selection dab and rectangle
A press inside the selection drags the outline only with Object Finder on;
with it off the click is still a Quick Selection dab and the drag a
rectangle. The dab subtracts with Alt again, including Shift+Alt.
* Object Selection: keep the Indexed/Bitmap alert for a press inside the selection
The New-mode outline gesture skipped finish_object_selection, so a click
deselected and a drag moved the outline without the tool's alert (#12867).
* Layers panel: off-screen group rows keep only their disclosure widget (#13117)
A group row scrolled out of view painted its whole row every frame. It now
registers only its disclosure triangle (hit area, click, tooltip, accessibility
label) and returns, like any other off-screen row (#125).
* Add layers_panel_bench: Layers panel frame time with many groups (#13117)
Idle frames of the app in a CPU harness on a document of N collapsed groups
(or plain layers, or expanded groups), with the panel scrolled to the top.
* Layers panel: keep a row whose eye-column menu is open (#13127 review)
The eye column's colour menu is the eye response's own popup, which
resp.context_menu_opened() doesn't see. A group row with it open now stays
whole when scrolled out of view, as egui closes a popup a frame doesn't show.
Also documents the bench example's helpers.
* Layers panel test: an off-screen group row adds no text shapes (#13127 review)
* Layers panel: skip off-screen Smart Filter rows (#13118)
Filter rows scrolled out of the list keep their place but no longer paint,
register widgets or take drops, as effect rows already do. A filter row takes
a reorder drop only where the list shows it, as masks do since #10485, so a
filter released over the panel header no longer lands on a row scrolled
under it. A selected filter stays selected while scrolled away.
* Re-run CI
* Move: frame and snap a dragged group (#13418)
A group dragged with the Move tool showed only the arrow: the moving box
came from snap::layer_rect, which skips groups, so a selected group got no
Move gesture, no box, no snapping and no Smart Guides.
Measure what the move carries instead: the move targets, a group as the
layers in it (whole, even with a selection), an artboard as its board.
Keep the targets and everything inside them out of the snap targets, so a
group doesn't snap to its own layers' starting edges.
* Rename the group drag test helper so it doesn't clash with drag()
* Move: keep linked layers as snap targets while selected pixels move
Camera Raw's window is an immediate viewport: it takes input and redraws
only while the main window draws a frame. Opened over a full-screen main
window, the new window covers it (Hyprland hands it the full screen, or
the maximized state), the compositor sends the hidden main window no more
frame callbacks, and the dialog freezes: sliders don't move and the app
looks hung until the main window is visible again.
While the main window is full screen, or maximized on Wayland, the dialog
is now drawn over the main window, as the web build already does.
Elsewhere a maximized window keeps Camera Raw in its own window.
Tested on Hyprland 0.56 (Omarchy): Super+F and Super+Alt+F both froze
the dialog before (a control-channel slider change timed out after 60 s
or took 48 s) and respond normally now.
A few samples can fit worse than more, so a failed doubling step looks one
step further before it stops. On whole-pixel samples the vee now gets the
reference application's four anchors at Curve Fit 2 (was five); the Curve
Fit 2 tests run on both the given and the whole-pixel strokes.
fit_trace places the Freeform Pen's anchors as the reference application
does on the issue's sample strokes: corners only at turns sharper than
80°, each run fitted one segment at a time as far as one cubic follows the
trace, ending on the first turning point in x or y within reach, with the
segment's turn bounded by Curve Fit (75° per px, at most 150°). The end
handle of each piece is fitted, not estimated. A closed trace keeps its
release point as an anchor. Make Work Path keeps its fitting.
Curve Fit 2: sine 6 anchors on the peaks, circle 5, open circle 4, vee 4
smooth, line unchanged; 0.5 and 1 px give more anchors.
At the History limit a successful deletion records one step and evicts the oldest, so the step count did not change and the click skipped selecting the anchor that followed the deleted one. `apply` now reports whether the engine ran the edit, and the click selects the follower on success instead of comparing step counts. Adds a test that fills History to its cap first.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
* Use craft-psd from craft-libs; remove crates/psd
* Regenerate the scorecard (28 crate roots)
* Take craft-psd at craft-libs 55d120f2
* Regenerate lockfile for current shared format crates
* Pin shared format crates to current craft-libs main
* Match brush import cap to shared decoder budget
* xtask: update layer fixtures after PSD extraction
The merge with main left `mod delete_anchor_alt_tests;` without its
#[cfg(test)], so the test module was built into the library.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and fixed
* Use craft-raster-codecs, craft-dpx, craft-tga, craft-gif and craft-exr for the format code in crates/codecs
* Regenerate lockfile for current shared format crates
* Pin shared format crates to current craft-libs main
* Fix codec module imports for clippy
The shape tools' options bar gets Path operations, Path alignment and
Path arrangement after the shape options. The operation is the shape
tools' own setting (New Layer by default, Exclude in Path mode); any
other operation adds the next drag to the active shape layer with
shape.create addTo and op. Reset Tool restores New Layer. Alignment
and arrangement entries stay greyed until the engine can move path
components.