Commit Graph
2767 Commits
Author SHA1 Message Date
xsvm 776b9dddd0 Clone Stamp and History Brush: releasing a stroke commits the live pixels (#4019)
Releasing a Clone Stamp or History Brush stroke rendered the whole
stroke again, although the live stroke had already drawn exactly those
pixels. The canvas now offers the live stroke to its commit as the Brush
does (#2131), through the same LiveCommit: a commit with the same
command, document revision, params and points takes the live surface
and reports fromLive; anything else renders as before.

LiveRetouch renders only the position and pressure of its points, which
is all the commands parse, and the offer is refused when the brush, layer
or clone source changed during the stroke (the revision doesn't cover
them). The Healing Brush, which blends on release, is left out.
2026-10-12 20:05:09 -04:00
13657f35f2 Docs: refresh target-app-parity and file-format-parity against main (#4417) (#4418)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 20:04:19 -04:00
af9316b97e Docs: refresh gaps.md against main (#4412) (#4413)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 20:03:29 -04:00
skylartaylor 3702555aa9 Make native RGB color policy explicit and preserve embedded profiles 2026-10-13 02:02:13 +02:00
4a53fad835 feat(codecs): add XPM2 plain-text image import support (#4264)
* 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>
2026-10-12 20:01:35 -04:00
5981a02e64 feat(codecs): support full X11 rgb.txt color names in XPM (#4263)
* 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>
2026-10-12 19:59:36 -04:00
David MannandClaude Opus 5.5 9970cf7214 Brush resize HUD: horizontal is size and vertical is hardness in a rotated view (#4228) (#4229)
The Control+Alt / Alt+right-drag HUD measured the drag in document axes and
only corrected for View > Flip Horizontal, so at 90 degrees a horizontal drag
changed the hardness and at 180 degrees dragging right shrank the brush.
Map the anchor and pointer to screen with the active ViewXform and divide by
its zoom: identical numbers at rotation 0 (flipped or not), screen-relative
when rotated, as in Photoshop. Without a view the old behaviour remains.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:59:11 -04:00
David MannandClaude Opus 5.5 f52549571f Rotate View: the image follows the pointer in a flipped view (#4226) (#4227)
ViewXform::to_screen mirrors x after rotating, so with View > Flip
Horizontal on a larger rotation turns the image counter-clockwise on
screen. The Rotate View drag and the trackpad rotate added their screen
delta unchanged, so the image spun against the pointer. A small helper,
rotate_view::screen_delta, negates the delta in a flipped view and serves
both paths; Shift still snaps the final angle.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:58:57 -04:00
David MannandClaude Opus 5.5 4566f5f55f Shape tools: the click-to-open-dialog tolerance is the same on HiDPI displays (#4205) (#4207)
finish_gesture passed the device-pixel zoom (current_zoom) to
shape_dialog::is_click, whose 3-point slop is in egui points, so at
pixels_per_point 2 the tolerance halved and a slightly jittery click drew
a tiny shape instead of opening Create Rectangle/Ellipse/... Pass
point_zoom(), as the marquee already does.

Regression test: a 2 pt jitter at ppp 2 and 100 % opens the dialog and
adds no layer.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:58:43 -04:00
David MannandClaude Opus 5.5 f60202879d Exposure: the slider keeps fractional stops (#4164) (#4165)
The shared adjustment slider rows rounded to 0.01 for ranges of 10 or
less and to whole numbers otherwise, so Exposure (-20..20) snapped to
whole stops: 0.5 became 1. Each row can now carry its own decimals:
Exposure keeps 2 and Offset 4, as in Photoshop, and their fields show
that many places. All other rows (Gamma, Posterize, Threshold,
Brightness/Contrast, Vibrance, Hue/Saturation ranges, Photo Filter
density) keep the old rule.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:58:30 -04:00
David MannandClaude Opus 5.5 5ce608e5f5 Image Size and Canvas Size: no silent resize of large documents in physical units (#4162) (#4163)
The Width/Height fields capped non-pixel units at 30000. egui's DragValue
clamps the shown value to its range when drawn, and number_edit reports that
as an edit, so a 40000 px document at 72 ppi shown in points opened as
30000 pt and OK resized (Image Size) or cropped (Canvas Size) it with no
edit. The bound is now the engine's 300000 px limit converted into the
current unit, never below 30000.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:58:16 -04:00
Vikram Bhaskaran bfa63f77b4 Snap 1 px pencil clicks to the pixel the aliased tip paints. (#4140)
A shift-line between two clicks inside the same pixels changed with the sub-pixel position. Pencil and pencil-mode Eraser clicks now use the same grid centre as the dab.

Fixes #4071
2026-10-12 19:58:03 -04:00
Vikram Bhaskaran 4709af046b Draw the Free Transform rotate arrow instead of hiding the pointer. (#4139)
The rotate zone requested CursorIcon::None, which removes the system cursor. Crop already draws a turn arrow for that case. Free Transform now uses the same arrow.

Fixes #4050
2026-10-12 19:57:49 -04:00
Vikram Bhaskaran 9fd20de408 Keep Liquify undo and redo working while a numeric field is focused. (#4134)
Ctrl+Z was ignored whenever a slider's text edit had focus, so a stroke could not be undone from the keyboard. Other Liquify keys still stay out of that field. The bracket keys also step the brush by at least one pixel, so a 2 px brush can reach 1 px.

Fixes #4027
2026-10-12 19:57:34 -04:00
David MannandClaude Opus 5.5 d323d872b2 Layers panel: double-clicking an effect row opens that layer's Layer Style (#4157) (#4131)
Root cause: the effect rows' double-click handler called
layer_style::open, which always reads the active layer. Double-clicking
an effect row of a layer that wasn't active opened (and on OK edited)
the active layer's style instead.

Fix: new layer_style::open_for_layer selects the row's layer with
layer.select first, as the row's right-click menu does, then opens the
dialog. Regression test covers two layers with only the inactive one
carrying an effect.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:56:50 -04:00
David MannandClaude Opus 5.5 b26bab69ee Layer Style: OK keeps an Inner Glow's Center source (#4152) (#4120)
The dialog's values_of() started each Inner Glow from the factory
defaults ("source": "edge") and never loaded the effect's own source,
so a Center-source glow (from a command or a PSD) showed Edge, previewed
as Edge and became Edge on OK, since the engine's overlay applies the
"source" key it receives.

values_of() now loads the source for Inner Glow. Regression test:
inner_glow_center_source_survives_ok.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:56:47 -04:00
SckraTandClaude 1e9f1ae486 perf(engine): read selection coverage per row when mixing edits back (#4424)
Destructive adjustments (pixels::adjusted) and the RGBA filter helper (adjust_cmds
process_surface) each had the same loop mixing the edited pixels back over the original by
selection coverage, looking the selection's tile up for every pixel (sample_channel, a
BTreeMap lookup per pixel). They now share pixels::mix_through_selection, which reads the
coverage one row at a time with sample_row_strided: one tile lookup per tile per row, and a
row-sized buffer.

Results are unchanged: a new test compares the helper with the per-pixel sample_channel mix,
bit for bit, for 8-bit, 8-bit-with-alpha and 32-bit selections over 8- and 16-bit layers,
across tile edges and untouched (default) selection tiles.


Claude-Session: https://claude.ai/code/session_01QUda3K786uop8kvEQoUjSf

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 19:55:21 -04:00
SckraTandClaude e683108613 perf(compose): read PHOTOCRAFT_FX_NOCACHE once (#4423)
effect_maps looked the environment variable up (taking the process environment lock) for
every effect-map lookup, i.e. per styled layer per render tile. It is read once, like
PHOTOCRAFT_FX_TRACE in the GPU compositor.


Claude-Session: https://claude.ai/code/session_01QUda3K786uop8kvEQoUjSf

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 19:55:13 -04:00
SckraTandClaude 3c469bfd05 perf(engine): move layers without a second document clone (#4422)
move_layers cloned the document on every layer.translate, Align and Distribute only to read
the pre-move state while type and shapes re-render. Session::edit already clones the
document for the edit, and the callers hold the starting state (an Arc), so they pass it in.

The clone copies the layer tree and every surface's tile map (pixel tiles themselves are
shared), so its cost grows with the layer and tile count: a nudge in a document with many
layers or very large layers goes from two such clones per command to one.


Claude-Session: https://claude.ai/code/session_01QUda3K786uop8kvEQoUjSf

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 19:55:05 -04:00
SckraTandClaude be43d58f1a perf(compose): stop sweeping the bounds cache on every call once it is large (#4415)
compose::bounds caches per-tile content bounds and drops entries of dead tiles when it holds
more than 65,536 of them. Once more than 65,536 tiles were alive (a large document plus its
history), every call (every render tile, every GPU plan) walked the whole map again and kept
it all. The sweep now runs past a high-water mark: twice the entries that survived the last
sweep, and at least 65,536.

The per-tile scan moves to raster as Tile::content_bounds, used by both
Surface::content_bounds and the cache instead of two copies of the loop; an empty pixel
yields no content instead of a chunks_exact(0) panic.


Claude-Session: https://claude.ai/code/session_01QUda3K786uop8kvEQoUjSf

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 19:54:57 -04:00
SckraTandClaude 97448b27f3 perf(raster): moved fills keep sharing one copy-on-write tile (#4414)
Surface::translated copied every target tile of a move that isn't a whole number of tiles,
so a solid fill (one shared tile) became one unique tile per coordinate after its first
nudge: each later move, undo and refresh then rescanned and re-uploaded all of them, and
history kept them all.

A target tile whose window reads only tiles holding one single pixel value (each source
tile is scanned once per call; one tile is kept per value) now shares that tile. Fully
covered targets also skip the default-pixel pre-fill they are about to overwrite.

12 MP RGBA fill nudged by +-(10, 7) twenty times (release, 4-core container): unique tiles
192 -> 76; RGBA16 move p50 55 -> 21 ms; RGBA8 unchanged within noise. Pixels are unchanged
(the test compares every depth with the copied pixels, and checks that a shared but
non-uniform tile is still copied).


Claude-Session: https://claude.ai/code/session_01QUda3K786uop8kvEQoUjSf

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 19:54:50 -04:00
Valentin KleinandClaude Opus 5.5 dc8a3f0b35 Paint Bucket and Edit > Stroke: a CMYK foreground writes the picked inks (#4351) (#4352)
* Color Picker: CMYK foreground fills a CMYK document with the picked inks (#4196)

The tool colours are stored as sRGB, so a colour picked as CMYK 100/0/0/0
(outside sRGB) came back as 100/28/0/17 when a CMYK document was filled.
tools.setColors now takes foregroundCmyk/backgroundCmyk, the picker sends
the entered inks, and Edit > Fill (and the fill keys) write them as they
are into a CMYK target. Fill also enters the document's CMYK profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Brush and Pencil paint a CMYK foreground with the picked inks in a CMYK document (#4344)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Paint Bucket and Edit > Stroke: a CMYK foreground writes the picked inks (#4351)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:54:16 -04:00
skylartaylor 4a730912c0 Preserve PDF text and CMYK paths and reject native layout mismatches 2026-10-13 01:53:16 +02:00
skylartaylor 9f7f8b7a7b Check that driver copy fields remain active after validation 2026-10-13 01:51:12 +02:00
skylartaylor 3c4e1536a0 Merge current PhotoCraft and preserve asynchronous native printing 2026-10-13 01:48:54 +02:00
Valentin KleinandClaude Opus 5.5 46740ad4bc Brush: CMYK foreground paints the picked inks into a CMYK document (#4344) (#4347)
* Color Picker: CMYK foreground fills a CMYK document with the picked inks (#4196)

The tool colours are stored as sRGB, so a colour picked as CMYK 100/0/0/0
(outside sRGB) came back as 100/28/0/17 when a CMYK document was filled.
tools.setColors now takes foregroundCmyk/backgroundCmyk, the picker sends
the entered inks, and Edit > Fill (and the fill keys) write them as they
are into a CMYK target. Fill also enters the document's CMYK profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Brush and Pencil paint a CMYK foreground with the picked inks in a CMYK document (#4344)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:47:14 -04:00
f48a72cbe3 Codecs: open and save DPX frames (#1049) (#4104)
Read SMPTE 268M DPX in both byte orders (SDPX, XPDS): the first image
element, descriptor 6 (luma), 50 (RGB) or 51 (RGBA), at 8, 10, 12 or 16
bits, with 10-bit packing 0, 1 (method A) or 2 (method B) and 12-bit
packing 0 or 1. 10- and 12-bit code values open as U16
round(c * 65535 / (2^b - 1)) and come back exactly when written at their
depth. No colour transform: the transfer characteristic and colorimetric
specification are reported as import notes; extra image elements are
reported; the orientation is applied on import.

Write big-endian, one unencoded element, at 8, 10, 12 or 16 bits
(EncodeOptions::dpx_bits; `dpxBits` on the save commands, MCP and RPC;
`--dpx-bits` in the CLI), following the document depth by default. Gray
with alpha is written as luma. Creator, project, copyright, creation
time, transfer, colorimetric, frame position, frame rate and time code
travel as DPX:* text metadata and are written back.

Unsupported descriptors, signed or run-length-encoded data and other bit
depths return CodecError::Unsupported; malformed and truncated files
return errors.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:44:04 -04:00
Valentin KleinandClaude Opus 5.5 3ba0e9419c Color Picker: CMYK foreground fills a CMYK document with the picked inks (#4196) (#4339)
The tool colours are stored as sRGB, so a colour picked as CMYK 100/0/0/0
(outside sRGB) came back as 100/28/0/17 when a CMYK document was filled.
tools.setColors now takes foregroundCmyk/backgroundCmyk, the picker sends
the entered inks, and Edit > Fill (and the fill keys) write them as they
are into a CMYK target. Fill also enters the document's CMYK profile.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:39:55 -04:00
7366a0b429 Italian: native-speaker review of the UI catalog (#4290)
* Italian: native-speaker review of the UI catalog

Fix mistranslations: Trap (Trapping, was overprint), HDR Toning (Mappatura tonale, was chemical toning), Display Text (Mostra testo), Vine Color, Override Action "Save" Commands. Improve phrasing: Bevel & Emboss, East Asian / Middle Eastern Features, linked smart objects, Conditional Mode Change (metodo, as in Image > Mode), bit depth labels. Record the review and new glossary terms in the localization docs. Credit lucAmbr0 in people.toml.

* Tests: allow Italian's compact 8/16 bit labels, which match the source

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>
2026-10-12 19:38:55 -04:00
Bihacsy LászlóandBrandon Thomas 992d945da7 i18n(hu): informal address in the rows added on merge (#4334)
* i18n(hu): informal address in the rows added on merge

Seven of the 23 rows translated when #3831 was merged addressed the
user with the formal verb forms; the catalog uses the informal second
person throughout. One of them also glued a hyphen to the {key}
placeholder; it now reads "{key}+kattintás: …" like the other hints.

* i18n(hu): translate the Enable animations preference

The Interface › Enable animations preference (#3273) reached main after
#3831; preference_labels_are_translated needs a Hungarian row for it.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 19:32:00 -04:00
Marco Arnulfo a1fbfd9a6c Filters on a type layer: ask to rasterize or convert to a Smart Object instead of greying out (#4333)
* 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)
2026-10-12 19:30:58 -04:00
9f98df582a Stylus: a light Windows pen tap is pressure 0, not a full-pressure click (#4072)
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>
2026-10-12 19:27:14 -04:00
David MannandClaude Opus 5.5 6c0dcf428b Color Table: keep the transparent index inside the palette (#4186) (#4187)
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>
2026-10-12 19:24:17 -04:00
61c52c08db Brush Settings: toggle switch rows without losing smoothing settings (#4463)
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>
2026-10-12 19:23:17 -04:00
d303bf9b83 Brush cursor: the tip's centre mark follows Show Crosshair in Brush Tip (#4105)
* 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>
2026-10-12 19:22:05 -04:00
ae72084a9c Resolve every pattern name before a batch delete removes any of them. (#4132)
* 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>
2026-10-12 19:19:23 -04:00
AlonandClaude Opus 5.5 9d37e1518b fix(move): no trail when dragging a shape past the canvas edge (#3946) (#4069)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:17:36 -04:00
Nigel Ihza Firdausy 5c4ad593de fix(ui): reject rotation angles outside f32 range (#4044) 2026-10-12 19:17:31 -04:00
Nigel Ihza Firdausy fd4ba52f79 fix(ui): cancel unfinished polygon on lasso tool switch (#4021) 2026-10-12 19:17:25 -04:00
67c90e307f Quantize the colour field when Only Web Colors is on. (#4138)
* 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>
2026-10-12 19:12:59 -04:00
Fabio Lissi 90c42f5555 Map Place, Open, Image Size in percent, the Paint Bucket and Insert Conditional (#219) (#4470)
- 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'.
2026-10-12 19:12:35 -04:00
3f0a633056 Make channel filter previews match the committed edit (#3979)
* Fix selected-channel filter previews and reduced Quick Mask proxies

Co-authored-by: Codex <noreply@openai.com>

* Keep the new dialog preview aligned with the selected channel

* Bound full-resolution dialog preview textures to the device limit

---------

Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:05:49 -04:00
e2a35b04fd Windows: keep full screen and maximized apart; never restore full screen (#4396)
* 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>
2026-10-12 19:05:37 -04:00
Fabio Lissi 13f5c104f6 Map the remaining frequent Photoshop action events (#219) (#4465)
- 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.
2026-10-12 19:02:15 -04:00
FredApps 7015f2916d View: Esc leaves full screen modes (part of #4116) (#4145)
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.
2026-10-12 19:01:24 -04:00
bbfaefd5f0 File Handling: wire Maximize PSD and PSB File Compatibility (#204) (#3955)
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>
2026-10-12 19:00:03 -04:00
4ff2dc33ca Fix multi-layer Delete and Move drags on a locked Background (#4109)
* 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: #4078 fixes #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>
2026-10-12 18:56:58 -04:00
Marco Arnulfo cd36d1ea0d Delete with several layers selected deletes them, not the active layer's targeted mask (#4025) (#4078) 2026-10-12 18:52:43 -04:00
Fabio Lissi 9862dfbb09 Actions panel: set folders, dialog toggle, Insert Stop (#219) (#4456)
* 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).
2026-10-12 18:51:53 -04:00
Marco Arnulfo 40427f25dc Properties: a new adjustment or fill layer brings Properties to the front (#4028) (#4079) 2026-10-12 18:47:59 -04:00