Convert Point is a tool of its own, built on the existing path.convertPoint
edit that Pen + Alt already drives. A click makes a smooth anchor a corner, a
drag from an anchor pulls out smooth handles, a drag on a handle moves it
alone. The tool selects the anchor it converts so its handles show; a press on
empty canvas or a segment does nothing, and Cmd/Ctrl is Direct Selection, as
with the Pen. It shares the Pen's key (P).
Original glyph with an ATTRIBUTION.md row; tool name in all catalogs; README
tool list and the G6 / ui-parity / roadmap counts updated (tools 56 of 68, the
earlier 53 was stale).
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* GPU startup: skip wgpu indirect-call validation shaders (#3107)
wgpu compiles its indirect-call validation compute shaders inside
request_device. On Intel Iris Pro (Metal GPUFamily 1, Monterey) the Metal
compiler hits an internal error on them, which wgpu-core reports as
Device(Lost), so the window device can't be created in any mode, including
--safe-gpu and PHOTOCRAFT_CPU_CANVAS. PhotoCraft issues no indirect draws or
dispatches, so drop the flag; WGPU_VALIDATION_INDIRECT_CALL=1 still re-enables it.
* gpu_startup: skip indirect-call validation only on Intel Macs
Keep wgpu's default validation on Apple silicon, Windows and Linux; only Intel Macs,
where Metal's compiler crashed on the validation shaders (#3107), skip them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix(ui): respect native desktop scale under Auto UI scaling (#1278)
* UI scale: drop the now-unused monitor size, update docs and scorecard
Merge main; Auto no longer reads the monitor size, so remove the parameter and its
computation, simplify the test, and update ui-design.md and UI-217-9 to match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Rasterize Extrude blocks and scatter Wind streaks once
* Prepare Spin Blur rotations and fixed-channel sampling
* Share Lens Correction interpolation across color and alpha
* Make wide prepress traps independent of radius
* Add full-operation timing for five imaging algorithms
* Record 24 full-operation benchmarks and exact-output checks
---------
Co-authored-by: Brandon Thomas <[email protected]>
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 <[email protected]>
Desktops that move windows with Alt+drag grab every Alt+left press, so a
Zoom tool Alt-click never arrived and nothing happened. Watch the Zoom
tool for that grab (alt_grab) so the status bar explains it, and let a
latched Alt (Window > Modifier Keys) zoom out: the click, cursor and zoom
rectangle now honour it.
Claude-Session: https://claude.ai/code/session_01JgB5X7gyFEFo4oDW6vH4VN
Co-authored-by: Leel Wasanatha <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Preserve HDR range in float adjustments and stack statistics
* Cover HDR exposure round trips and Levels record order
* Report the backend used by GPU parity tests
* Compare float PSD oracles at native depth and preserve signed Exposure
* Retain merged alpha metadata when decoding float references
---------
Co-authored-by: Brandon Thomas <[email protected]>
* Type: Edit › Paste, Copy and Select › All work on the text being typed (#2893)
While editing type, Edit › Cut already worked on the selected characters (#1355), but the other
menu rows still ran their pixel commands: Edit › Paste tried to paste an image (and did nothing
with text on the clipboard), Edit › Copy copied rendered pixels, and Select › All selected the
canvas. Route them through the type editor like the keyboard: Paste asks the window for the
clipboard text, which arrives as a Paste event and replaces the selection; Copy copies the
selected characters; Select › All selects every character. Copy is enabled for a non-empty
character selection, Paste and Select › All while typing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Type tool: right-click menu while editing text (#2968)
A right-click inside an on-canvas type edit opened nothing: the Type tool
had no canvas menu, and the inline type editor is not an egui TextEdit, so
the text-field menu doesn't reach it. It now opens Photoshop's type edit
menu (canvas_tool_menu.rs TYPE_EDIT_MENU), built from existing commands:
Undo, Cut, Copy, Paste and Select All (dispatched as the Edit/Select menu
items, which act on the text being edited), Check Spelling..., Find and
Replace Text..., Faux Bold/Italic (type.setStyle on the edit's selection,
checked), Rasterize Type, Create Work Path, Convert to Shape (these and the
paragraph/point conversions end the edit first), Horizontal/Vertical and
the anti-alias modes (checked), Convert to Paragraph/Point Text and
Warp Text.... The right-click keeps the text selection. While the menu is
open the keyboard belongs to it: Esc closes only the menu instead of
committing the edit. Agents open it through ui.pointer as for the other
canvas menus.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Aligns the Spanish UI with the terminology a Spanish-speaking Photoshop user already knows,
fixes mistranslations and makes the register consistent. 351 rows changed out of 2,457.
- Dodge/Burn were swapped (tools, Color Dodge/Burn, Paint.NET variants).
- "Constrain proportions" read "Construir proporciones" (build) instead of "Restringir".
- Feather and Fade shared one label, as did Artboard and Canvas.
- Photoshop terms: Mesa de trabajo, Motivo, Ajuste preestablecido, Métodos abreviados,
Cuadrícula, Sector, Deformación de posición libre, Tampón de clonar, Calar, menú Vista…
- Formal "usted" throughout (the catalog mixed tú and usted).
docs/localization-es.md records the terminology so later additions stay consistent.
Claude-Session: https://claude.ai/code/session_012JC2QZ1Qc5KQMBswS38GKy
Co-authored-by: Diego Ospina <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
validate_planes bounded its zlib loop by the uncapped declared size, so a
PSD/PSB declaring more than MAX_DECODED_BYTES of merged image could make
PsdFile::from_bytes decompress far past the decode budget before any
error. decode_planes refuses such a layout with LimitExceeded anyway, so
the stream is now kept verbatim without inflating it, like an unknown
compression method. A layered file whose merged composite is too large
still opens (the importer only warns when the composite can't be decoded).
Co-authored-by: Claude Opus 5.5 <[email protected]>
Photoshop's Window menu ends with the open documents; PhotoCraft's had no
list in either menu bar (#1264 got the tab overflow menu only). Window now
ends with a separator and one row per document in tab order, labelled with
its name and checked when active; choosing one brings it forward. The ids
are window.document.<index>, so the rows also work over ui.menu.invoke.
- In-window menu bar: the rows come from menus::menu_items, after Window's
last item. Names are shown as they are, never translated.
- macOS menu bar: the rows follow Bring All to Front, where a Mac app lists
its windows. Opening or closing a document rebuilds only the Window menu:
AppKit adds its own items there (window tiling, the window list), so muda
can't insert rows by position, and the other menus stay as they are.
Switching documents moves the check in place. The menu state hash now
includes the documents' names, so a Save As rename shows up.
Tested in a Tart guest with three generated files (.psd, .png, .tif) in
both menu bars: on main neither lists them; with this change both do, and
choosing a row switches the document and moves the check.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Only the last dialog takes input, but egui only lets the modal drawn on top
take input, and it raises an area that is pressed or wasn't drawn the frame
before. With several stacked dialogs (three files opened together, each with
an Embedded Profile Mismatch prompt) an older prompt could end up in front:
greyed out, with the live one blocked behind it, so nothing could answer
either and the app had to be quit. Raise the live dialog when another one
is above it.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Preferences › Units & Rulers › New Document Preset Resolutions did nothing:
Print Resolution was hidden as unread, and the File › New presets always used
300 / 72 ppi. As in Photoshop, print and photo presets now keep their size in
inches at the Print Resolution (Letter at 200 ppi is 1700 x 2200 px), and
screen presets keep their pixels at the Screen Resolution. The Clipboard
preset keeps the copied pixels. Values that are not positive and finite (a
hand-edited preferences file) leave the preset as listed, and sizes clamp to
the file.new limit.
Print Resolution is now shown in the Preferences dialog; settings that do
nothing: 43 -> 42 of 150.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
While editing type, Edit › Cut already worked on the selected characters (#1355), but the other
menu rows still ran their pixel commands: Edit › Paste tried to paste an image (and did nothing
with text on the clipboard), Edit › Copy copied rendered pixels, and Select › All selected the
canvas. Route them through the type editor like the keyboard: Paste asks the window for the
clipboard text, which arrives as a Paste event and replaces the selection; Copy copies the
selected characters; Select › All selects every character. Copy is enabled for a non-empty
character selection, Paste and Select › All while typing.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Mirror the shell for right-to-left UI languages (DIST-220-4)
Arabic, Hebrew, Persian and Urdu read right to left, but the window's
chrome was hard-wired left to right, so an RTL catalog (#2505) would
put Arabic labels into a left-to-right layout.
The reading direction now follows the language code
(i18n::dir_for_code: ar, ckb, dv, fa, he, ps, sd, ug, ur, yi), so a
catalog needs no extra setting and the LANGUAGES rows are unchanged.
In a right-to-left language the shell mirrors:
- the menu bar starts at the right with File; menus open under their
title's right edge, and the arrow keys step through them in screen
order
- the toolbox docks on the right, the panel dock and its rail on the
left; tool flyouts open towards the canvas and their corner marks
and the header chevrons follow
- the title, options and status bars read from the right, with the
workspace controls and the Fit/progress group at the left end; the
title and window-drag gap use the free span between them
New module direction.rs holds the helpers (start_panel/end_panel,
row/trailing layouts, horizontal_centered, the popup alignment). Left
to right, every change lays out as before.
No RTL catalog ships yet, so PHOTOCRAFT_UI_DIRECTION=rtl forces the
mirrored layout for review (read once, native only). Panel interiors,
dialogs, menu rows, submenus and document tabs stay left to right, and
egui still draws UI labels without Arabic shaping or bidi reordering;
docs/localization.md and the parity docs say so.
Adds Lucide's chevron-left (the status bar's info menu arrow points
left in RTL; Timeline's Previous frame button asked for it too and drew
the fallback square).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Localization docs: egui shapes Arabic words; what's missing is a font and bidi word order
egui 0.36 shapes right-to-left runs with HarfRust, so letters join within
a word. The gaps are an Arabic fallback font in the UI and paragraph bidi
(words are placed left to right; emilk/egui#8576, #8577).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MathMattTech <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
View › Rulers (⌘R) now writes unitsAndRulers.showRulers, which is applied
at launch and whenever the preference changes, so new and opened documents
show the rulers as last set, as in Photoshop. The Preferences dialog doesn't
list it: the View menu sets it.
Guides are Vec<f64> (doc, New Guide/Move Guide, guide layouts, artboard and
canvas geometry, snapping, PSD resource 1032), so a guide placed in ruler
units or read from a PSD's 1/32 px positions keeps its exact place.
Scorecard UI-217-20 done (part of #217).
Co-authored-by: Claude Opus 5.5 <[email protected]>
Brush phase 6c. A stroke composites in float, then each pixel is rounded to 8 bits, so a soft,
low-flow stroke's gentle gradient collapses into flat bands (the audit counted 35 levels against
119 at 16-bit). The pixels the stroke changes now get the same deterministic per-pixel offset of
under half a level on every channel before quantisation:
- fractional values round up or down in proportion, so bands become fine grain;
- a value already on an 8-bit level rounds back to itself, so exact colours never change, and
pixels the stroke doesn't touch are left alone;
- the offset depends only on the pixel's position, so the live preview, the commit and a replay
agree.
16-bit and float targets are unchanged.
* Fix canvas context menus closing immediately on trackpad taps
Ignore outside-click dismissal during each menu's opening frame.
Share the guard across canvas tool and layer menus, including control-channel openings.
Add regression coverage for quick taps, repeated reopening, dismissal, and selection/history preservation.
* Share popup dismissal guard with the brush picker
* Merge main: pass the context to main's new control-channel Free Transform menu opener
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Tests: the marquee ellipse check walks several dash periods of the curve
The old check sampled ~12 positions along a 28 px arc of a 4-on/4-off ants
pattern: a margin of one sample, so renderer, antialiasing or dash phase
decided pass or fail. Walk the true ellipse around each edge midpoint over
±0.3 rad instead, with the ±2 physical px band hugging the curve, so the
check sees the dashes wherever the phase puts them (#253 follow-up).
* rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Pixel grid: fade the line colour across mid-tones instead of switching at one luma
The grid line was a full white line over pixels at or below luma 0.55 and a weaker
black one above it. On an image whose tones sit near that cut, neighbouring cells
landed on either side and the grid showed bright, dark and different-strength lines
in patches that traced the 0.55 contour.
The line is now a white layer and a black layer weighted by a smoothstep over luma
0.35 to 0.75, so mid-tones get a faint line that varies smoothly and dark and light
pixels keep the old white and black lines. The GPU shader and the CPU path
(pixel_grid.rs) use the same band; the CPU share is rounded to eighths so equal-colour
runs still merge into long segments.
Tests: unit tests for the fade and mid-tone evenness, and a GPU render test that
fails on the old shader (grid line 30 levels off its pixel at grey 0x87).
Co-Authored-By: Claude <[email protected]>
* Pixel grid: test the line over opaque white too (the #2461 review case)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Preserve profile, competing-dialog, crop, discard and authorization gates. Add control-handler regressions for RAW preview, commit and cancel.
Prepared with Codex assistance.
* Text fields: right-click edit menu (#2921)
Right-clicking in any text field (layer rename, a number field being
typed into, dialog and panel fields) opens Undo, Cut, Copy, Paste,
Delete and Select All at the pointer, acting on that field. A
right-click inside the selection keeps it, Esc closes only the menu,
and clicking the menu doesn't take the field's focus (the layer rename
would commit). The right-click never reaches the canvas or tool menus.
An egui plugin (text_field_menu.rs) opens and closes the menu for
every TextEdit; the app draws it before any field, so a chosen item is
handed to the field as its keyboard input in the same pass. Paste asks
the window for the clipboard text like Cmd+V in a field. No spelling
suggestions: there is no spell checker (UI-217-8 stays partial).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Text field menu: correct the spell-checker note (#2921)
PhotoCraft has a spell checker (edit.checkSpelling with a suggest
action); it just isn't wired into the text-field context menu yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Context-menu parity doc: same spell-checker correction (#2921)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Preferences > Type > Show Font Names in English (default on) now drives the
font menus (Type options bar, Character panel, style and Glyphs panels). With
it off, a family whose name table carries a non-English family name (e.g.
"MS Gothic" -> "MS ゴシック", "Microsoft YaHei" -> "微软雅黑") is listed by
that native name, sorted by the shown label, and the search matches either
name. Choosing it still applies the English family, so documents and PSD
output never change with the preference.
- text: FontDb::localized_family_names() reads each family's first face
(typographic family name, else family name) and a pure localized_name()
picks the first non-English record.
- ui-egui: sync_font_names() publishes the labels to the egui context once a
frame (cached per font generation); family_picker_in shows them.
- prefs: the row is no longer hidden; scorecard: dead settings 43 -> 42.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Every effect with a contour (Drop/Inner Shadow, Outer/Inner Glow, Satin,
Bevel & Emboss Gloss Contour and Contour element) now shows a contour
thumbnail with a preset picker and an Anti-aliased checkbox. Clicking the
thumbnail opens the Contour Editor: add/drag/delete points (the shared
point-curve editor), Corner per point, Input/Output, Preset, and Load…/Save…
of .shc contour files, read and written from Adobe's public File Formats
Specification (Contours). Edits go to the dialog's pending params, so the
preview follows and OK applies them in the one layer.layerStyle.replace.
- doc: Contour::Custom gains per-point corner flags (serde default).
- compose: contour_curve_lut splits the spline at corner points; CPU and
GPU effects use it.
- engine: the 12 standard presets with our own control points (older names
still accepted); `contour` params also take a curve object
{name, points, corners}; antiAlias params; Satin contour; the bevel
Contour element edits in place; contours_from_shc / contour_to_shc.
- io: PSD contour points read and write the Cnty (continuity) flag.
- psd: shc.rs parser/writer.
- scorecard FILE-215-15 missing -> partial.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Layers: Pattern Fill dialog for new and edited pattern fill layers (#2908)
Layer > New Fill Layer > Pattern... created the layer with default
settings and no dialog, and double-clicking a Pattern Fill thumbnail only
opened Properties, which has no pattern controls, so a pattern fill could
not be adjusted after creation.
Both now open Photoshop's Pattern Fill dialog: the pattern swatch picker,
Snap to Origin, Angle, Scale and Link with Layer, previewed live on the
canvas from a document snapshot. OK runs one engine command, so it is one
History step; Cancel changes nothing (a new layer is not created).
- engine: new `layer.fill.pattern.set` (pattern_fill_cmds) edits a Pattern
Fill's pattern, scale, angle, link and phase in one undo step, validating
every param; `preview` applies it or `layer.newFillLayer.pattern` to a
copy of the document for the dialog. `new_fill_layer`'s edit is shared
as `insert_fill_layer` so the preview is exactly what OK commits.
- ui: pattern_fill_ui (dialog body, OK, canvas preview), routed from the
menu item (no params) and Layer Content Options; explicit params still
run the engine directly. Catalog entries for the new strings.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Pattern Fill test: pick the swatch picker's Patterns button (main added a Patterns dock tab)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* app.open: open directory .pcraft through the automation read root (#2490)
The native on-disk format is a directory, but the automation read path
serves regular files only, so agents could not open the files the app
itself writes. The format crate's storage seam is now public (ByteSource
+ load_bytes_source); the workspace opens the .pcraft subdirectory through
the held capability and loads through it. app.open tries the directory
form for .pcraft paths and falls back to the bytes path for the
single-file (zip) form; every other format is unchanged.
* app.open .pcraft: review fixes (#2581)
read_entry now bounds the read itself (the stat length is only a buffer
hint) and both directory sources reject non-regular entries, so a crafted
bundle with a FIFO entry can no longer block the open and stream unbounded.
The desktop opener runs under the crash guard like its import sibling, the
zip fallback is structural (Ok(None) instead of an error-message match),
a directory-form document no longer arms a save-back target the file-only
automation writer could never replace, and the open status is translated
(Opened {name}, 16 locales).
* read_entry: saturating max+1 so a usize::MAX budget can't overflow
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Tools: wire Enable Flick Panning for the Hand tool (#204)
Preferences > Tools > Enable Flick Panning (default on) was never read.
Like Photoshop, a Hand drag (the selected Hand, the Space-held Hand or a
middle-button drag) released while the pointer is still moving now keeps
the view gliding that way and slows smoothly to a stop; a click, new drag
or key press stops it. Off, the view stops where the drag ended.
flick_pan.rs holds the pure state: the release velocity from the last
80 ms of drag steps, an exponential friction step (tau 0.18 s, so a fast
flick rests in under a second), a start/stop speed threshold, and clamps
for huge, NaN or infinite numbers. The canvas drives it per frame with
request_repaint only while gliding; with Overscroll off the glide is
clamped like the scrollbars and stops at the pan limit.
Settings that do nothing: 43 -> 42 of 149 (docs/scorecard.md).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Preferences: show Enable Flick Panning now that the Hand tool reads it (#204)
Remove tools.enableFlickPanning from prefs::HIDDEN_UNTIL_IMPLEMENTED so the
prefs_usage test passes and the dialog shows it; regenerate the scorecard
(settings that do nothing: 36 of 153) and mark flick panning done in
docs/ui-parity.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Channels: keep RGB off while an alpha or mask is shown, and select from that channel.
Co-authored-by: Cursor <[email protected]>
* Channels: rustfmt the RGB-off view check.
Co-authored-by: Cursor <[email protected]>
* channel view: pass the active layer to render in main's new tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Preferences: wire Tools › Show Transformation Values (#204)
The size and angle readouts beside the pointer (marquee W/H, Crop frame
W/H and angle, Crop Straighten angle) now go through
canvas::draw_value_readout, which draws them only while
tools.showTransformationValues is on, as in Photoshop. The brush HUD
isn't a transformation value and keeps drawing.
The preference leaves HIDDEN_UNTIL_IMPLEMENTED, so Preferences › Tools
shows it (its label is already translated). Settings that do nothing:
43 → 42 of 149.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Contributors: add my display name
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* feat(engine): Always Create Smart Objects When Placing gates the Place layer kind (#204)
* scorecard: regenerate after wiring Always Create Smart Objects When Placing
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Smart Objects: linked smart objects follow their files while open
As Photoshop documents ("Linked Smart Objects update automatically when
the Photoshop document is open, provided the source file has changed"),
a linked smart object whose file another app saves is re-rendered
without Update Modified Content. The desktop app stamps the active
document's linked files (size + modification time) on a worker thread
every two seconds and when its window comes back to the front; each
changed file is one Update Modified Content undo step, independent of
the selected layer. Stamps are kept per document; a file that can't be
read yet leaves no step and is tried again; files changed while the
document was closed still wait for Update Modified Content.
layer.smartObjects.updateChanged runs one look (refused over the
control channel like the other layer.smartObjects commands).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Smart Objects: a linked file that can't be read is tried again once it changes, not on every look
A linked file the automatic update couldn't read (still being written, or a
format smart objects don't read) kept its old stamp, so every two-second look
saw it as changed and read it again on the UI thread. It now keeps its new
stamp and is tried again once it changes again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Katt <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Right-clicking the "Effects" row or an effect row under a layer did nothing.
Like Photoshop, it now opens the layer style menu for that layer: Blending
Options, Copy/Paste/Clear Layer Style, Global Light, Create Layer, Hide All
Effects and Scale Effects. Items run the existing commands with menu-item
params (dialogs open as from Layer > Layer Style), selecting the right-clicked
layer first. The item loop of the layer row menu is shared.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
As in Photoshop's pop-up menus, typing a letter in an open dropdown list
jumps to the next option starting with it (the same letter again cycles
through them), letters typed within a second match a longer prefix, and
Enter closes the list on the option shown. Matching is case-insensitive
on the translated labels (Unicode lowercase, no byte slicing). While a
dropdown list is open it owns the keyboard like an open menu, so its
letters no longer reach the tool shortcuts.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* MCP bridge: reconnect before idle sockets die, retry reads, actionable timeouts (#2487)
The app closes idle control connections after 30s (IO_TIMEOUT) while the
bridge kept one persistent connection, so the first call after a pause
failed with "operation may have completed". The bridge now drops and
reconnects before sending once the connection has been idle 25s, so a
request is never written to a socket the app may have dropped.
The default call timeout drops from 60s to 55s, strictly below the app's
60s reply deadline, so the bridge's own error — naming the method and the
likely causes — is the one callers see instead of a race between the
app's deadline reply and the caller's own cutoff.
Idempotent read methods (ui.inspect and friends) get one automatic retry
on a transport failure; edits keep #1007 semantics and are never resent.
A timed-out call is never retried (it would only double the wait).
* MCP bridge: caller-vouched read retries, per-failure wording, docs contract (#2487 review)
The retry decision moves from a hand-maintained method list — most of whose
entries were engine command ids that never travel as control-protocol
methods, so they could never match — to the call sites that know a call is
a read: call never resends (#1007), call_read retries once on a transport
failure, and the MCP server opts its pure-read tools in. Failures are
worded by what they prove: a connection that could never be established is
"the request was not sent", not "may have completed"; a read timeout says
a read may be retried safely instead of demanding a state inspection. The
55s client cap, the read-retry exception, and the idle reconnect are now
documented in the control protocol and the security book.
The Pro toolbar drew each tool glyph at 16 px in a 30 px button (the
generic icon-button proportion), which reads small and thin next to
Photoshop's toolbar. Pro themes now draw tool glyphs at 20 px; the other
themes keep the icon-button size.
Claude-Session: https://claude.ai/code/session_01CVtZHu998NDhf4Z8QvS5pd
Co-authored-by: ngrafton2006 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Image Size: Photoshop's resampling, measured
Photoshop 25.4's Image Size was measured as a black box (synthetic 16-bit
images in, exact RAW out); every method but Preserve Details' enlargement is
linear, so each was taken apart from its impulse responses.
- Bicubic is the Keys cubic at a = -0.75, not Catmull-Rom.
- Bicubic Smoother (stretched x1.15, a = -0.625) and Bicubic Sharper
(x1.05, a = -1), with a growing with the reduction, and Bicubic Automatic.
- Reducing widens the filter by 1/s; below 1/4 the image is first averaged
down to four times the target. Bilinear is never widened and averages
whole pixels below 1/2. Positions are rounded to 1/128 pixel and stop at
the edge pixel's centre.
- Two passes, the smaller scale first, clipped and stored between them; the
bicubic filter is designed for the height's scale and serves both axes.
Bicubic Automatic splits a mixed resize into Sharper, then Smoother.
- Image Size and Fit Image take automatic, bicubicSmoother, bicubicSharper
and bicubicAutomatic; the dialog lists Photoshop's methods and opens on
the interpolation preference.
On 225 probes the mean error falls from 240-550 to under 0.5 levels of
32768; 8-bit results are identical to Photoshop's for 99 % of values.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Format: one rustfmt change main needs
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* perf: Duplicate Layer recomposites only where the copy draws (#2883)
Duplicating a layer reported no damage, so the canvas recomposited the
whole document after every Duplicate Layer (P8/P9). The copy itself was
already cheap (tiles are shared Arcs). Report the copy's change bounds
instead, through the step_damage walk extended to bounded additions:
self-contained added layers (not an adjustment, not a new clipping base,
no video or excluded channels) contribute their change bounds; anything
else still recomposites everything.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* perf: pasting a layer recomposites only where it draws (#2901)
Paste (and Paste in Place, Paste Into, Paste Outside) inserted the
pasted layer through Session::edit, which reports no damage, so every
paste recomposited the whole document (P10/P11). Capture the document
before the paste and report the new layer's change bounds with
note_insert, as Duplicate Layer does (#2884). Anything that isn't
self-contained (a new clipping base, video, excluded channels) still
recomposites everything.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* feat(ui): add guide layout dialog with live preview
* test(ui): restore path helper used by save dialog assertions
* chore(i18n): avoid conflicts with appended catalog entries
* chore: avoid append conflict for contributor credit
* Merge main: one people.toml entry for Skyfuller1303, drop the duplicate joined helper
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
The Frame tool (K, its own toolbar slot after the Crop group) draws a
rectangular or elliptical frame; the options bar picks the shape, Shift
constrains to a square or circle and Alt draws from the centre. A frame
drawn over the selected pixel layer's content takes that layer in;
otherwise it is an empty frame.
Frames are built as Frame from Layers builds them: a group clipped by a
layer mask (rectangular or anti-aliased elliptical). The new engine
command layer.new.frame does it in one Session::edit, so one undo step.
Co-authored-by: Claude Opus 5.5 <[email protected]>
The Elliptical Marquee tested and wrote every pixel one at a time
(Surface::write_pixel), single-threaded, with no shared full tiles. The
add/subtract/intersect modes read both selections over the whole canvas
into f32 buffers. On 6000x4000 an ellipse took ~0.6-1.1 s and a mode
combination ~0.3-0.5 s.
ellipse_surface builds the shape per row: a run of fully selected pixels
(bounded analytically, checked with the exact f32 tests at both ends)
and edge pixels that get the original per-pixel coverage test. Tiles are
built in parallel, and fully selected tiles share one tile.
combine_surfaces maps two GRAY8 selections tile by tile through a
256x256 byte table, sharing tiles that come through unchanged. Both
produce exactly the old bytes (equivalence test against the old
per-pixel code).
Co-authored-by: Claude Opus 5.5 <[email protected]>
* scorecard: eight Open bugs rows describe work that is already in the tree
AGENTS.md sends contributors to the scorecard's missing and partial rows when
picking work, so a stale row costs someone a session: they sit down to write a
test that is already there. These eight ask for work that exists and is covered
by passing tests.
#205 / #207, mask and channel transforms:
- BUG-205-4 ("no raster-mask test"): linked_raster_mask_moves_with_the_pixels.
- BUG-205-5 ("the other two not implemented"): all three cases are covered, by
linked_mask_with_a_selection_moves_only_the_selected_region,
unlinked_mask_stays_put and targeted_unlinked_mask_transforms_alone.
- BUG-207-4 ("transform() has no target handling"): edit.transform takes target
(pixels, mask, quickMask or {channel: i}) and documents it in its params doc;
targeted_alpha_channel_transforms_alone covers the alpha channel.
#206, kerning — the whole section:
- BUG-206-1: CharStyle has both `kerning: Kerning` and `kern: f32`.
- BUG-206-2 / BUG-206-3: io/tests/psd_kerning.rs round-trips manual, Off and
Optical kerning, and reads the AutoKerning flag and Kerning value of every
character back out of the written TySh.
- BUG-206-4: Alt+Left/Right at a collapsed caret calls kern_pair, and the
Character panel field parses Metrics / Optical / a manual value; both tested.
- BUG-206-5 stays partial, not done: the round trips are covered, but no oracle
corpus file is dedicated to kerning values, which that row also asks for.
FILE-216-5 keeps its missing status (no save-path tests for layered TIFF, PDF,
SVG, ACO/ASE, .shc or Smart Filters), but its note claimed there is no
atomic-save failure-injection test; format/src/atomic/tests.rs has two, and
BUG-203-6 already records them as done.
docs/scorecard.md regenerated with cargo xtask scorecard; Open bugs moves from
22/2/8 to 29/2/1.
* scorecard: the Type section's kerning and Thai rows are out of date too
TYPE-218-6 ("Optical renders as Metrics; Alt+arrow moves by word") is done:
Alt+Left/Right at a collapsed caret calls kern_pair, optical kerning is computed
from the glyph outlines in text/src/optical.rs and applied by layout.rs when the
run's mode is Optical, and both modes round-trip through PSD. Covered by
type_tool_alt_arrows_kern_the_pair, optical_matches_photoshop,
optical_kerning_tightens_open_pairs, txt2_carries_optical_kerning and
io/tests/psd_kerning.rs. The row points at #206, whose own rows are in the same
state.
TYPE-218-3 goes to partial, not done: Thai is covered (THAI_FAMILIES in fonts.rs
and thai_in_latin_font_falls_back_to_installed_thai_font, #1909), but Khmer and
Indic have no fallback list and there are still no per-script golden tests.
Unchanged because they read correctly: TYPE-218-1 (TextShape is still Point and
Box), TYPE-218-2 (no direction UI), TYPE-218-4 (the axes in the shell are the
brush's), TYPE-218-8 (no triple or quadruple click) and TYPE-218-9.
* scorecard: Tools and Distribution rows are out of date too
TOOL-213-7 ("Live stroke only for Brush and Eraser"): canvas.rs strokes_live
covers Brush, Pencil, Eraser, Blur, Sharpen, Smudge, Dodge, Burn and Sponge,
and live_retouch_command adds the Clone Stamp, Healing Brush and History Brush.
TOOL-214-8 ("ruler only feeds straighten"): image.rotation.arbitrary opens
seeded with the measured ruler angle and the direction that straightens it;
arbitrary_rotation_starts_at_the_ruler_angle checks 5.7106 deg within 1e-3 and
the direction for three lines.
TOOL-214-5 goes to partial, not done ("No context menu in canvas.rs or
transform_tool.rs"): TRANSFORM_MENU does open on a right click during a
transform, with Free Transform, Scale, Rotate, Skew, Distort, Perspective and
Warp, and three tests cover it — but the row also asks for Flip and
Rotate 90/180, which are not there.
DIST-220-2 ("release.yml only x86_64 and i686 msvc"): the Windows matrix has
{ arch: arm64, target: aarch64-pc-windows-msvc }, and windows-arm64.yml
installs that MSI on a Windows 11 ARM64 runner, checks the binaries are ARM64
and runs the CLI natively. Signing takes the same path as x64 (DIST-2).
Unchanged because they read correctly: the whole Automation section (ABI_VERSION
is still 1 with Kind::Filter only, there is no ONNX dependency, no such xtask
check, and agent_tasks.rs really does run ten scripted tasks), TOOL-213-12,
TOOL-214-4, TOOL-214-6, DIST-220-1 and DIST-220-3.
* scorecard: Workspace and UI and File compatibility rows are out of date too
UI-217-9 ("prefs.rs UiScale unused"): prefs::UiScale offers Auto plus 75 to 300
percent, display_scale resolves it and sync_display_scale applies it with
ctx.set_zoom_factor; being a preference, it is remembered. The zoom_factor
assert the note cites is in a canvas-zoom test, checking that zooming the
document does not scale the interface.
FILE-216-1 ("No corpus step in ci.yml; in progress in #190"): ci.yml has a
corpus job on every PR and on main, pinned, sha256-verified and cached by the
pins. #190 is merged.
UI-217-22 and UI-217-23 go to partial, not done:
- layer.stampVisible merges the visible layers into a new layer and leaves the
originals, and layer.stampDown exists, but neither has a menu entry and
Alt+Merge is still missing.
- layer.copyToDocument copies layers into another document keeping masks,
styles and blending (#589), but there is still no copy/paste layer clipboard.
UI-217-11 keeps missing; only its note changes, from "five themes" to the eight
ThemeKind has. No Soft dark/Soft light, previews or outlined knobs.
Unchanged because they read correctly: UI-217-20 (guides are still Vec<f32> and
there is no rulers preference), FILE-215-6 (SmartObject.transform is still an
Affine), FILE-215-19 (no JPEG Options dialog), and the rest of both sections.
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Photoshop has four interface brightness levels; PhotoCraft had the two
darkest (Pro, Pro Medium Gray). Add the other two with the Pro layout:
- Pro (Light Gray), id proLightGray: #B8B8B8 panels.
- Pro (Light), id proLight: #F0F0F0 panels.
They are light themes: Preferences > Interface lists them under Light
Theme, Window > Theme has an entry for each, and the legacy
interface.theme preference selects and migrates them. ThemeKind::is_pro
replaces the hard-coded Pro | ProMedium checks so the dock keeps the Pro
tab layout on every Pro level. Labels are translated in every catalog.
Tests check the panel greys, ids and names, the darkest-to-lightest
order, and WCAG contrast of text, icons and accent text on every surface.
Claude-Session: https://claude.ai/code/session_01CVtZHu998NDhf4Z8QvS5pd
Co-authored-by: ngrafton2006 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
A dab that painted past a layer's stored tiles changed its resident page
region, which recreated the texture and converted and uploaded every tile
of the layer in that page cell again. Copy the overlap from the old
texture on the GPU instead (in its own submission, ahead of the queued
tile writes) and keep its tile map, so only new or changed tiles upload.
32-bit float RGB/gray tiles also convert to RGBA16F straight from their
samples (same bytes as the general path) instead of decoding into a float
buffer and converting pixel by pixel.
P16 (32-bit, 20 layers) brush dab + refresh, median of 6 alternating runs:
p50 8.9 -> 3.1 ms, p95 27.2 -> 7.5 ms. P13 is unchanged (~1.6 / ~4.5 ms).
Co-authored-by: Claude Opus 5.5 <[email protected]>
The Fit button was a full-size 30 pt button in the 30 pt Studio bar and
the zoom field a 24 pt box in the 24 pt Pro bar, so both touched or
overflowed the bar's edges. Inside every value field the theme's 24 pt
interact height and 4 pt button padding made the number taller than its
slot, so it sat low and, in a narrow box, spilled out of the left edge.
The status bar's controls now take a compact height (22 pt in Studio,
18 pt in Pro) from status_bar_heights, leaving even margins, through
new compact_value_field and compact_button widgets. A value field's
number slot is at least one text row tall and the number widget is
sized to it, so the digits sit 1 pt above the box's centre as intended.
With the bar at its true height the canvas shifted by 1 pt, which showed
that the live lasso ants were not on pixel centres; they are now snapped
like the marquee's.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Duplicating a layer reported no damage, so the canvas recomposited the
whole document after every Duplicate Layer (P8/P9). The copy itself was
already cheap (tiles are shared Arcs). Report the copy's change bounds
instead, through the step_damage walk extended to bounded additions:
self-contained added layers (not an adjustment, not a new clipping base,
no video or excluded channels) contribute their change bounds; anything
else still recomposites everything.
Co-authored-by: Claude Opus 5.5 <[email protected]>