Commit Graph
1518 Commits
Author SHA1 Message Date
a97cfd103e Fix Camera Raw preview color management (#3427)
* Fix Camera Raw preview color management

* Apply canvas color management to refined previews

* Camera Raw detail preview: clippy and rustfmt

allow too_many_arguments on DetailPreview::paint (the display joins seven
existing params) and as_chunks for the RGBA8 rows (clippy 1.99).

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-11 16:47:56 -04:00
88ffd89e9f fix(crop): unlock Background when extending canvas (#3081) (#3158)
* fix(crop): unlock Background when extending canvas (#3081)

* Crop: fill an extended Background with the background colour

Photoshop's Crop tool keeps the Background when the frame extends past
the canvas and fills the added area with the background colour, as an
opaque Canvas Size extension does. Do that instead of converting the
Background to a normal layer; other layers still get transparent pixels.

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-11 16:46:09 -04:00
Valentin KleinandClaude Opus 5.5 62e0335116 Transform Path: Distort, Perspective and Warp apply to paths (#3027) (#3032)
* Transform: Distort, Perspective and Warp keep shape layers live (#3009)

Edit › Transform › Distort / Perspective and Warp on a shape layer let the
user edit the box or mesh and then refused at commit ("needs a rasterized
shape"). Photoshop maps the shape's path and keeps it a shape.

- photocraft-vector: new `map` module. `map_points` sends every anchor and
  control point through a map (Distort / Perspective: exact for straight
  segments, exact end tangents for curves). `map_path` follows a curved map
  (Warp), halving each segment until the mapped control points follow the
  mapped curve within a tolerance (at most 64 pieces a segment, bounded in
  total); straight segments whose image is straight stay straight.
- engine: `transform_layer` and `warp_layer` map a shape's path instead of
  refusing; a live rectangle/ellipse/polygon/line becomes a plain path. A
  linked vector mask now follows Distort / Perspective / Warp too. A map that
  sends a point to infinity is an error, not a corrupt path.

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

* Transform Path: Distort, Perspective and Warp apply to paths (#3027)

path.transform accepted only an affine matrix, so the Free Transform
Path commit refused any box pulled out of a parallelogram ("Distort and
Perspective can't be applied to a path"), and Warp was not offered on a
path at all. path.transform now also takes a `quad` (with an optional
`rect` frame, default the path's control bounds) and a warp (`style`,
`mesh` or `warp`, as edit.transform.warp), validated (finite numbers,
magnitude at most 1e6, non-degenerate quad) and mapped with the
vector::map helpers from #3025. A parallelogram quad stays affine, so a
live shape stays live. The UI commit sends the quad or warp instead of
refusing, and Transform Path offers Warp; each is one undo step.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:43:13 -04:00
rafi612 014dd5f25d fix(ui): drop doubled options-bar borders reintroduced in UI.9 (#2226) (#3425) 2026-10-11 16:40:36 -04:00
f155374ac3 fix(type): navigate adjacent line with large leading (#3066) (#3141)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:38:14 -04:00
76c02862c6 Toolbox: make the colour swap arrow clearer (#3416)
* Toolbox: bolder Switch Colors arrow with filled heads

* Toolbox: match the reference Switch Colors glyph

A bold diagonal curve from the icon's top-right to its bottom-left with
filled heads, measured from the reference glyph, replacing the quarter
arc. Geometry extracted to swap_arrow.rs with unit tests.

* Toolbox: correct the swap glyph reference and arrow directions

* Fix formatting and Clippy checks for swap glyph

* swap_arrow: drop the test's env-var PNG dump

Tests shouldn't read env vars to write files; the render assertions stay.

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-11 16:32:18 -04:00
Sir Starch b66ebb84d2 fix(type): count normalized insert text caret (#3074) (#3138) 2026-10-11 16:31:51 -04:00
c234865b17 fix(type): respect Lock All on text edits (#3092) (#3132)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:26:10 -04:00
7aba2bd305 Type: scan system fonts off the UI thread (#3021) (#3030)
* Type: scan system fonts off the UI thread (#3021)

The first `photocraft_text::shared()` call scans every installed font, and
nothing made that call before the Type tool's options bar drew its font
menu, so the scan ran inside a UI frame (over 60 s on some Macs, #2518).

- photocraft-text: `warm_shared()` starts the scan on a background thread;
  `try_shared()` returns the engine without blocking (None while the scan
  runs, started there if nobody warmed it; built in place on the web, in
  tests, if the thread can't start or the worker died). `shared()` keeps
  its blocking behaviour for engine commands.
- The desktop app warms the fonts at start-up.
- Font menus that draw every frame (options bar, Character, Glyphs and the
  Character/Paragraph Styles family menus) show a disabled "Loading fonts…"
  box and repaint until the scan is done. "Loading fonts…" is in all 16
  catalogs.

Complements #2644 (CoreText discovery on macOS): this changes where the
scan runs, not how fonts are discovered.

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

* Type: axis sliders wait for the background font scan too

#2996 (landed on main) added axis_sliders, which called the blocking
photocraft_text::shared() every frame; use try_shared() so the Character
section doesn't stall while the scan runs.

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-11 16:25:57 -04:00
buivanvu94 3e45d769fa fix(ui): focus and select hex field when color picker opens (#3375) (#3408) 2026-10-11 16:21:57 -04:00
Sir Starch a893113502 fix(gradient): respect Lock All on Gradient Fills (#3093) (#3120) 2026-10-11 16:19:40 -04:00
Valentin KleinandClaude Opus 5.5 8458cf7095 Transform: Distort, Perspective and Warp keep shape layers live (#3009) (#3025)
Edit › Transform › Distort / Perspective and Warp on a shape layer let the
user edit the box or mesh and then refused at commit ("needs a rasterized
shape"). Photoshop maps the shape's path and keeps it a shape.

- photocraft-vector: new `map` module. `map_points` sends every anchor and
  control point through a map (Distort / Perspective: exact for straight
  segments, exact end tangents for curves). `map_path` follows a curved map
  (Warp), halving each segment until the mapped control points follow the
  mapped curve within a tolerance (at most 64 pieces a segment, bounded in
  total); straight segments whose image is straight stay straight.
- engine: `transform_layer` and `warp_layer` map a shape's path instead of
  refusing; a live rectangle/ellipse/polygon/line becomes a plain path. A
  linked vector mask now follows Distort / Perspective / Warp too. A map that
  sends a point to infinity is an error, not a corrupt path.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:16:04 -04:00
Sir Starch f435eb0b55 fix(paint): respect Lock All on layer masks (#3094) (#3111) 2026-10-11 16:15:08 -04:00
b3993e89b3 Linux: say so when no file dialog can open (#3354); read ldconfig on non-English systems (#3397)
* feat(linux): say so when no file dialog can open (#3354)

File › Open / Save / Export did nothing on a minimal Wayland session
(labwc, Void) and said nothing: rfd loads libdbus-1 for the XDG file
chooser portal and falls back to zenity, and when neither works it only
logs and answers "nothing picked", which the app took for Cancel.

A background probe now looks once for the pieces (libdbus in the linker
cache, the portal's FileChooser interface on the session bus, zenity on
PATH). When a dialog comes back empty and none of them is there, the
shell answers with the new FileDialogAnswer::Unavailable and the status
bar tells the user what to install. The probe only explains an empty
answer; it never blocks a dialog, and anything it can't tell counts as
present.

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

* fix(linux): read the ldconfig cache on non-English systems

The startup library check and the new file dialog probe parse
`ldconfig -p`, but accepted the listing only when its first line
contained "libs found in cache". On a German system the header is
"5522 Bibliotheken im Cache ... gefunden", so the cache was treated as
unreadable and the check silently fell back to a directory search.

Run ldconfig with LC_ALL=C, and accept any listing that starts with a
count. Test: a translated header is a valid listing.

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

* file_dialog_probe: no dead-code errors off Linux

Findings, problem and on_path are only used by the Linux probe (and the
tests); the empty non-Linux start() was never called. Windows/macOS
clippy -D warnings failed on them.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 16:14:47 -04:00
5f55a29d2b Color: honour the EXIF sRGB tag on open; wire Ignore EXIF Profile Tag (#204) (#3026)
A file with no embedded ICC profile whose EXIF ColorSpace tag says sRGB
(what cameras write) now opens as sRGB, as in Photoshop: it is tagged
sRGB before the Color Settings policy runs, so it is kept, reported as a
mismatch, or converted like any embedded profile. Before, it was read as
untagged and took the working space, so with a wide working space such
as Display P3 its colours were reinterpreted.

Preferences › File Handling › Ignore EXIF Profile Tag (off by default)
restores the untagged reading, and is now shown in the dialog.

photocraft_codecs::exif_color_space reads the tag from the Exif sub-IFD,
bounds-checked; anything malformed reads as no tag. Settings that do
nothing: 43 → 42 of 150.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 16:07:03 -04:00
56b192453e i18n: translate the Select and Mask dialog labels (#3014) (#3015)
Smart Radius, Shift Edge, Decontaminate, Output and the Refine Edge
title had no catalog rows, so the Select and Mask dialog stayed partly
English in every language; Greek and Polish also missed Modify's
Apply At Canvas Bounds. A new test checks every generated select.*
dialog (params, choices, title) in all registered languages.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 16:01:04 -04:00
57cba5ef8e Window: a window saved while minimized reopens at the minimum size, not 160 x 64 (#3380)
On Windows, eframe saves a minimized window's size (160 pt wide, raised to
64 pt tall), and the minimum size isn't applied to a restored window, so
PhotoCraft reopened as a 160 x 64 strip on every launch. A window_builder
hook grows a restored window to the minimum size, keeping its centre.

Co-authored-by: Katt <238968166+KattKnight@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:00:11 -04:00
b1900ace28 Installed ICC profiles in the profile menus, and Mirror in Print (#3011)
* Installed ICC profiles in the profile menus, and Mirror in Print

- photocraft_engine::installed_profiles: scans the OS colour profile
  folders (Windows, macOS, Linux), bounded and cached; new
  edit.installedProfiles command.
- Print's Printer Profile and the Proof Setup, Convert to Profile and
  Assign Profile dialogs list the installed profiles after the
  built-ins; a profile given by path that isn't listed shows by name.
- file.print emulsionDown mirrors the page (PDF and preview), with a
  Functions › Emulsion Down (mirror) checkbox.
- Print dialog: cm/mm/in, Height and Width fields, Print Resolution.

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

* Print dialog: translate Emulsion Down, Functions, Print Resolution in every catalog; cargo fmt

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-11 15:55:11 -04:00
bbb05cabfb fix(shapes): options bar Fill/Stroke edit the selected shape (#3347) (#3370)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 15:54:38 -04:00
539bf7ba14 Color panel: HSB on sRGB values, like Photoshop (#3323) (#3327)
The Color panel converted between the foreground and its hue, saturation and
brightness with egui's `Hsva`, which works in linear light. So a 50% grey
(128) read as Brightness 22%, and the colour field's left edge halfway down
was drawn as 188, not 128. Photoshop's HSB, and the Color Picker dialog's,
work on the sRGB-encoded values.

`srgb_hsva` / `hsva_srgb` now use the Color Picker's `rgb_to_hsv` /
`hsv_to_rgb`, and the field, hue strip, HSB slider tracks and swatch draw
through a new `hsva_color32` instead of egui's linear `Color32::from(Hsva)`.
`Hsva` only holds the numbers now.

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:45:50 -04:00
Valentin KleinandClaude Opus 5.5 d9445c7d1d Free Transform: ⌘-drag of a corner scales editable type instead of distorting it (#3010) (#3012)
Distort and Perspective are greyed for editable type (#2701) and the engine
refuses them at commit, but the box's ⌘ / ⌘⌥⇧ corner drags didn't look at the
layer, so the box distorted and the commit then failed. On a box over live type
those drags now act as a plain corner drag (⌥ and ⇧ keep their meaning).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:42:43 -04:00
4ecf8ea6b9 feat(ui): Motion Blur and other filter angles get the angle dial (#3152) (#3407)
Move Drop Shadow's angle dial out of layer_style.rs into a shared
widgets::angle_dial_row that takes the param's range and uses the
dialogs' value_field. The field keeps Motion Blur's -360..360, and it
takes the dialog's opening focus. The filter dialog uses the dial for
direction angles that count counter-clockwise from three o'clock:
Motion Blur, Emboss, Oil Paint, Lighting Effects, Relight and Global
Light. Rotation amounts (Twirl, Rotate Arbitrary, Crop, Lens Correction,
Blur Gallery) and Flame's up-zero angle keep their sliders.

Arrow keys on the focused dial wrap round only when the range is the
dial's own -180..180 (Drop Shadow); on a wider range they step on, so
Motion Blur's 270° goes to 271°, not -89°.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:38:26 -04:00
67747dfd02 Type: Warp on live type takes the warp styles and stays editable, no prompt (#3004) (#3008)
* Type: ask to rasterize before Warp on live type (#3004)

Edit › Transform › Warp (the menu, or the warp toggle of an open transform
box) on editable type opened Warp mode, let the user shape the mesh, then
failed on commit with "Warp needs rasterized type". It now opens Photoshop's
"This type layer must be rasterized before proceeding…" prompt, the one the
pixel tools already use. OK applies an open transform box, rasterizes the
type (its own history state) and opens Warp on the pixels; Cancel does
nothing. The prompt's continuation is a menu command from an allow-list,
run with the egui context right after the confirm (control channel, OK
button) or on the next frame.

Distort and Perspective stay greyed for type (#2701), Puppet Warp and
Perspective Warp stay greyed for non-pixel layers, as in Photoshop.

Scorecard TYPE-218-9 -> done.

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

* Type: Warp on live type takes the warp styles, no rasterize prompt (#3004)

Photoshop 2026 keeps Edit › Transform › Warp enabled on live type and asks
nothing: the mesh can't be dragged, only the options bar's warp styles
(Warp Text's) apply, and the type stays editable. The earlier version of
this change asked to rasterize first; that is gone (and with it the prompt
continuation, which nothing else used).

Warp mode on a type layer now starts from the layer's own Warp Text style
over the unwarped type, ignores mesh drags and splits (Split, Grid and the
Custom style are greyed/hidden, the split menu items are greyed), and the
commit runs edit.transform.warp, which on type sets the Warp Text: one
history step, recordable, the layer stays Text. None takes a warp off. A
box scaled or rotated before toggling Warp is applied first.

Scorecard TYPE-218-9 note updated: Photoshop doesn't prompt for Warp;
Distort and Perspective stay greyed (#2701).

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-11 15:35:47 -04:00
Valentin KleinandClaude Opus 5.5 bd3be48785 Pen: Alt breaks a handle while drawing, and close-drag pulls out the first anchor's handles (#3306) (#3314)
Alt during a drag unlinks the handle pair (only the outgoing one keeps following),
Alt-press on the last anchor's outgoing handle turns that handle alone, and pressing
the first anchor now closes on release, so a drag pulls out that anchor's handles
(a stand-in knot folded into the first one on commit). A plain click still closes
without touching the first anchor's handles. Still one History step per path.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:29:33 -04:00
KnottyDyes 2d6ff3f6cc Inspect: report each layer's vector mask (#2980)
`hasMask` in document inspection covers only raster masks, so a layer
with a vector mask looked unmasked to an agent. Layers, groups included,
now also report `hasVectorMask` and `vectorMaskEnabled` (null when there
is no vector mask). `hasMask` keeps its meaning.
2026-10-11 15:21:03 -04:00
28874bbfad Select and Mask: Output To New Document, with or without a layer mask (#2998) (#3007)
* Select and Mask: Output To New Document, with or without a layer mask (#2998)

select.refineEdge takes output newDocument / newDocumentWithMask: a new
document like the source (size, mode, depth, resolution, profile) with
the active layer cut out by the refined edge, or with the matte as a
layer mask. The source document is left untouched. The dialog's Output
choice lists all six targets, translated in every catalog.

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

* i18n: drop the duplicate pt-br New Layer row (main already has it)

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-11 15:16:45 -04:00
8a72978549 File Handling: wire Image Previews to embed a PSD thumbnail (#204) (#2971)
* File Handling: wire Image Previews to embed a PSD thumbnail (#204)

Preferences > File Handling > Image Previews is live for PSD/PSB saves. With
Always Save (the default) and Ask When Saving, the file carries Photoshop's
thumbnail image resource (1036: the 28-byte kJpegRGB header and a JFIF stream,
at most 160 px on the long side); Never Save writes none. The thumbnail is the
reduced composite converted to sRGB through the document's profile with
photocraft-cms (8/16/32-bit; Gray, CMYK and Lab become RGB), over white where
transparent. Ask When Saving has no Save As checkbox yet, so it saves the
preview, as Photoshop's checkbox starts on.

The preference reaches Save, Save As, Save a Copy and automation saves in the
app (ExportSettings.image_previews), and the engine's headless saves
(file.saveACopy, batch, Layers/Comps/Artboards to Files) through SaveOpts.
photocraft_io::ExportOptions gains image_previews (default on).

Settings that do nothing: 32 of 153 (#204).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* File Handling: rustfmt the Image Previews save path

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 15:14:49 -04:00
f92f13490b Fix/3144 taskbar maximized (#3308)
* Window frame: trim a caption-less maximize clear of the taskbar (#3144)

* Credit axlecoffee in the About window

* rustfmt

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-11 15:08:18 -04:00
58406c38da Cycle Color Lookup LUTs with keyboard and wheel (#3304)
* Preview Color Lookup LUT selection while cycling

* Test Color Lookup keyboard and wheel cycling

* rustfmt; drop a useless into_iter clippy flags

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-11 14:57:43 -04:00
Valentin KleinandClaude Opus 5.5 b54fdfd94f Tests: panic_hunt reruns a timed-out command before calling it hung (#2994) (#2997)
The first type.create starts the shared text engine, which scans every
system font once (3.5-5 s under load). panic_hunt billed that to
type.create against its 4 s deadline and failed intermittently. A real
hang reproduces, one-time setup does not: a timeout now counts only if a
rerun on a fresh session times out too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:54:14 -04:00
Valentin KleinandClaude Opus 5.5 139df7ae85 Type: variable font axis sliders; optical size follows the type size (#2985) (#2996)
The Character section (Properties and Window > Character) shows one slider
per axis of the style's variable font below the font style (Weight, Width,
Slant, Italic, or the font's own tag), setting type.setStyle variations; a
drag is one history step. Optical size has no slider: the layout now sets
opsz to the type size unless the style sets it, as CSS font-optical-sizing
auto. Picking a font style or weight resets the wght axis to it, so an
earlier slider value no longer overrides the style the dropdown shows.

Scorecard TYPE-218-4 -> done.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:46:26 -04:00
Dan HiornsandClaude Opus 5.5 bb5d26e052 Free Transform: right-click W or H to pick their unit (#3235) (#3301)
As in Photoshop, right-clicking the W or H field of the Free Transform
(and Free Transform Path) options bar opens a menu of units: Pixels,
Inches, Centimeters, Millimeters, Points, Picas and Percent, the
current one checked. W and H then show the box's size in that unit at
the document's resolution (1200 px at 300 ppi is 10.16 cm), and a
value typed in it scales the box to that length; Percent, the default,
is the scale as before. The pick is remembered with the dialog
settings (`dialogs["edit.freeTransform"].sizeUnit`).

The menu is the rulers' unit menu (`rulers::unit_menu`), the
conversion is the Properties panel's `LenUnit`, and the size limits and
the link toggle work on the scale as before. Length fields are wider
(84 pt) so "2116.67 mm" fits; Percent keeps its 66 pt.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:40:14 -04:00
76c7d723f4 Move tool: double-click type on the canvas to edit it (#1132) (#3300)
Since Photoshop CC 2019 a double-click on a type layer with the Move tool starts
editing its text without switching tools by hand. PhotoCraft only offered this
from the Layers panel thumbnail. A double-click with the selected Move tool now
picks the topmost visible type layer under the pointer (the Type tool's own hit
test), makes it active and opens an edit session with the caret on the glyph
there, switching to the Type tool. Elsewhere the double-click does nothing, ⌘
held over another tool doesn't count, and an open transform box keeps its
double-click (commit).

The type tests' harness steps a quarter second per frame, past egui's
double-click delay, so the new test uses a 60 fps harness.

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:28:05 -04:00
Valentin KleinandClaude Opus 5.5 65e8d5f020 GPU canvas: price the display textures before building them (#2934) (#3299)
The GPU canvas built a full-resolution, mipmapped texture set covering the
whole declared canvas (DocTextures::new) with only per-texture side limits,
so a document's size alone decided how much GPU memory was committed: a
300000x300000 canvas asks for hundreds of gigabytes.

canvas_texture_bytes prices the set in closed form (every tile and mip
level) and DocTextures::new refuses one over canvas_budget (a quarter of
RAM, at least MIN_GPU_BUDGET) without creating anything. refresh reports
"over-budget" and ensure_gpu then draws the document on the reduced CPU
canvas, so a supported large document still displays.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:16:27 -04:00
myangaa1 e5dbcab335 Open several files in name order, keeping it for background opens (#3248) (#3293)
Files opened together (File > Open with several files, paths on the
command line, the OS's "open these documents") appeared in random tab
order: each one opens in the background and its tab was added when it
finished decoding, so small files came first. The paths were also used
in the order the picker returned them.

`open_paths` now sorts the paths naturally (`File_2` before `File_10`,
case-insensitively) and opens them as a set: every background open
carries its position (`OpenOrder`), and when it finishes its document is
moved after the members before it that have already opened, so the set
reads in order however long each file takes. Members opened at once
(preset files) take their place immediately. The web build sorts the
picked files' contents the same way.

Tests: `natural_cmp`, a set opened synchronously, and a set whose
background opens finish out of order, mixed with a tab dropped at a
slot and an open from a forgotten set.
2026-10-11 13:51:46 -04:00
Gert Pellin - GPEandswitch87 e26442657f ui: Dust & Scratches previews at full resolution, as it applies (#3265) (#3287)
A 1/4 proxy averages away the specks the filter exists to remove, so the
preview showed an untouched image, at a scale that upscaled back to blocky
— while `radius` stayed in document pixels and covered k times the area it
covers in the result. OK always applied at full resolution, so only the
interactive feedback lied; add the filter to FULL_RESOLUTION_PREVIEW and
place its regression test on that path.

Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
2026-10-11 13:39:11 -04:00
Valentin KleinandClaude Opus 5.5 740dd3019f Rulers: stay level and fully ticked on a rotated view; guides turn with the image (#3233) (#3282)
* Rulers: stay level and fully ticked on a rotated view; guides turn with the image (#3233, #3277)

The rulers measured along the rotated document axes, so at most angles the
ticks were squeezed into a few pixels (none at 90°) and the rest of the
ruler was empty. They now measure with the view minus its rotation: same
zoom, flip and pixel aspect, same document point under the canvas centre.

Guides were drawn as screen-level lines through the screen position of
(pos, 0), so on a rotated view they no longer marked their document
coordinate while hit-testing still used it. They are now drawn along the
document line across the clip rect.

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

* Rulers: anchor a rotated view's rulers at the document origin (#3233)

Photoshop 2026 keeps the rulers level at zoom scale on a rotated view, with 0
at the screen position of the document origin (where the rotated image's
top-left corner lands), not at the unrotated frame through the canvas centre.
ruler_xform now re-centres the level mapping so the origin projects where the
rotated view draws it; tick spacing still follows zoom, flip and pixel aspect.

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

* Rulers: label distances before the origin without a minus sign (#3233)

Photoshop's rulers print the distance from the origin, so ticks left of /
above it read "1000 500 0 500 1000", not "-1000 -500 0". The label is now
the absolute value (extracted into `ruler_label`), for every view.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:27:41 -04:00
Gert Pellin - GPEandswitch87 0144cb283d ui: keep the Color panel knob in the corner it was dragged to (#3228) (#3275)
The SV field re-derives Hsva from the colour every frame, and black reads
back as s = 0 — so a drag that bottoms out into black drew the knob at the
bottom-left one frame later, although the colour it picked was right.

Remember what the field was showing (hue *and* saturation) and restore both
only when the colour cannot carry them: both for black, the hue alone for a
grey, whose saturation is genuinely zero.

Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
2026-10-11 13:16:02 -04:00
a35af4e279 Add FITS read/write (astronomy images) (#2956)
* Add FITS read/write codec

FITS (the astronomy format) opens and saves as a flat image: the primary
HDU or first IMAGE extension; gray, or RGB when NAXIS3 = 3; BITPIX 8/16/
32/64/-32/-64 with BSCALE/BZERO/BLANK; rows follow ROWORDER (bottom-up by
default). Camera U16 data stays exact; float data is normalised to 0..1.
Header cards round-trip as text metadata (HIERARCH for long names).
Tile-compressed (fpack) files are refused as unsupported.

Wired into File > Open, Save As and the batch commands as .fits/.fit/.fts.

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

* Test FITS open and save through photocraft-io

Gray 16-bit, RGB 8-bit and RGB 32-bit float documents round-trip exactly,
and header cards travel as the document's text metadata.

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

* FITS: hostile-header regression test (absurd axes, bad BITPIX, no END, HDU chains, overflowing sizes)

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-11 13:15:22 -04:00
f8af27d261 Prevent white flash when opening the screen color picker (#3256)
* Avoid white flash when opening screen color picker

* Normalize screen picker source line endings

* rustfmt; doc comment on viewport_builder

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-11 13:04:55 -04:00
Gert Pellin - GPEandswitch87 308ef63294 raw: charge the lossless-JPEG intermediate against max_alloc (#2925) (#3194)
Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
2026-10-11 12:52:41 -04:00
9441484d16 Quick Mask: an empty start and Select All stay apart (#2743) (#2922)
* Keep Quick Mask's empty start and Select All apart (#2743)

Quick Mask stored "no selection" as an all-white mask, the same as Select
All. With Color Indicates Selected Areas that white was coloured over the
whole canvas, and leaving Quick Mask turned any all-white mask (a real
Select All too) into no selection.

The document now records that Quick Mask was entered with no selection.
The empty mask is white for Masked Areas and black for Selected Areas, so
neither shows colour; switching the option on an untouched empty mask
keeps it empty. On exit only that untouched white start becomes no
selection, so Select All survives the round trip. The flag is saved with
the document.

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

* Quick Mask tests: pass the active layer to channel_view::render (new main signature)

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 12:51:18 -04:00
b814fd5241 Zoom and Hand tools: right-click the canvas for the view menu (#3242) (#3255)
Right-clicking the image with the Zoom or Hand tool opens Zoom In, Zoom Out,
Fit on Screen, Fill Screen, 100%, 200% and Print Size. Zoom In/Out step about
the right-clicked point (or centre it with Zoom Clicked Point to Center); the
other rows run their View menu command. Outside the image the pasteboard menu
still wins. The canvas keeps the view a menu row sets instead of writing its
frame copy back over it. Agents get the same menu through ui.pointer and
ui.context.choose.

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:50:01 -04:00
a7f26522ff i18n(pt-br): translate 782 missing UI, filter, and tool strings (#3254)
Co-authored-by: AlastorIRL <100581152+AlastorIRL@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:42:10 -04:00
Brennan StehlingandClaude Opus 5.5 2b19987bf1 Menus: a submenu stays open while the pointer heads for it (#1218) (#3251)
In the in-window menu bar (Windows, Linux, --in-window-menus), moving from a
submenu's row toward one of its lower items crosses the rows below. egui keeps
the submenu open only while the pointer moves in a straight line at it: a
bowed path or a pause on the way closed it, and the crossed rows lit up as if
chosen.

As in macOS and Photoshop menus, the submenu now stays open while the pointer
is inside the triangle from where it left the row to the submenu's near edge,
until it rests on another row for 0.3 s or moves away. Its row stays lit and
the crossed rows don't light up; when the submenu lets go, the row under the
pointer takes the highlight. Clicks, keys and leaving the triangle behave as
before.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 12:34:12 -04:00
Tanish Mankuandjerrykimo da5abd10f9 Native brush outline and crosshair cursors on macOS, X11 and Wayland (#1343) (#3243)
* Native brush outline and crosshair cursors on macOS, X11 and Wayland (#1343)

The brush outline and crosshairs were OS cursor bitmaps only on Windows; on
macOS and Linux the app painted them, so they trailed the pointer by the
render, present and compositor latency and jumped to catch up on slow frames.
tool_cursor now hands the OS the bitmap on every desktop root viewport.

macOS takes NSCursor images in logical points, so the bitmap scales with the
UI zoom only there; Windows, X11 and Wayland take physical pixels. Off Windows
bitmaps stay within 255 px (X servers and hardware cursor planes), and a
larger tip falls back to the painted outline plus a small OS crosshair.

The Pencil's grid-snapped square and the pipette stay painted, and the Hand
bitmap stays Windows-only (the other platforms draw their own hands).

Co-authored-by: jerrykimo <104250063+jerrykimo@users.noreply.github.com>

* Cursor test: the Hand is a bitmap only on Windows; elsewhere the OS draws Grab

---------

Co-authored-by: jerrykimo <104250063+jerrykimo@users.noreply.github.com>
2026-10-11 12:21:40 -04:00
Myosh1 3f0970923d Add built-in rainbow gradient presets (#2916)
Add a "Rainbows" group to the built-in Gradients panel with five presets: Rainbow,
Pastel Rainbow, Neon Rainbow, Oil Slick and Rainbow Fade. The three hue ramps sample 13
evenly spaced hues (red 0 deg to magenta 300 deg) so the transitions are smooth rather
than the seven coarse ROYGBIV points; Rainbow Fade reuses the classic ramp with
transparent ends. All colours are original (clean-room).

Covered by a new engine test, presets::tests::rainbow_gradient_presets.
2026-10-11 12:18:19 -04:00
28622aabfc Move dock tabs between groups individually (#3239)
* Report which dock tab leaves its strip

* Carry dragged dock tab identity

* Expose dock panel name normalization

* Move individual tabs across native dock groups

* Document cross-group dock tab drops

* Test individual tab drops between dock groups

* Resolve a dropped tab by its original index; rustfmt

The strip reports visible indices, so a reordered group moved the wrong
panel. Adds a regression test.

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-11 12:10:27 -04:00
David MannandClaude Opus 5.5 66c0b59a23 Layers: Combine Shapes merges the selected shape layers (#2400) (#2906)
Layer › Combine Shapes › Unite / Subtract Front Shape / Intersect Shape
Areas / Exclude Overlapping Shapes only changed the operations between the
components of the active layer. With two single-shape layers selected (the
report's case) the active layer had one component, so nothing happened.

With two or more shape layers selected and no `layer`/`subpath` param, the
commands now merge them into the top-most layer in one history step: the
bottom layer's components keep their operations and every component of the
layers above takes the chosen one, in stacking order, then the other layers
are removed. Merge Shape Components merges them the same way (each layer
combined) and bakes the result. Selected non-shape layers are left alone,
locked layers refuse the merge, and one selected layer behaves as before.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 12:05:57 -04:00
Valentin KleinandClaude Opus 5.5 c1bd296dcf Select and Mask: Refine Edge Brush (#2972) (#2989)
* Select and Mask: live preview in Photoshop's view modes (#1754)

The Select and Mask dialog had no preview of the refined selection. It now
previews live in Photoshop's view modes: Onion Skin (default), Marching
Ants, Overlay, On Black, On White, Black & White and On Layers, with a
per-view Opacity, Show Edge (J) and Show Original (P). F cycles the views,
O/M/V/A/T/K/Y pick one and holding X shows the image without a view.

The view state lives in the dialog's fields, so ui.dialog.set drives it.
The preview runs select.refineEdge on the proxy with output=selection and
builds the view from it (veil fill layers, a layer mask, or the refined
selection, whose marching ants the canvas draws scaled back up), through
the existing filter-preview worker.

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

* Select and Mask: Refine Edge Brush (#2972)

Select and Mask had no Refine Edge Brush. select.refineEdge now takes
refineStrokes: [{"points":[[x,y],...],"size":px,"mode":"add|subtract"}],
applied in painting order. Added areas become unknown and take a colour
estimate: each pixel's colour projected on the line between the mean known
foreground and background colours around it (normalised convolution over
a box of the brush size, four times as far where none are near), so hair
or fur beyond the edge-detection band joins the selection. Subtracted
areas keep the original edge. Malformed or oversized strokes are rejected
(stroke / point / swept-area limits), never a panic.

The dialog gets the brush: Refine Edge Brush (R), Mode (Expand Detection
Area / Restore Original Edge, Alt swaps), Size ([ and ]) and Clear Brush
Strokes. With it on, dragging on the canvas paints strokes into the
dialog's refineStrokes field, so the live preview, Show Edge and OK use
them, and ui.dialog.set / agents drive them like any other param.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:00:25 -04:00