* Filters on a type layer: ask to rasterize or convert to a Smart Object instead of greying out (fixes#4303)
* Filters on a type layer: Liquify, Filter Gallery, Camera Raw, Adaptive Wide Angle, Vanishing Point and Last Filter ask too (fixes#4303)
winit's Windows pointer path reports pen pressure 0 as no force, so a tap
lighter than the pen's first pressure level arrived as a force-less touch.
Stylus::update ignored those, saw no pen, and the tap painted at 1 like a
mouse click.
Track touch contacts: a touch move outside a contact (hovering) or any
reported force marks a pen, and a pen contact with no force yet is pressure
0. A finger on a screen without force sensing still paints at 1. A contact
that starts in the frame another lifted now keeps its force too.
Also credit @Furash in contributors/people.toml.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Replacing the colours could shrink the table while the transparent index
still pointed past its end, so the palette PNG export refused the table.
A stale index is now dropped (pixels end up as with "transparent": null),
and an explicit invalid index (out of range, negative, non-integer) is a
BadParams error before any edit instead of silently keeping the old one.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Noise, Wet Edges, Build-up, Smoothing and Protect Texture toggle from
anywhere on their row while keeping the selected controls open. Keep
lock clicks independent and expose each switch as one accessible checkbox.
Give Smoothing a default-on enable flag so disabling it preserves the
amount and modes. Old brush JSON stays compatible; toolbar amount edits
and explicit stroke overrides enable it, and ABR imports retain disabled
smoothing values.
Cover row, checkbox, lock and keyboard gestures, old section selections,
command replay, presets, legacy JSON and 8/16/32-bit rendering.
Fixes#4283
Co-authored-by: augrclk <217665348+augrclk@users.noreply.github.com>
Co-authored-by: GPT-6 <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Brush cursor: the tip's centre mark follows Show Crosshair in Brush Tip
Brush tips wider than 6 screen pixels always drew the centre crosshair, so
Preferences > Cursors > Show Crosshair in Brush Tip could add it but never
remove it. Photoshop shows it only when that preference is on. The
Background Eraser keeps its sampling hotspot, and Clone Stamp / Healing
Brush still show it while Option is held.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rustfmt canvas.rs
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>
* Resolve every pattern name before a batch delete removes any of them.
A missing name used to delete the earlier presets and then return an error. The pattern library is not on the document undo stack, so that partial delete could not be undone.
Fixes#4103
* Pattern batch delete: reject non-string entries and resolve against the library only
A document-only pattern resolved through pattern_cmds::resolve, so the batch
deleted earlier entries before pattern.delete refused it. Non-string array
members were silently dropped (ported from #4127).
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>
* Quantize the colour field when Only Web Colors is on.
The picked colour already snapped to the web-safe ramp, but the field and slider stayed continuous, so the restriction was invisible.
Fixes#4107
* Only Web Colors: draw the field and slider in flat web-safe bands
The coarse interpolated mesh blended between snapped corners, so it showed a
smooth low-resolution gradient rather than bands. Draw flat cells sampled at
their centres instead, and test that every cell is a single web-safe colour.
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>
- image.imageSize takes percent (both sides; an explicit side wins).
- actions.conditional {if, then, else}: Photoshop's Insert Conditional as a step, 33 conditions
on the document (shape, mode, depth, selection, layers, alpha channels, unsaved) and the
active layer (kind, locks, visibility, masks, effects); a failing branch stops the caller.
- .atn events: placeEvent → file.placeEmbedded/placeLinked (path, scale), open → file.openAs,
imageSize in percent, fill from a point → paint.bucket (pixels), conditional. 105 → 108.
- Panel labels conditional steps as 'If cond → then / else'.
* Windows: keep full screen and maximized apart; never restore full screen
winit on Windows keeps the maximized style on a window that goes full
screen from maximized. The shell then draws the taskbar black instead of
hiding it, and on the way back the window stays maximized at the
monitor's full size, over the taskbar (#3550, #4018, #4230). Un-maximize
before entering full screen and maximize again after leaving it.
eframe also restores full screen from ui.ron, as full screen and
maximized at once; drop that flag in the window builder hook, so
PhotoCraft starts in Standard Screen Mode as its own state says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: add aweussom
* view_cmds: mark the winit#4756 workaround and how to retire it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rustfmt view_cmds.rs
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>
- edit.transform gains the relative form (scaleX/scaleY %, rotate, dx/dy about the frame
centre), as a Photoshop action records a Free Transform; image.applyImage takes a source
layer by name.
- .atn events: transform (layer → translate or relative transform; selection →
transformSelection), applyImageEvent, export (Save for Web, with the txtu path record
decoded and written back; kept verbatim without a destination), newPlacedLayer /
placedLayerEditContents / updatePlacedLayer, close with the Save answer → save + close,
delete of the target channel → layer mask delete, Quick Mask on/off, polygon selections →
select.lasso, Gradient Map (direct and as an adjustment layer). 92 → 105 events.
- Export tries every event listing a command id (Quick Mask on → set, off → clearEvent).
- Docs: event counts in the parity docs.
Esc returned to Standard only from Full Screen Mode, and was consumed in
the UI pass before tools, dialogs and popups could see it. Handle it in
the shortcut chain instead, after everything that owns Esc, for both
full screen modes, via view.screenMode.standard.
The preference was stored but read nowhere, so Edit › Preferences hid it.
Never now writes Photoshop's white placeholder instead of the merged image
and records has_real_merged_data = false in resource 1057, without
rendering the composite: smaller files and faster saves. Always and Ask
When Saving keep the real merged image, as Image Previews already does
with Ask. A file without layer records always keeps its real merged image,
and layered TIFF still renders the composite it is made of.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fix multi-layer Delete and Move drags on a locked Background.
Delete was removing only the active layer's targeted mask when several layers were selected. A Move drag that starts on the locked Background now draws the layer marquee instead of showing the lock dialog.
* Drop the multi-layer Delete hunk: #4078fixes#4025
This PR keeps only the Move-on-locked-Background marquee fix (#4058).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move: a click on a locked layer still selects it before the marquee
Keeps hidden_layer_tests::auto_select_skips_hidden_layers green: Auto-Select
picks the visible Background under the pointer, and a drag's marquee replaces
that selection on release.
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>
* Actions panel: set folders, dialog toggle column, Insert Stop (#219)
- Engine actions_edit_cmds: actions.insert (any command at a position; Insert Stop… uses it
with actions.stopMessage), actions.dialog (the per-step dialog flag), actions.assignSet.
Flag sets follow inserted steps; nothing edits during playback.
- Panel: loose actions first, then each set as a collapsible folder row with its count
(ui.actions.collapsedSets); dropping an action among another set's rows moves it there;
Save Set… on a folder's context menu; a dialog toggle column next to the check marks;
Insert Stop… in the panel menu with a small message/Allow Continue dialog.
- Shell playback pauses on a dialog-toggled filter or adjustment step: the command's dialog
opens seeded with the recorded values; OK runs the dialog's values and resumes from the next
step, Cancel stops. Agents play headlessly. The play result reports paused: {step, id, dialog}.
- Docs: control protocol, scorecard AUTO-219-2 done, gaps G11.
* Translate the Actions panel's set, dialog-toggle and Insert Stop strings
* Resume a paused action only after the dialog's background job lands
A filter confirmed from a dialog runs as a background job in the desktop app; the resumed
steps met a locked document and failed. Wait for the job first (test covers background jobs).
Photoshop ships with Auto-Select on and its target set to Group;
PhotoCraft defaulted to Layer. ToolOptions::default and the serde
default now return "group"; saved preferences keep their stored value.
Co-authored-by: highperfocused <highperfocused@pm.me>
* feat(shape): recolor vector shape fill via Alt/Cmd+Backspace fill keys
Photoshop fill keys (Alt+Backspace for foreground and Cmd+Backspace for background, as well as their Shift variants) now recolor the active vector shape layer's solid fill when targeting a shape layer, matching Photoshop 1:1 instead of erroring out with 'active layer is a Shape layer, not a pixel layer'. Channel and mask targets continue to be routed to edit.fill. Includes undo support, locked layer checks, and regression tests.
* Fill keys: keep Fill from History's own enablement after merging main; rustfmt
edit.fillHistory joined the shared fill-key IDS on main and shares their
enabled check, which now also accepts shape layers. Give Fill from History
edit.fill's check so it isn't enabled where it can only fail, and limit the
locked-shape test to the four colour keys.
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>
* Load and save Photoshop Actions (.atn) (#219)
- photocraft-psd: `atn` parser and writer for version-16 action sets, exact round trip,
truncation/garbage safe, fuzz target and `atn_dump` example. Descriptor fix: `indx` and
`Idnt` reference items carry a class before the number (real action files failed to parse).
- photocraft-engine: `atn_cmds` maps 92 Photoshop events to commands and back (filters,
adjustments per channel, image/canvas/crop/trim/rotate/flip/mode, make/set/select/delete/
hide/show/move/duplicate, fill/stroke, selection, clipboard, purge, profiles, play, stop).
Other steps load as `actions.photoshopStep` (descriptor kept, written back by save, fails at
playback by name); tool selections load switched off. Commands `actions.load`,
`actions.save`, `actions.stopMessage`, `actions.photoshopStep`. `Action` gains `dialogs`
and `set`. `layer` params accept a layer name, so loaded steps replay in any document.
- UI: Actions panel menu › Load Actions… / Save Actions…; Photoshop steps labelled.
- CLI: `batch --actions set.atn`. Automation policy covers the new path params.
- Docs: file-format parity (.atn R/W), gaps G11, target-app parity, scorecard AUTO-219-2
partial, development guide.
* Translate the Actions panel's Load/Save strings in every catalog
The eight new panel strings (Load/Save Actions…, the loaded/saved counts, the kept-step label)
were missing from all 19 catalogs; the languages that were complete are back at 100%.
* Show the Actions entries in the default dock's panel menu
The legacy dock renders its own group menu and only knew the Layers and Swatches entries, so
Load Actions… and Save Actions… were missing there.
* Selection: lasso polygons scan only the edges that cross each row
The polygon fill behind Lasso, Polygonal Lasso and Magnetic Lasso tested
every edge on every sub-row of the whole document, O(height x 4 x points),
on one thread. It now keeps an active edge list over the edges sorted by
their top and fills bands of 64 rows in parallel. Crossings use the same
formula and spans accumulate in the same sorted order, so the mask is bit
identical; a test compares it with the previous fill.
* Selection: the lasso fill bands go through par_rows; edges are a named struct
photocraft_io::named_for_file(doc, name) serialises the document under the
file it is being written to, so a .pcraft manifest carries the identity the
save is about to adopt rather than the previous one — the same rule every
other format follows on import, where document.name comes from the file it
was read from. A bare extension names no file and changes nothing.
Applied once, in the .pcraft branch of export_stages, which export and
export_with both funnel through: the desktop Save (services.rs:242),
Export (services.rs:384), the web build (web.rs:355) and the engine file
command (file_cmds.rs:295) are covered by one site, and the previews
rendered before the bytes describe the same document as the manifest.
Scope per review: the automation funnels (files::save / save_bytes) already
landed in #3319 with their own named_for_output, so this carries no
crates/automation change; its scenarios are covered by
crates/automation/tests/native_save_name.rs on main.
The 8-bit stroke dither (render.rs `dither8`) ran on every stroke, including
aliased ones. An aliased stroke paints each pixel by the same amount, so it
has no gradient to band; dithering only scattered it over neighbouring
levels. A 1 px Pencil line of 219/41/61 at 30% opacity over white came out
as four colours (244/191/197, 245/191/197, 244/190/196, ...), and a
foreground between 8-bit levels did the same at 100%. Photoshop's Pencil
paints one value, and pixel art relies on it: a tolerance-0 Magic Wand or
Paint Bucket and Indexed Color palettes trip on the grain.
Composite now skips the dither when the brush is aliased (Pencil, Eraser
Pencil and Block modes). Soft brushes keep it.
Test: an_aliased_stroke_at_partial_opacity_is_one_flat_colour (fails
before the fix at pixel 5). photocraft-paint 96/96, photocraft-engine
1529/1529, ui-egui pencil 13/13, clippy -D warnings and fmt clean.
Co-authored-by: cloudalister <ngnlif3@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: language-invariant bit-depth labels are allow-listed; add missing tr/hu 'Enable animations' (#4359)
Turkish writes '8 bit'/'16 bit' exactly like the English source, so the
translated-differs assertion needs an explicit allow-list (the entry must
still exist). hu and tr had no 'Enable animations' preference label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge main; drop the Enable animations rows main already has
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>