Commit Graph
1334 Commits
Author SHA1 Message Date
myangaa1 d10ded55d6 Clip the checker under rounded colour swatches to their corners (#3208) (#3363)
The colour swatches (Layer Style's Stroke, Color Overlay and the rest,
and every `color_swatch` / `color_swatch_button`) painted a square
checkerboard under a rounded fill, so the checker's light cells showed
at the four corners, white specks on the dark themes.

The checker under a rounded swatch is now a repeating two-by-two tile
textured onto a rect with the swatch's corner radius: one shared texture
per context, nearest-filtered so it never blurs, scaled so a cell stays
4 pt. `checker` itself is unchanged for the square previews.
2026-10-11 11:48:13 -04:00
5025fd2895 Color Sampler tool (#1046) (#2753)
* Color Sampler tool (#1046)

Add the Color Sampler to the Eyedropper (I) flyout: click to pin numbered
sample points to canvas pixels, drag to move, Alt-click (or drag off-canvas)
to remove, Clear All in the options bar, at most 10 per document. Each point
reads one composite pixel (RGB 0-255 and naive device CMYK, same as the
pointer readout) and the Info panel lists them under their number with the
position in the ruler unit; the readouts refresh when the document changes.

Points are per-document engine view state (DocState.color_samplers): they
belong to their document, are discarded on close, and are not saved to
.pcraft or PSD. They are driven by commands shared by the UI, CLI, control
channel and MCP (view.colorSamplers.add/.move/.delete/.clear/.list); the
mutations change no pixels and add no History step.

Points follow the canvas geometry (crop, Canvas Size, Image Size, Image
rotation/flip, rotated crop) through a pending Session.sampler_map that
Session::edit applies before culling the points that land outside the new
canvas; the count removed is reported in the status bar.

- engine: new sampler_cmds.rs (commands + shared readout helpers) and the
  sampler_map wiring in lib.rs, image_cmds.rs, mode_cmds.rs, commands.rs,
  analysis_cmds.rs (compound rolls points back on error).
- ui-egui: Tool::ColorSampler + original sampler.svg icon, toolbar slot,
  Info panel list, options bar, pointer handling, overlay markers, cursor.
- docs/scorecard.md regenerated (TOOL-1649-1 no longer says the tool is
  missing). No menu item, so parity is unchanged (627/627).

* Color Sampler follow-up: tool count, README, i18n, roadmap

The full ui-egui suite (not just the filtered tests) surfaced these gaps:

- state.rs: Tool::ALL.len() assertion 52 -> 53.
- README.md: "52 tools" -> "53 tools" and add Color Sampler to the list,
  so readme_tool_list_matches_tool_all passes.
- i18n: the options-bar hint, "{n} of {max} sample points" and
  "No document open" need entries in every catalog that claims complete
  menus (every_tl_literal_is_translated); added to all 16 (coverage
  1584/1584). The translations were drafted alongside the tool.
- docs/roadmap.md: drop Color Sampler from the missing-tools list and note
  it landed.

* Color Sampler: fit main's new tools and Image Size resample type

Reset Tool covers ColorSampler, the test ViewXform sets aspect, and
Image Size checks Method::None instead of an Option.

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 11:43:09 -04:00
d22a9fac6e fix(ui): Global Light opens with stale values and overwrites document setting (#3217)
Populate Layer › Layer Style › Global Light dialog fields from the
active document's shared global lighting rather than schema defaults or
remembered values from other documents. This ensures opening Global Light
and confirming unchanged preserves the active document's lighting.

Fixes #3054.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:36:23 -04:00
Valentin KleinandClaude Opus 5.5 7fd5715dbd Type menu: Anti-Alias › None first and checked; checks follow the edited layer (#2983) (#2984)
- menu_catalog: add Type › Anti-Alias › None before Sharp, as in Photoshop.
  It used to come from the engine list, appended after Windows and built
  without a checked state, so a layer set to None showed no check.
- view_cmds::type_checked reads type_tool::target_text (the edited layer,
  else the active or a selected type layer), as the options bar and the
  Type commands do, not only the active layer.
- Anti-alias and orientation items stay check items (Some(false)) with no
  type layer, so the native menu doesn't rebuild.
- Parity floor 629 -> 630; docs/parity-checklist.md regenerated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:35:38 -04:00
Valentin KleinandClaude Opus 5.5 fd351918e5 PSD: cap the layer tree for psd_to_document; layered TIFF checks group depth (#2940) (#2975)
The file-open path rejects PSDs nested past MAX_GROUP_DEPTH before
conversion, but the public psd_to_document built file.layer_tree() to the
file's full depth (~16000 levels) before Ctx::build capped it, and layered
TIFF reached it with no preflight at all.

- photocraft-psd: PsdFile::layer_tree_max_depth(n) builds the tree
  iteratively and drops the children of groups nested n deep, so walking
  or dropping it recursively is bounded. layer_tree() is the uncapped case.
- psd_to_document uses it with MAX_GROUP_DEPTH, the same cap Ctx::build
  and apply_link_groups apply, so imports are unchanged.
- Layered TIFF rejects nesting past the cap with the same error as a PSD
  (shared psd_import::check_group_depth).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:20:32 -04:00
buivanvu94 6a2aaebe99 fix(ui): unchanged Custom Proof Setup replaces active document proof profile (#3213)
Populate View › Proof Setup dialog fields from the active document's
current proofing condition rather than schema defaults or remembered
values from other documents. This ensures opening Custom Proof Setup and
confirming unchanged preserves the active proof profile and condition.

Fixes #3055.
2026-10-11 11:18:41 -04:00
d8d8d9f3d2 Tools: Convert Point tool in the Pen flyout (#3204) (#3211)
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 <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:09:38 -04:00
c0110fb4d2 fix(engine): allow targeting layer mask and vector mask via channel.target (#2458) (#2705)
* fix(engine): allow targeting layer mask and vector mask via channel.target (#2458)

* fix(ui): apply channel target actions before syncing mask targets in channels panel

---------

Co-authored-by: huudinh1401 <huudinh1401@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:06:57 -04:00
a1de9ce4c8 fix(ui): respect native desktop scale under Auto UI scaling (#1278) (#3165)
* 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 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:53:55 -04:00
AyanandBrandon Thomas acce8e0e99 Five exact-output speedups: dense Extrude 160s to 0.39s (#2685)
* 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 <echelon@gmail.com>
2026-10-11 10:50:45 -04:00
Valentin KleinandClaude Opus 5.5 0aaf5b60fe Select and Mask: live preview in Photoshop's view modes (#1754) (#2970)
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>
2026-10-11 10:48:26 -04:00
d67e28c118 fix(zoom): Alt-click with the Zoom tool on desktops that take Alt+click (#3137) (#3164)
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 <260480244+kavishkashehan726-sudo@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:47:37 -04:00
Elvis33LE 2c3d972f18 Speed up large content-aware scale operations (#2596) 2026-10-11 10:46:43 -04:00
LessaandBrandon Thomas 53d09f5850 Preserve HDR range in float adjustments and stack statistics (#2592)
* 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 <echelon@gmail.com>
2026-10-11 10:44:55 -04:00
Valentin KleinandClaude Opus 5.5 989644cabd Type tool: right-click menu while editing text (#2968) (#2969)
* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:44:36 -04:00
0f8b370621 i18n(es): native-style review of the Spanish catalog (#3154)
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 <diegozpy@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 10:43:15 -04:00
Valentin KleinandClaude Opus 5.5 0fb4bbf1db PSD: don't inflate an over-budget ZIP merged image at parse time (#2926) (#2967)
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 <noreply@anthropic.com>
2026-10-11 10:40:31 -04:00
buivanvu94 1fdb0378a3 fix(ui): respect explicit dropdown defaults in schema dialogs (#3056) (#3149) 2026-10-11 10:36:15 -04:00
Brennan StehlingandClaude Opus 5.5 16bdcbd54e Window menu: list the open documents, the active one checked (#2991) (#3147)
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) <noreply@anthropic.com>
2026-10-11 10:31:26 -04:00
kayrockscreenprintingandClaude Opus 5.5 1f742629bb Dialogs: keep the live dialog in front of older ones (#3140)
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 <noreply@anthropic.com>
2026-10-11 10:25:29 -04:00
d45b3718ce New Document: presets follow Units & Rulers › Preset Resolutions (#204) (#3126)
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 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 10:15:40 -04:00
Valentin KleinandClaude Opus 5.5 7b88189e2d Type: Edit › Paste, Copy and Select › All work on the text being typed (#2893) (#2960)
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 <noreply@anthropic.com>
2026-10-11 10:09:30 -04:00
ade5799165 Mirror the shell for right-to-left UI languages (DIST-220-4) (#3114)
* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: MathMattTech <334629259+MathMattTech@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:06:18 -04:00
Valentin KleinandClaude Opus 5.5 1a54384d80 Rulers: remember View › Rulers for new documents; guides as f64 (#2918) (#2954)
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 <noreply@anthropic.com>
2026-10-11 10:05:13 -04:00
2d2e4993a1 fix(ui): reduced suffix spacing width in the value field to prevent label overflow (#3040) (#3084)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:59:53 -04:00
Tanish Manku 75101ddce2 8-bit documents: soft, low-flow brush strokes are dithered instead of banded (#3237)
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.
2026-10-11 06:58:47 -07:00
52e130d481 Fix canvas context menus closing immediately on trackpad taps (#3029)
* 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 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:52:26 -04:00
30bd8a5598 Fix Shape Stroke controls, popup interactions and paint colours (#3019)
Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:46:44 -04:00
3cbd727b85 Tests: the marquee ellipse check walks several dash periods of the curve (#3018)
* 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 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:40:44 -04:00
585131440a Pixel grid: fade the line colour across mid-tones instead of flipping at one luma (#2990)
* 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 <noreply@anthropic.com>

* Pixel grid: test the line over opaque white too (the #2461 review case)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 09:35:34 -04:00
Harrison O'Connor-Hoover 7a6058e6b4 control: allow requests that operate an open Camera Raw dialog (#2988)
Preserve profile, competing-dialog, crop, discard and authorization gates. Add control-handler regressions for RAW preview, commit and cancel.

Prepared with Codex assistance.
2026-10-11 09:26:46 -04:00
Valentin KleinandClaude Opus 5.5 885b754414 Text fields: right-click edit menu (#2921) (#2952)
* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* Context-menu parity doc: same spell-checker correction (#2921)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:25:19 -04:00
1234ba4c6f Type: wire Show Font Names in English (#204) (#2981)
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) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 09:21:55 -04:00
Valentin KleinandClaude Opus 5.5 7c96e9e120 Layer Style: contour picker and Contour Editor (#2912) (#2949)
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 <noreply@anthropic.com>
2026-10-11 09:21:18 -04:00
6601bbc3e7 Layers: Pattern Fill dialog for new and edited pattern fill layers (#2908) (#2979)
* 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) <noreply@anthropic.com>

* Pattern Fill test: pick the swatch picker's Patterns button (main added a Patterns dock tab)

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 09:16:18 -04:00
60339cf675 app.open: open directory .pcraft through the automation read root (#2490) (#2581)
* 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 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:12:59 -04:00
10fb9233d5 Tools: wire Enable Flick Panning for the Hand tool (#204) (#2964)
* 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) <noreply@anthropic.com>

* 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) <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 09:03:11 -04:00
cb9513c718 Channels: keep RGB off while an alpha or mask is shown, and select from that channel. (#2555)
* Channels: keep RGB off while an alpha or mask is shown, and select from that channel.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Channels: rustfmt the RGB-off view check.

Co-authored-by: Cursor <cursoragent@cursor.com>

* channel view: pass the active layer to render in main's new tests

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:03:05 -04:00
026432815c Preferences: wire Tools › Show Transformation Values (#204) (#2953)
* 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 <noreply@anthropic.com>

* Contributors: add my display name

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 08:57:49 -04:00
55a617cd66 Place: wire Always Create Smart Objects When Placing (#204) (#2543)
* 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 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:51:33 -04:00
a0ff3a01f2 Smart Objects: linked smart objects follow their files while open (#2510)
* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Katt <238968166+KattKnight@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 08:42:52 -04:00
David MannandClaude Opus 5.5 f781cb70fc Layers: right-click an effect row for the layer style menu (#2846) (#2950)
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) <noreply@anthropic.com>
2026-10-11 08:37:34 -04:00
509b596805 Dropdowns: type a letter to jump to an item, Enter to choose (#1481) (#2948)
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) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 08:33:29 -04:00
Ahmed Hosny 3e3e580217 MCP bridge: reconnect before idle sockets die, retry reads, actionable timeouts (#2487) (#2507)
* 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.
2026-10-11 08:29:35 -04:00
ecbd3d78be Toolbar: Pro themes draw tool glyphs at two thirds of the button (#2665) (#2932)
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 <ngrafton2006@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 08:28:38 -04:00
UnemarsandClaude Opus 5.5 0b4b1e4286 Image Size: Photoshop's resampling, measured (#2451)
* 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) <noreply@anthropic.com>

* Format: one rustfmt change main needs

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 08:25:21 -04:00
Valentin KleinandClaude Opus 5.5 7107c80042 perf: pasting a layer recomposites only where it draws (#2901) (#2917)
* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:24:51 -04:00
ec28fdd03d Add Photoshop-style guide layout dialog and live preview (#2930)
* 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 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:23:03 -04:00
Valentin KleinandClaude Opus 5.5 42e4af6416 Tools: Frame tool (#2914) (#2915)
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 <noreply@anthropic.com>
2026-10-11 08:20:15 -04:00
Valentin KleinandClaude Opus 5.5 614d06892c perf: marquee selections on large documents are built row by row (#2888) (#2907)
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 <noreply@anthropic.com>
2026-10-11 08:11:17 -04:00