* feat(engine): Resize Image During Place drives the implicit Place fit (#204)
* Merge origin/main; regenerate docs/scorecard.md
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(ui): the Dynamic Color Sliders preference controls the colour slider tracks (#204)
* Regenerate docs/scorecard.md
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>
Photoshop renames an action in place when its name is double-clicked in the
Actions panel. PhotoCraft had no way to rename an action at all, so every
action kept its "Action N" default name.
- New engine command `actions.rename {"action": name|index, "name": str}`.
Empty, whitespace-only and duplicate names are rejected (the old name
stays). Nested `actions.play` steps that call the action by name, and
its function-key binding, follow the rename. Refused during playback.
- Actions panel: double-clicking the name opens an in-place field with the
name selected; Enter, Tab or clicking elsewhere commits through the
command, Esc cancels. Double-clicking elsewhere on the row still plays.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity (merges parity-estimate.md), gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and UI, brush, format, hardware and localization parity docs. docs/parity.md is now docs/parity-checklist.md.
The Windows test failure (file_dialog::tests::save_dialogs_remember_where_each_document_was_last_saved) is pre-existing on main: the test hard-codes POSIX paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prerequisites listed `libxkbcommon-dev ... libgtk-3-dev`, which exist only
on Debian and Ubuntu: on Arch none of those names resolve and `pacman -S` fails
on the whole line, and on Fedora and openSUSE they all need `-devel`. Nothing
else in the repository gives the equivalents.
The app already speaks four distros at runtime: linux_libs.rs carries a
{soname, debian, fedora, arch, suse} table, reads ID and ID_LIKE from
/etc/os-release and prints the matching apt, dnf, pacman or zypper command when
a runtime library is missing. This brings the build instructions up to that.
Also adds the prerequisites to the book's Building page, which said only to
install the Rust toolchain.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Crop tool: Photoshop's default mode turns the image behind an upright box; Use Classic Mode (P), Auto Center Preview (#1919)
The Crop tool's gear menu gets Use Classic Mode (P toggles it while the tool shows a box; off by
default, as in Photoshop) and Auto Center Preview (on by default). In the default mode the box
stays upright on screen: a drag outside it turns the image about the box's centre (Shift 15 deg
steps, angle readout), a drag inside moves the image under the box, the arrow keys nudge the
image, Straighten turns the image so the line is level, and with Auto Center Preview a drawn or
resized box is re-centred in the canvas. Classic Mode is the previous behaviour.
The crop model and the commit are shared: crop_rect + crop_angle and one image.crop {angle}.
crop_mode.rs only drives the view: while a frame is edited the view's rotation is -crop_angle,
pivoting so the box (or the pressed point) stays put, and the user's view (a Rotate View turn,
its centre) is put back on commit, cancel, leaving the tool or switching to Classic Mode. The
untouched default frame leaves an R turn alone. The options live in ToolOptions.crop_shield
(classic_mode, auto_center_preview; ui.set cropShield). ui.pointer reads a default-mode drag's
points in the view at its press, so an up where the last move was is idempotent.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Crop default mode: match Photoshop (view upright on tool pick, live Auto Center, image-turn readout, gear menu order) (#1919)
Checked by hand against Photoshop in its default (non-classic) mode:
- Picking the Crop tool turns a Rotate View (R) turn upright at once (about the view centre),
and the view stays upright after Enter or Esc: the save/restore of the user's view is gone.
Classic Mode still leaves the view alone (not checked against Photoshop).
- The angle beside the pointer while the image turns is the image's turn, clockwise positive.
- Auto Center Preview keeps a box being resized centred live: the handle follows the pointer,
the box grows on both sides, and on the image the opposite edge (with Alt the centre) stays
put. Computed from the press, so the view following the frame can't feed back into the drag.
- Gear menu order as Photoshop's: Use Classic Mode (P), Show Cropped Area (H), Auto Center
Preview, no separator; Classic Mode greys out the other two. Show Cropped Area then acts as on
and H does nothing (the stored choice is kept). German: Classic-Modus verwenden, Automatische
zentrierte Vorschau.
- Crop preview past the canvas in a turned view: each band is drawn as its turned quad (the
screen bounding box stretched band textures and spread the checkerboard). The white corners in
the earlier offscreen snapshot were egui_kittest clamping the repeating checker texture; the
real app shows the checkerboard in both modes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs: regenerate scorecard for the crop default-mode row (#1919)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Crop default mode: the canvas keeps the view the Crop tool moves; a move starts from a centred box (#1919)
Tried by hand in the app: with Auto Center Preview on, a drag inside the box made the box wander
instead of moving the image, and a handle drag didn't keep the box's centre in place (after a
Space pan, too). Root cause: canvas_view works on a copy of the view for the frame and writes it
back at the end, so every camera change the Crop tool made during the frame (the image moving
with the pointer, the live-centred resize, Auto Center on release, the pivot while turning) was
undone, while the frame itself moved in the document. Tests driving tool_event never saw it.
- canvas_view notes the camera before the frame's input and, before writing its copy back,
keeps a centre or rotation a tool changed meanwhile.
- With Auto Center Preview a move inside an off-centre box (after a pan) centres the box first,
then grabs the image point now under the pointer.
- New tests/crop_default_mode.rs drives the full app UI with egui pointer and key events over
several frames (offscreen wgpu): the box stays put while the image follows the pointer, and a
resize after a Space pan keeps the box's centre fixed while the handle follows the pointer. Both
failed before the fix.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Crop default mode: a moved or resized box stays where it is on screen (#1919)
Checked in Photoshop: after a Space (Hand) pan the box isn't snapped back to the middle of
the canvas. A drag inside moves only the image, the box staying wherever it is; a resize keeps the
box's centre where it was at the press (also after a pan), and its release no longer pans the box
to the middle. Auto Center Preview still centres a newly drawn box on release and X still
re-centres. Classic Mode is unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Crop tool: Classic Mode turns a Rotate View turn upright too (#1919)
Checked in Photoshop: with Classic Mode on and the view turned with Rotate View (R), picking
the Crop tool also turns the view upright, and the crop frame stays on the same image area. So the
Crop tool now drives the camera in both modes: default mode keeps it at -crop_angle (the box
upright), Classic Mode at 0 with the frame drawn with its own turn. Picking the tool pivots about
the view centre; commit and cancel leave the view upright in both modes, and P switching modes
never brings the R turn back. Auto Center Preview stays default-mode only.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Regenerate the scorecard; update the crop test's header
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crop: snap a new box's corner inside the untouched default frame in both modes (#2549 with #2545)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- [profile.dev] debug = "line-tables-only", matching CI's CARGO_PROFILE_DEV_DEBUG; halves link
time and PDB size for test binaries.
- .config/nextest.toml: optional cargo-nextest runs with the GPU tests capped at 4 concurrent
processes (unlimited ones time out in wgpu).
- AGENTS.md / docs/development.md: iteration commands (--lib --tests, --test <file>, nextest,
doctests separately). CI is unchanged.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Reuse Facet quadrants and halftone dots without changing pixels
* Record full-image Pixelate timings and exact-output checks
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Interface > Show Channels in Color only reached the Channels panel
thumbnails (#1986); the canvas overlay hard-coded in_color = false, so an
isolated RGB/CMYK channel stayed gray while its thumbnail changed color.
channel_view::ensure now reads the preference (the view cache key already
carries the flag, so toggling rebuilds the texture), and a hidden Lab
Lightness goes neutral instead of black under the preference, so an
isolated a/b channel tints over mid gray like Photoshop. Lightness itself
stays grayscale (Adobe's exception).
Verified with the offscreen snapshot example: RGB green canvas goes
(128,128,128) -> (0,128,0) and CMYK cyan -> (0,176,232) when the
preference is on; unit tests cover render(), the Lab exception and the
cache key.
Co-authored-by: highperfocused <highperfocused@pm.me>
* EXR parts: import parts, channel groups and an AOV precomp as layers
file.exrParts lists what an EXR holds for a chooser - flat parts, prefixed
channel groups (diffuse.R/G/B), Cryptomatte layers as information;
file.openExrParts imports the chosen ones as the layers of one document
(F16 lossless as F32, linear sRGB tagged), with an optional additive
beauty precomp summed from the light-path AOV set (direct/indirect
diffuse+specular, emission; transmission/sss/volume when present) that
fails naming the missing AOVs. The codec gains decode_exr_channels for
exact named channels of one part; io shares the image_surface band writer.
File › Open EXR Parts… picks an EXR and opens a plain command form: a
checkbox per part and channel group (labels are the data's names with
size and channel count), a both/left/right view dropdown, and the
precomp checkbox. Cryptomatte layers ride along as non-rendered data for
the control channel. Form fields gain __labels for verbatim data-driven
labels; the new strings are catalogued in all sixteen complete languages.
* Regenerate parity and scorecard docs
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
image.crop takes optional targetWidth/targetHeight (px), resolution (ppi)
and resample: the cropped area is resampled to exactly that size (the
Image Size code path, now shared) and the resolution set, in the same
undo step, also after a turned crop. The Crop options bar gets
W x H x Resolution with unit-aware W/H fields, a px/in or px/cm
resolution, Photoshop's built-in size presets, Front Image (and I) and
Clear; the frame holds W:H while drawn or resized, X and the swap button
swap W and H.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Menus: name the platform file manager in Reveal in Finder off macOS
The command opens Explorer on Windows and the desktop's file manager
(xdg-open) on Linux, but the Smart Objects menu item and the document
tab menu still said "Reveal in Finder". menus::reveal_label picks
"Show in Explorer" on Windows and "Show in Folder" on Linux and the web;
macOS keeps "Reveal in Finder". The menu catalogue (docs/parity.md),
the engine command labels and the command ids stay unchanged.
The help search, command palette and keyboard shortcut list read the
relabelled menu item, so they follow. Every complete language gets both
new labels, with the localized Windows name for Explorer.
* scorecard: platform shortcut hints and Reveal label are done (UI-217-19)
* Make large pixel operations allocation-fallible
* Warp: drop the fixed 400 MP cap; fallible reservations already cover large transforms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Adapt #2530 to main: fallible steep-Distort warp, Result from mask paste in float moves; regenerate 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>
* fix(compose): Preserve native CMYK fills through PSD round trips
Share depth-quantized ink samples between rendering and PSD caches.
Use the document profile for RGB fill colours and preserve valid imported
caches. Uncached CMYK fills use CPU rendering.
Cover all sample depths, gradient styles and tiled renders. Fix the real
16-bit gradient round trip and raise the psd-tools floor from 307 to 308.
* perf(raster): Reuse exact 8-bit CMYK conversions within rows
Cache the previous ink bytes and ICC result per row, with alpha decoded
independently. Keep the 16/32-bit conversion loops unchanged.
Matched 1 MP reads improve by 77% for smooth ramps, 33% with dithering
and 10% for random colours. Bitwise regression coverage includes all
depths, alpha and tile boundaries.
Open and save .ora with pixel layers (any offset), nested groups, pass-through
groups, names, visibility, opacity, edit/alpha locks and blend modes (svg:* plus
Krita's krita:* names), at 8 and 16 bit. Save applies pixel masks, renders
text/shape/fill/smart layers to pixels and reports what OpenRaster can't hold.
Wired into File > Open/Save, the CLI, automation/MCP, the web picker and the
desktop file associations. Checked against 34 Krita 5.2.9-authored files
(opt-in krita_corpus test) and Krita re-opening our exports.
Co-authored-by: Scott Campbell <251750033+scottcampbelldata@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- The live set found beside Photoshop is offered, never applied unasked: an
"Import keyboard shortcuts?" prompt names the set and how many keys it
changes, with Import / Don't Import. Either answer is recorded in
dialogs.photoshopShortcuts, so it is asked once; ui.inspect shows the
pending offer as kysOffer.
- Parsing and label matching moved to the engine (photocraft_engine::kys), so
edit.keyboardShortcuts {"importKys": <xml>} imports a set from agents, the
CLI and MCP; the app's dialog uses the same plan with its menus' labels.
- "The keyboard shortcut set was not imported: {error}" is translated.
- .kys input over 4 MiB is refused before it is read as text.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Expose native shape stroke controls with staged previews
* Document shape stroke defaults in MCP control schema
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Distance fields: the Euclidean passes run on threads, results unchanged (#2119)
The chiselled bevel fields (#2006) and the precise glow fields are built
from an exact Euclidean distance transform, a few sweeps over the whole
map that all ran on one thread. They now run on scoped OS threads, in
groups of rows or columns handed out from a queue: edt_nearest (the
column pass, then the row pass), sharp_edges (rows), refine_edge_normals
(one edge pixel each) and the final per-pixel maps of the bevel and plain
Euclidean fields. Not rayon, as for the blurs (rows_par): maps are built
from inside rayon tiles, where a waiting worker steals other tiles (#276).
Maps under 65536 pixels stay on the calling thread, wasm always does.
Every unit is computed from its own inputs, in the same arithmetic as
before, so the fields are bit for bit the old ones whatever the thread
count: 216 fingerprints (bevel in and out, plain in and out, bevel_maps on
bars at 0 to 150 degrees, thin bars, discs, rounded rectangles, soft,
patchy and half-opaque masks, 40 stars, tiny and 1 pixel maps) are equal
at 1, 7, 16 and the default threads, and equal to the code before.
edt_nearest returns the nearest pixel as u32 (it was usize), which takes its
working memory from 20 to 16 bytes per pixel; a map past 2^32 pixels or a slice that does
not match its size gets "far" distances instead of a panic.
CPU compositor, 12 MP, bevel size change (ms, bevel_bench), main -> this:
ellipse 474 -> 217 (size 8), 990 -> 318 (250); text 747 -> 214 (8),
1110 -> 267 (250). A distance_field call on a 256x256 map goes from 2.2 to
1.8 ms, on 512x512 from 8.0 to 2.8, on 2048x2048 from 99 to 15.
Tests: par_units hands every unit to the closure once with its own
slices (short last unit, no second slice, units of different sizes, 1 to
64 threads); the nearest-pixel transform equals itself on 1, 2, 3 and 8
threads on awkward sizes and returns a seed at the distance it reports;
degenerate sizes; sharp edges and the four Euclidean fields are bit-equal
on 1, 2, 5 and 16 threads on discs, rounded rectangles, bars at several
angles, patchy opacity and an edge on the border. The nested-rayon
deadlock test (#276) now has a chiselled bevel layer in its document.
* GPU effect maps: the Euclidean fields are one window, not bands (#2119)
A bevel size change rebuilds the distance fields over the whole effect
region (the region grows with the size). The GPU path cut that into bands
of max(128, halo) rows, each computed from a window grown by the halo,
which for the bevel fields is reach + 10: at size 250, 262-row bands from
windows of about 786 rows. Every pixel went through the transform and the
edge refinement about three times, a 12 MP region gave about 12 tasks for
32 threads, and the two bevel fields ran one after the other.
The Euclidean fields (chiselled bevels, precise glows) now come from one
window, and distance_field spreads its own passes over the threads. A
rebuild is the whole region, which is also what the CPU compositor
computes, so the GPU canvas gets exactly the CPU's field: bands resolved
ties between equidistant edge pixels per window, and on a rebuild they
differed from the whole region by up to 0.07 px (ellipse) and 1.3 px (600
discs) at size 250, on a few dozen pixels in 12 MP. A partial update (a
brush dab) still computes the damage grown by the halo, from one window.
The chamfer fields (shadows, strokes, spread) are a sequential sweep and
keep the bands, and so does wasm, where there are no threads to spread
over and bands keep the memory small.
RTX 4080 SUPER, release, 12 MP, ms per size change on the GPU canvas
(bevel_bench from #2118, p50, median of 3 runs, before #2006 / main /
this), ellipse: 90 / 103 / 98 (8), 162 / 188 / 119 (50), 286 / 446 / 219
(250); text: 83 / 139 / 113, 140 / 215 / 130, 222 / 489 / 165. Alone in the
process (--no-cpu) ellipse at 250 is 407 -> 157 and text 468 -> 169.
36 MP ellipse at 250: 779 -> 382. Peak RSS: 12 MP at 250 1604 -> 862 MiB,
36 MP at 250 2987 -> 1726 MiB; at size 8 on 36 MP it is 163 MiB more
(1380 -> 1543). Precise glow fields on 12 MP: 46-84 ms -> 26 ms at reach
52-252 (32 ms at 12).
The window test now checks that the whole region is bit for bit the CPU's
field, and that a too small halo is noticed on a part of the region.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
doc_open, doc_save and the other automation file paths only took
forward-slash relative paths, so an agent passing C:/Users/me/a.png with
a C:/ root was refused. An absolute path is now compared with the
root's absolute path as text: if it lies beneath the root, the root's
components are stripped and the rest goes through the same checks as a
relative path and is opened through the same directory capability.
Nothing is resolved on the filesystem, so traversal, prefixes, device
names and link escapes are refused as before; absolute paths outside
the root, or with no root granted, are still refused.
Fixes#2176.
* Expand arithmetic expressions across numeric inputs
* Test arithmetic and scientific notation in indexed color counts
* Drop the numeric_fields example (it wrote PNGs to /tmp; the widget tests cover the behaviour)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
On a smart-object layer, Image > Adjustments now records an editable
smart filter instead of committing disaster.
- new engine helpers:
- adjust_cmds::has_adjustable / adjust_as_smart_filter route
image.adjustments.<kind> onto the source, with the selection as the
filter mask, through smart_cmds::add_smart_filter
- brush/eraser/pencil/live preview strokes now render inside
with_cmyk_space(doc_cmyk_space(session)), so painted colours follow the
document profile the same way the compositor does
- docs/scorecard.md: FILE-215-4 partial -> done
- tests: adjust_cmds/tests + brush_cmds/tests, plus panic_hunt and
the lib suite all green on the committed tree
Known follow-up, already filed:
- FILE-215-5 rows: double-clicking an adjustment smart-filter row is still
a no-op. smart_ui::open_editor only proceeds when
filter_dialog::has_dialog returns true, and has_dialog accepts
filter.*, a fixed non-adjustment set and PREVIEWED - but not
image.adjustments.* (those open through adjust_dialog::open, which isn't
wired into the smart-filter row).
This is one commit on top of the latest upstream/main (rebased to fix the
stale fork ancestry that was blocking PR creation).
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Add the Remove tool with non-local patch completion
Tool::Remove (J flyout) paints a hole that paint.remove fills from its
surroundings as a background job, in one undo step. A stroke around an
object also removes what it encloses; the selection limits the hole.
The fill (photocraft_algo::nonlocal) follows Newson, Almansa, Gousseau and
Pérez (IPOL 2017): texture features in the patch distance, onion-peel
initialisation, match fields upsampled between levels and a best-patch
final copy, plus gradient-domain hiding of edge and interior seams.
Sparse holes are filled in tiles. inpaint::complete (Content-Aware Fill,
Spot Healing, Content-Aware Move) is unchanged.
On 20 holes in public-domain photos (remove_quality) the fill keeps 0.89
of the texture it replaces, against 0.54 with the Wexler/PatchMatch
completion. 24 MP (remove_bench): 100 px ring 120 ms, 1000 px ring 2.5 s.
* Translate the Remove tool's label and tighten comments
Adds "Remove Tool" to every UI catalog, following each one's pattern for
tool names and its existing word for Remove. Corrects comments that no
longer matched the code: the benchmark's description, the tile and margin
minimums, and the reasons given for two parameters.
* Put the Remove tool first in the J group
The J flyout's slot now shows Remove until another J tool is picked, and
J from another group selects it; repeated J goes on to Spot Healing Brush,
Healing Brush, Patch, Content-Aware Move and Red Eye.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* feat(ui): follow system appearance with a System theme
Follow the OS appearance through the existing egui input while retaining System as the saved theme choice. Apply the resolved palette to both egui styles and custom surfaces, and keep document surround settings independent.
Cover launch and live transitions, persistence, manual themes, menu checks, the Interface picker and Home contrast. Validate engine/UI suites, adversarial commands, wasm, layering, parity and the AutoReview gate. Strict Clippy reports pre-existing findings documented in the PR.
Refs: storytold/photocraft#2074
* Keep the document surround on the displayed theme in Sync with system (drop the dark-surround pinning)
---------
Co-authored-by: coygeek <coygeek@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Pen pressure curve: Preferences › Tools shapes how pen pressure reaches every brush
Brush phase 6a (TOOL-213-11, #610). Every pen paints its pressure straight through, so artists
with a heavy or a light hand, or a tablet whose response feels off, had no way to adjust it.
- `tools.pressureCurve`: [input, output] points, linear by default, settable with `prefs.set`
(validated: 2..=16 pairs within 0..1).
- `prefs::PressureCurve` reads it as a monotone cubic (Fritsch–Carlson), so a firmer press never
gives less pressure. It sanitises the points itself (finite, clamped, sorted, one per input,
capped, outputs made non-decreasing; flat beyond the ends), because a whole-section update or a
hand-edited file can store anything.
- The stylus maps every pen sample's pressure through it before any brush sees it, so the live
stroke, the commit and the journal agree. A mouse stays at full pressure.
- Preferences › Tools draws a curve editor under Use Tablet Pressure: drag a point, click to add
one, right-click to remove one, with Linear, Soft and Hard presets. Translated in all 16 catalogs.
* Regenerate docs/scorecard.md
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 SVG import detection and configurable group depth
* Fix malformed SVG error routing and formatting
* Refresh scorecard for SVG depth preference
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Photoshop switches to the Move tool while Cmd (Ctrl off the Mac) is held
with every tool but the Hand, Rotate View, Zoom, Crop, Slice, Path Selection,
shape, Pen and Type tools. #896 did this for the selection tools per press; this
does it for the rest (Brush, Eraser, Gradient, Clone Stamp, Eyedropper, ...).
hold_keys gains Temporary::Move (a modifier, not a rebindable key); the
canvas sets PhotocraftApp::tool_override for the frame and the tool state
machine, the Move tool's selection check (move_ui::moves_selected_pixels)
and the tool modules that gate on the tool read active_tool(). The held-tool
latch now lasts through the release frame, otherwise the Move's Up went to
the selected tool. So Cmd-drag with the Brush moves the layer, Cmd+Alt-drag
duplicates it first (move_mods), Cmd-drag with a selection floats the
selected pixels, and Cmd-click picks the layer under the pointer.
Off the Mac, Cmd+Alt is Ctrl+Alt, which the brush tools keep for the
on-canvas resize (brush_resize, also Alt+right-drag there): with a brush
tool Ctrl+Alt-drag resizes, with the other tools (Gradient, Eyedropper...) it
duplicates. Tests pin both layouts on any host. docs/control-protocol.md
describes `command` on ui.pointer events.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Masks: target a new layer mask and keep separate colour pairs for mask and pixels (#2166)
Photoshop keeps one foreground/background pair for layer masks and one for
layer pixels and swaps them when the edit target changes; adding a mask
targets it, with white/black. PhotoCraft never touched the colours when a
mask was targeted, and adding a mask left the pixels targeted.
- engine: ToolState::target_mask swaps in the other target's pair
(mask pair starts at white/black, colours picked on either side are kept).
- ui-egui: sync_mask_targets applies the effective target (mask targeted,
active layer has a mask, composite channel, no Quick Mask) after every
command, every frame and every control request; the Layer Mask add
commands target the new mask.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fix merge: close mask colour-pair test before the deletion tests
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* scorecard: Marquee Shift, Alt and size readout are done (TOOL-214-2)
The row still said canvas.rs passes false for Shift (#208), but #208 is
fixed: marquee_corners applies Shift (square or circle) and Alt (from the
centre), the W x H readout follows the drag, and marquee_tests.rs covers
all three.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* scorecard: on-canvas brush resize drag is done (TOOL-213-10)
The row said only [ ] resize the brush, but brush_resize.rs (#297, #370)
implements Photoshop's drag: Alt+right-drag, or Control+Option-drag on
macOS, sets diameter and hardness with a circle and readout, and is
covered by nine tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Use shared Color Picker for Layer Style colors (#2015)
* Use shared Color Picker for text style colors (#2015)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Edit › Keyboard Shortcuts: Import Photoshop Shortcuts… loads a .kys set (#1163)
Photoshop saves a shortcut set as .kys XML: <command name="Gaussian Blur...">
rows with one or more <shortcut>Control+Opt+Cmd+G</shortcut>, then the tool
keys. The dialog's new button (and File › Open or a drop of a .kys) parses it
with roxmltree, matches each row to our commands by menu label, and fills the
dialog's overrides, so OK applies the set the way hand-made changes are
(a key moved to a command is taken from its old owner) and Cancel discards it.
Rules, from Photoshop's own ambiguities: a label in several menus (Levels…
under Image › Adjustments and Layer › New Adjustment Layer; All under
Select, Edit › Purge and View › Show) goes to the command whose default is one
of the row's keys, else the one with a default, else the first in menu order.
A row's first key is kept unless one of its keys is already the command's
default (Zoom In is Cmd++ then Cmd+=). Scripts, Bridge, the frames commands
and the macOS application menu have no command here and are counted in the
message. Tool keys are not imported: the tool letters are fixed.
* First launch beside Photoshop imports its live shortcut set; the Open dialog lists .kys
Photoshop keeps the set in use as Keyboard Shortcuts.psp in its per-version
settings folder (macOS ~/Library/Preferences/Adobe Photoshop <v> Settings,
Windows %APPDATA%\Adobe\Adobe Photoshop <v>\Adobe Photoshop <v> Settings),
the same XML a saved .kys holds. The desktop app hands the newest version's
file to the UI (Services::photoshop_shortcuts); at the first launch that has
no shortcut overrides yet, kys_import::auto_import applies it through the
Keyboard Shortcuts dialog's own OK path and posts a notice naming the set and
the count. The preference dialogs.photoshopShortcuts records the source, so
the import happens once; someone who already changed a shortcut keeps theirs.
Reset All to Defaults undoes it.
The Open dialog's filter now lists .kys (it greyed them out), and dropping
Keyboard Shortcuts.psp imports it like a .kys.
* kys import: Ukrainian and Dutch strings; tests follow Cmd+K and Cmd+F as the defaults (#1900)
* kys import: user-facing strings name no other product (#1985)
The button is Import Shortcuts…, its tip names the .kys format, the notice
says "Keyboard shortcuts imported" and names the set, and a file that is not
a set is "not a keyboard shortcut set (.kys)". A set without a name is called
by its file name. Comments, identifiers and the XML root tag keep the format's
own name.
* rustfmt: format panels.rs and prefs_ui.rs
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Notices: auto-hide after a delay, with Interface preferences (#2022)
Non-blocking notices no longer stay on screen until closed: each card
hides itself after Interface › Notification › Auto Hide Notifications'
delay (new, default 6 s), and the timer pauses while the pointer rests
on the stack until it moves away. Two new Interface preferences,
notificationAutoHide and notificationDurationSeconds, are read by
notices.rs; errors still land in the status bar.
The decision lives in the pure notices::expire helper, unit-tested
without an egui clock; Notice.shown_at round-trips through the control
channel with serde defaults. Labels translated across the complete
catalogues. Regenerated docs/scorecard.md.
* Notices: repaint on expiry, and a fresh delay when a long hover ends (#2022)
Addresses review of the auto-hide change:
- When the last notice expires, `show` had already painted it and, with
nothing left, requested no further frame, so the card lingered until
unrelated input. It now requests a repaint in the frame that drops
anything, clearing the stack at once.
- A stationary hover over a settled UI refreshed `shown_at` only on
frames, so hovering longer than the delay then leaving expired the
notice instantly. `show` now tracks the pointer leaving the stack and
restarts the timers on that frame, so hovering never counts as elapsed
time.
Regression tests: expiring a notice requests a frame to clear it;
leaving a long hover gives a fresh delay.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Appearance modes: sync with system, separate dark and light themes
* Appearance button cycles Auto, Light, Dark
* Appearance: default stays Dark, follow the portal signal instead of polling, split out the Preferences resize
* Preferences: size both appearance cards for the five dark themes
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Toolbar: Studio themes draw large round colour chips with Switch Colors right below
* Toolbar: round chips keep Default Colors, follow Tokens::round_chips, lie side by side in two columns
* Theme: Solarized Dark and Adwaita use the Studio round colour chips
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Brush preset chip: draw the tip preview in the icon colour on light themes
The options-bar chip always painted the round tip white, which nearly vanished on Studio
Light's chrome (246 gray) and disappears entirely on a white header bar. Dark themes keep
Photoshop's white tip; light themes use their icon token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add Adwaita and Adwaita Dark themes
Two opt-in themes that put the Studio layout in GNOME's libadwaita palette, so PhotoCraft
sits with the rest of a GNOME desktop: header bar, sidebar, card and view colours, the
GNOME blue accent (#3584e4), destructive and warning colours, 6 / 9 / 12 px radii, and a
neutral (not red) close button as in GNOME's title bars. The default theme is unchanged.
Wired through ThemeKind, the interface.theme preference (adwaita, adwaitaDark), Window ›
Theme and the control channel, with menu and preference labels in every complete-menus
language ("Adwaita" kept as the proper name). A test holds both themes to WCAG contrast:
7:1 for text and 4.5:1 for dim text on every surface that carries labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: translate the Adwaita theme names in Dutch and Ukrainian
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Squashed onto main, with the maintainer's asks applied:
- Photoshop's blend menus (layers, brush, pencil, gradient, layer style) stay as they are;
the Layers panel and Blending Options list a Paint.NET mode only for a layer that uses one.
- The six Paint.NET mode labels are translated in every catalog, and the blend-mode i18n test
iterates layer_modes().
- pdn::pixels rejects truncated uncompressed (version 1) data before allocating its buffer.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fix mask trash targeting and Solid Color Fill editing
* Fix Solid Color picker mode conversion and validate RGB ranges
* Apply rustfmt to Solid Color regression tests
* Fix Solid Color mode regression test using distinct RGB values
---------
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>