Files
photocraft/docs/ui-dialogs.md
T
+3 67e4b8a499 feat: Alt+Cancel → Reset for opt-in command dialogs (#3871)
* Update .gitignore and add UI Dialogs documentation

- Added .specstory, .private, and .vscode to .gitignore to exclude additional files from version control.
- Introduced a new documentation file, ui-dialogs.md, detailing the architecture, rules, and usage of modal and modeless dialogs in the egui shell, mirroring Photoshop's functionality.

* Enhance UI Dialogs documentation in ui-dialogs.md

- Expanded the section on command routing and dialog types, clarifying the roles of various dialog kinds and their associated modules.
- Improved descriptions of routing markers and private keys, ensuring better understanding of the dialog architecture and usage.
- Updated the menu command entry details to reflect changes in module organization and functionality.

* Implement Alt+Cancel (Reset) functionality for various UI dialogs

- Introduced a new module `dialog_alt_reset` to handle the Alt+Cancel behavior, allowing users to reset dialog fields to their original values while keeping the dialog open.
- Updated multiple dialog modules to support this feature, including adjustments for color pickers, sizing, and guide layouts.
- Added tests to ensure that the reset functionality correctly restores original values and maintains dialog state.
- Enhanced documentation to reflect the new Alt+Reset behavior and its implementation details across different dialog types.

* Update manual UI testing command to include build timing summary

- Modified the `manual-ui-testing` command in `.cargo/config.toml` to include the `--timings` flag, enabling the generation of a build timing summary after compilation and linking.
- Refactored the `reset_fields` function in `filter_dialog.rs` for improved readability by consolidating the retrieval of the `spec` variable into a single line, enhancing code clarity.

* Layers panel: dragging the vector mask to the Trash asks to delete just the mask (#3119) (#3125)

* Layers panel: Shift+Plus/Minus cycle blend modes; effects dropped on the Trash are deleted (#3102)

- blend_keys.rs: Shift+Plus / Shift+Minus step to the next / previous blend mode, wrapping:
  the brush's mode with the Brush or Pencil (tools.setBrush), the Gradient's with the Gradient
  tool, otherwise the active layer's (layer.setProps; groups include Pass Through, the
  Background is left alone). One undo step per press.
- panels.rs: an effect row dragged onto the footer's Delete button deletes that effect, the
  "Effects" row clears the whole style (layer.layerStyle.clear).
- engine: layer.layerStyle.clear takes an optional "effect" index ("Delete Effect" step);
  bad indices and layers return Err.
- scorecard UI-217-14 -> done (its wheel, footer drop and thumbnail checker parts were already
  on main via #1961, #965).

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

* Layers panel: Shift+Plus/Minus step the blend mode on a German layout too (#3102)

On layouts with a key of its own for `+` (German, Italian, Spanish) Shift+`+`
types `*`, which egui has no key for, so egui-winit reports the physical key:
`]` on German. That matched Shift+] (Increase Brush Hardness) before the blend
keys ever saw it. Shift+`-` types `_` and arrives as the physical `/`.

shortcuts::layout_symbols now turns such a fallback press (Shift, the key equal
to its physical key, not a digit, next to the text `*` or `_`) into `+` / `-`
before any shortcut is dispatched. A US Shift+] still arrives as `}` and keeps
raising the hardness; US Shift+8 (`*`) stays a digit for the opacity keys.

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

* Shortcuts: Ö / # are the brush keys on a German layout (#3109)

Photoshop's German shortcuts step the brush size with Ö / # and the hardness
with Shift+Ö / Shift+#, since [ and ] need AltGr there (and that press carries
Alt on the Mac and Windows, which the shortcut match rejects). egui can't name
ö or #, so they arrived as the physical ; and \ and matched nothing; Shift+#
types ' and arrived as Quote.

shortcuts::layout_symbols (from #3102) now reads a table of typed symbol and
Shift: ö / Ö -> [, # / ' -> ], next to * -> + and _ -> -. It runs at the top of
shortcuts::handle (unless a text field has focus), so Liquify's and the
Magnetic Lasso's [ ] get the German keys too. Only presses whose key egui took
from the physical key or from that very text change, and never digits.

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

* Layers panel: a mask dragged onto the Trash asks to apply or delete it (#3113)

A drag that started on a layer's mask thumbnail dragged the whole row, and
dropping it on the Trash deleted the layer. Now a drag from the pixel mask
thumbnail (where it was drawn last frame) drags the mask: it shows a "Layer
Mask" ghost and starts neither a layer reorder nor a cross-document transfer.
Dropped on the Trash it opens Photoshop's prompt "Apply mask to layer before
removing?" with Apply (layer.layerMask.apply, greyed out where it can't run),
Delete (layer.layerMask.delete) and Cancel. Clicking the Trash still deletes
the targeted mask at once (#2197).

The prompt (mask_trash_prompt.rs) is an ordinary dialog: ui.dialog.confirm
applies, ui.dialog.set {"choice": "delete"} + confirm deletes, cancel keeps.
Two new strings in all 16 catalogs.

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

* Layers panel: dragging the vector mask to the Trash asks to delete just the mask (#3119)

A drag that started on a row's vector mask thumbnail picked up the whole
layer row, so dropping it on the Trash deleted the layer. The mask drag
from #3113 now covers both thumbnails: a vector mask dropped on the Trash
opens Photoshop's "Delete the vector mask?" (OK / Cancel), and OK runs
layer.vectorMask.delete. Clicks on the thumbnail are unchanged.

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

* Brush keys: layout table plus bracket-position fallback for every layout (#3109)

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

* layout_keys: drop unused field, 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>

* Interface: an Enable Animations preference (#3273) (#3276)

Preferences › Interface › Enable animations (interface.enableAnimations, on by
default). Off, prefs_ui::sync_animations sets egui's animation_time to 0 and
scroll_animation to none in both the dark and light styles, so menus, popups
and collapsing sections change at once and scroll areas jump. It is re-applied
each frame, so a theme change keeps it. Translated in all 16 catalogs; the
scorecard row UI-217-10 goes to partial.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Edit > Copy takes a targeted layer mask, so masks travel between layers and documents (#2928) (#3267)

* automation: export layers, data sets and CSVs through the granted roots (#3216) (#3230)

* automation: doc.place reads a file through the read root (#2810)

file.placeEmbedded takes an ambient path, so automation refuses it and an
agent could not place a file into the open document at all. doc.place
resolves the path beneath the read root the way doc.open does and hands the
bytes it already read to the public file_cmds::place_bytes, so the existing
cap-std rules stay the only path rules: no new scope logic, and the ambient
command keeps its refusal. Trusted-local callers still run the command.

* automation: export layers, data sets and CSVs through the roots (#3216)

file.export.layersToFiles and file.export.dataSetsAsFiles take an ambient
dir, file.import.variableDataSets an ambient path, so all three are refused
in automation even for files inside the granted roots, and an agent could
not export a single file or a data set at all.

Both halves already existed: the engine writes bytes it has just encoded
(save_doc is encode + write_file, and the data-set exporter encodes with
photocraft_io::export before writing), and the automation side already
holds a write capability — AuthorizedWorkspace::export_sequence creates the
directory, refuses to overwrite, accepts only single-component names and
removes every file it created when a step fails, which is what Render Video
uses. So each command body moves behind the same sink callback
render_video_with takes: the command keeps writing the bytes it is handed
to its dir, the new methods write the very same bytes through the root, and
the two implementations cannot drift.

---------

Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Type: fall back to installed Khmer, Lao and Burmese fonts (#218) (#3259)

Text in these scripts drew .notdef boxes although the OS had a font for
them. Detection was never the problem: the system font scan loads them,
and asking for one by name shapes correctly. The fallback stack is a
list of family names intersected with the installed fonts, and it had no
entry for any of the three, so an installed Khmer, Lao or Burmese font
could not be reached from a Latin-only face.

- photocraft-text: KHMER_FAMILIES, LAO_FAMILIES and MYANMAR_FAMILIES join
  fallback_candidates next to THAI_FAMILIES; the web build's served-font
  scripts gain Khmr, Laoo and Mymr (with their ranges and languages).
- photocraft-ui-egui: the UI's lazy loader had one non-CJK slot (Thai).
  It becomes a LazyScript slot table, so the three scripts are data
  rather than a third and fourth copy of the same state machine.
- Tests assert detection and selection separately, so a future failure
  names its layer, and skip when a machine lacks the script's font.

Co-authored-by: Raccoon <235524086+RaccoonPie-314@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Curves: pencil mode, Smooth and Edit points (#3088) (#3108)

* Curves: pencil mode, Smooth and Edit points (#3088)

The Curves editor (Image > Adjustments dialog and the adjustment layer's
Properties panel) gains Photoshop's "Edit points to modify the curve" and
"Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn
curve is a 256-level map per channel, evaluated piecewise-linearly.

- doc: Adjustment::Curves gains `pencil` (serde default), with
  curve_map / curve_map_outputs helpers.
- compose: curve_map_lut / curve_lut_mode for drawn curves.
- engine: curves params take `pencil` and `smooth`; channels accept
  256-level maps; switching modes draws the spline into maps or fits up to
  16 points to the drawing. Eyedroppers fit points first.
- io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read
  as padding, so pencil curves from Photoshop were misparsed; they are now
  read and written as 256-byte maps, and smart-filter Curves use `Mpng`.
- scorecard UI-217-15 note updated.

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

* Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088)

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>

* fix(io): smooth PSD gradients no longer overshoot their stops (#3028) (#3285)

The Catmull-Rom tangents used for Smoothness are derived from the
neighbouring stops, so a segment between two equal-coloured stops (a
flat plateau) picked up a non-zero tangent and dipped below its own
colour. In the Gradient Map from #3028 the 27%-53% plateau (#1c1c1c)
rendered about 19/255 too dark.

Bound each interpolated channel to the range of the segment's two end
colours. Smoothness still bends the ramp, but never invents a colour
outside the segment, so plateaus stay exactly flat.

The regression test uses the four stop positions and grey values from
the issue (synthetic, no artwork) and fails without the clamp.

* Codecs: decode PNM straight into the budgeted output buffer (#2938) (#3291)

Limits::max_alloc budgets the decoded pixel buffer, but the PNM decoder
collected one u32 per sample first (4x an 8-bit image, plus a u16 copy
for 16-bit) and PFM built a Vec<f32> beside the output bytes. Samples are
now rescaled and written straight into the native-endian output buffer
(reserved fallibly), so the only header-sized allocation is the one
Limits::check already charged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Properties: Curves adjustment layers get the black/gray/white eyedroppers (#3110) (#3115)

* Curves: pencil mode, Smooth and Edit points (#3088)

The Curves editor (Image > Adjustments dialog and the adjustment layer's
Properties panel) gains Photoshop's "Edit points to modify the curve" and
"Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn
curve is a 256-level map per channel, evaluated piecewise-linearly.

- doc: Adjustment::Curves gains `pencil` (serde default), with
  curve_map / curve_map_outputs helpers.
- compose: curve_map_lut / curve_lut_mode for drawn curves.
- engine: curves params take `pencil` and `smooth`; channels accept
  256-level maps; switching modes draws the spline into maps or fits up to
  16 points to the drawing. Eyedroppers fit points first.
- io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read
  as padding, so pencil curves from Photoshop were misparsed; they are now
  read and written as 256-byte maps, and smart-filter Curves use `Mpng`.
- scorecard UI-217-15 note updated.

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

* Properties: Curves adjustment layers get the black/gray/white eyedroppers (#3110)

The Set Black/Neutral Gray/White Point pickers existed only in the
Image > Adjustments > Curves dialog (#873). The Properties editor of a
Curves adjustment layer now has them too (layer_picker.rs): with one
armed, a click on the image runs one layer.setAdjustment with the new
`eyedropper` param, which samples the image below the layer (the layer
hidden, like its Properties histogram) at the Eyedropper's Sample Size,
so a repeated click keeps the curve and the edit is one undoable,
recordable step agents can send directly. Esc, another tool or another
layer disarms it; ui.pointer and ui.inspect (`layerPicker`) cover it.

Scorecard UI-217-15 is done.

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

* Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088)

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>

* Format: admit the declared .pcraft canvas before loading it (#2935) (#3295)

* Format: admit the declared .pcraft canvas before loading it (#2935)

A .pcraft manifest's canvas size was copied into the Document unchecked, so
a few bytes of JSON could declare a 300000x300000 (or u32::MAX) canvas that
the CPU reduction, thumbnails and display textures then work against in
proportion to its area.

The loader now refuses an empty canvas, a side above 300000 (Photoshop's PSB
limit, as for PSD and file.new) and an area above 4 gigapixels, right after
reading the manifest and before any tile or blob is decoded. Both limits are
LoadOptions fields so trusted callers can change them.

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

* Format: admit every canvas the app can create on .pcraft load (#2935)

The default pixel budget was 2^32 (65536 x 65536), but file.new, Canvas
Size, Image Size with Resample off and PSB import all reach 300000 x
300000, so a document a user could create and save would not reopen.
The default is now the full 300000^2 PSB canvas; the side limit still
refuses anything larger, and callers handling untrusted files can lower
either budget. Loading stays proportional to the file: layers keep only
the tiles the bundle holds, so a huge declared canvas allocates nothing
for its area.

A compile-time assert in document_preset_cmds ties file.new's
MAX_DIMENSION to the loader's defaults.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(format): save the target file name in native bundle manifests (#3312) (#3319)

* Levels: black, gray and white point eyedroppers (#3116) (#3128)

* Curves: pencil mode, Smooth and Edit points (#3088)

The Curves editor (Image > Adjustments dialog and the adjustment layer's
Properties panel) gains Photoshop's "Edit points to modify the curve" and
"Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn
curve is a 256-level map per channel, evaluated piecewise-linearly.

- doc: Adjustment::Curves gains `pencil` (serde default), with
  curve_map / curve_map_outputs helpers.
- compose: curve_map_lut / curve_lut_mode for drawn curves.
- engine: curves params take `pencil` and `smooth`; channels accept
  256-level maps; switching modes draws the spline into maps or fits up to
  16 points to the drawing. Eyedroppers fit points first.
- io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read
  as padding, so pencil curves from Photoshop were misparsed; they are now
  read and written as 256-byte maps, and smart-filter Curves use `Mpng`.
- scorecard UI-217-15 note updated.

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

* Properties: Curves adjustment layers get the black/gray/white eyedroppers (#3110)

The Set Black/Neutral Gray/White Point pickers existed only in the
Image > Adjustments > Curves dialog (#873). The Properties editor of a
Curves adjustment layer now has them too (layer_picker.rs): with one
armed, a click on the image runs one layer.setAdjustment with the new
`eyedropper` param, which samples the image below the layer (the layer
hidden, like its Properties histogram) at the Eyedropper's Sample Size,
so a repeated click keeps the curve and the edit is one undoable,
recordable step agents can send directly. Esc, another tool or another
layer disarms it; ui.pointer and ui.inspect (`layerPicker`) cover it.

Scorecard UI-217-15 is done.

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

* Levels: black, gray and white point eyedroppers (#3116)

Image > Adjustments > Levels and a Levels adjustment layer's Properties
panel get Photoshop's Set Black/Neutral Gray/White Point eyedroppers.
Black/white move each channel's input black/white to the sample, gray
sets each channel's midtone gamma so the sample turns neutral at its
luma (Grayscale: composite input, no gray). The engine takes the same
`eyedropper` param as Curves on image.adjustments.levels,
layer.newAdjustmentLayer.levels and layer.setAdjustment.

On a smart object, image.adjustments.levels/curves with `eyedropper`
now record the resolved adjustment as the smart filter instead of the
raw sample, which re-rendered as an identity (#3127).

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

* Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088)

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>

* Puppet Warp: the mesh follows the object outline (#3105) (#3117)

The mesh was every square grid cell touching the layer's opaque pixels:
a staircase up to a cell outside the shape, and shapes closer than about
a cell shared cells, so pinning one dragged the other along.

build_mesh now traces the outline of the opaque region grown (or shrunk)
by Expansion from a signed distance field, simplifies it (Douglas-Peucker,
corners kept), splits its sides to the Density spacing, fills the inside
with a triangular lattice and triangulates the points (Bowyer-Watson
Delaunay, outline sides recovered by midpoint splits), keeping the
triangles inside the outline. Separate shapes get separate meshes, so an
unpinned one stays put, as in Photoshop. Vertices are numbered in reverse
Cuthill-McKee order to keep the solver's banded factor narrow, and the
density's spacing counts the grown region so a large Expansion on a small
layer no longer builds tens of thousands of vertices.

Scorecard TOOL-214-6 done; docs/gaps.md G7 updated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Clone Source: W/H link and overlay blend modes (#3298) (#3310)

The Clone Source panel gets Photoshop's W/H chain (Maintain aspect
ratio): with it on, a new W or H scales the other by the same factor.
cloneSource.set takes "linked" and cloneSource.list reports it.

The overlay's Darken, Lighten and Difference modes were stored but
never drawn. draw_clone_preview now blends the source with the
composite under the brush (clone_source::overlay_blend), so Difference
goes black where source and image line up.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Vanishing Point: on-canvas plane editor with Create/Edit Plane, grid and paste (#3122) (#3133)

Filter > Vanishing Point now opens a dialog mode on the canvas instead of
the generic parameter dialog:

- Create Plane (C): four clicks, in any order, make a plane (corners are
  normalised clockwise from the top-left, the order paste expects).
- Edit Plane (V): drag corner nodes, or edge nodes to slide an edge along
  the plane's own perspective; click a plane to select it, Delete removes it.
- The plane grid is drawn at the Grid Size, in the accent colour when the
  plane is valid, warning yellow when its perspective is doubtful and red
  (outline only) when the quad isn't convex (VpPlane::check).
- With an image on the clipboard the selected plane previews it pasted in
  perspective; drag to move it, Width sets its size, New Layer renders it
  on a new layer. OK / Enter runs filter.vanishingPoint with the planes and
  the paste as one history step; the control channel drives the same
  session through {"ui": {...}}.

The engine now rejects a drawn plane whose corners don't form a convex
quad (bow-tie, dented) instead of warping along it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Let `[` shrink the brush to 1 px (#3229) (#3332)

`[` steps the brush size down by 25 %, rounded: from 2 px that is 1.6,
which rounds back to 2, so the key never reached 1 px and pixel artists
had to type the size. `]` already grows by at least a pixel; `[` now
shrinks by at least a pixel the same way, still never below 1 px.

* MCP: command_batch says it is not atomic, and the docs explain recovery (#3338)

Agents read the tool description, not docs/mcp.md, and the command_batch
description only said it stops at the first error. It now also says the
steps run in order and that earlier successful steps stay applied: there
is no rollback. docs/mcp.md gives the stop_on_error default and a recovery
recipe: inspect with doc_inspect, undo with edit.undo once per history
entry (undo does not reverse files written to disk), or experiment on a
copy made with image.duplicate or history.newDocument. A test checks the
listed description.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Valentin Klein <vangel1976@googlemail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com>
Co-authored-by: Gert Pellin - GPE <switch87@users.noreply.github.com>
Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
Co-authored-by: RaccoonPie-314 <alsorandomkay@gmail.com>
Co-authored-by: Raccoon <235524086+RaccoonPie-314@users.noreply.github.com>
Co-authored-by: Sir Starch <starch@velx.cc>
Co-authored-by: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com>
Co-authored-by: myangaa1 <58806636+myangaa1@users.noreply.github.com>
Co-authored-by: KnottyDyes <47539659+KnottyDyes@users.noreply.github.com>
2026-10-12 10:52:19 -04:00

14 KiB
Raw Blame History

Dialogs

Modal and modeless parameter windows in the egui shell. They mirror Photoshop: no dimmed backdrop, canvas pan/zoom under an open dialog, Tab cycles text and number fields only, Esc cancels, Enter confirms when OK is enabled. Automation drives the same fields map as the UI (ui.dialog.* in control-protocol.md).

Architecture

Piece Location Role
Frame, routing, OK/Cancel/Esc, stacking crates/ui-egui/src/dialogs.rs Single hub for every UiState::dialogs entry
Alt+Cancel → Reset (opt-in) crates/ui-egui/src/dialog_alt_reset.rs Relabels Cancel while Alt is held; dispatches per-marker reset
Tab-through-fields crates/ui-egui/src/field_tab.rs Photoshop-style Tab / ⇧Tab (tests in same file)
State crates/ui-egui/src/state.rs Dialog, DialogKind, open_dialog
Button row crates/ui-egui/src/widgets.rs dialog_buttons, focus_first_field; number_edit registers for Tab
Menu / command entry dialogs::open_command_dialog, menus.rs Picks a dedicated module, schema dialog, or bare confirm
Filters filter_dialog.rs Schema from engine params, live preview
Adjustments adjust_dialog.rs Shared editors with Properties; preview via adjust_preview
Generic forms view_cmds.rs __form grids for simple command params
Dedicated bodies See Command routing below body + usually open / confirm

Routing. Non-command kinds use DialogKind alone. DialogKind::Command bodies are chosen by marker keys on fields (owns() or contains_key("__export"), etc.); first match in dialogs::show wins, so new markers must not collide with an earlier arm. show, confirm, and cancel share the same dispatch table—wire all three when adding a dialog.

Field contract. Dialog.fields is a JSON map: command parameters agents read and write, plus private keys prefixed with __. Never use __ for user-facing or engine params.

DialogKind (top-level stack)

Kind Body module Typical entry
NewDocument new_doc_ui.rs File › New, file.new with no size params
About dialogs.rs (About / Contributors / Models tabs) Help › About PhotoCraft; systemInfo: true → System Info variant
Command Routed—see below Most menu commands with parameters
LayerStyle layer_style.rs Layer › Layer Style…
Error inline in dialogs.rs message field only (e.g. move lock, locked transform)

Command routing (DialogKind::Command)

Dispatch order matches dialogs::show in dialogs.rs (check earlier rows before adding a marker).

Match Module Role
__guideLayout guide_layout_ui.rs View › New Guide Layout…
__fill fill_ui.rs Edit › Fill…
__patternFill pattern_fill_ui.rs Pattern Fill layer / New Fill Layer › Pattern…
__stroke stroke_ui.rs Edit › Stroke…
__createShape shape_dialog.rs Click-with-shape-tool creation (rectangle, ellipse, …)
__deleteLayer delete_layer_prompt.rs Layers panel trash confirmation
__newLayer new_layer_ui.rs Layer › New › Layer… (⇧⌘N)
__rasterize rasterize_prompt.rs Rasterize-before-paint prompt
__resetAllTools tool_reset.rs Reset All Tools confirmation
__cropPresetNew / __cropPresetDelete crop_presets.rs New / delete crop preset
__variables variables_ui.rs Image › Variables (Define, Data Sets, Apply)
__web / __print file_ui.rs Save for Web, Print
__colorPicker color_picker_ui.rs Foreground/background, nested field/command pickers (see nested keys below)
__colorRange color_range_ui.rs Select › Color Range…
__prefsui prefs_ui.rs Edit › Preferences… and related preset/import subdialogs
__export export_dialog.rs Export As (document or layer)
__sizing sizing.rs Image › Image Size… / Canvas Size…
__adjust adjust_dialog.rs Image › Adjustments… (also sets __filter + __preview for live preview)
__filter filter_dialog.rs (+ select_mask_view.rs when __command is select.refineEdge) Filter Gallery commands, Edit › Color Settings…, Select and Mask, and other schema dialogs
__form view_cmds.rs Simple param grids (e.g. slice options, many Image › Apply Image-style commands)
(none of the above) — Bare confirm: title + OK/Cancel only; OK runs __command with fields via generic confirm

Menu shortcut. dialogs::open_command_dialog opens guide layout, stroke, and color range directly; then tries adjust_dialog::open, then filter_dialog::open; otherwise a bare confirm with __command + __label. Other dedicated modules are opened from menus.rs, tools, or panel actions with their marker already set.

Shared private keys

Key Meaning
__command Engine command id run on OK (bare confirm, filters, sizing, …)
__label Menu/catalog label for the title
__document Target document id (validate staleness)
__dialog / __field Nested color picker: parent dialog id and field name to write on OK
__params Extra command params for command-targeted color pickers
__preview Live canvas preview enabled (filters and adjustments)
__choices Dropdown options for __form dialogs
__filter Filter-schema body (filter_dialog); also set on adjustments for preview plumbing
__adjust Image › Adjustments editor kind (adjust_dialog)
__form Generic view_cmds::form_body
__formOrig Snapshot of non-__ params at open; Alt+Reset restores it for __form dialogs
__variablesOrig Snapshot of __variables at open; Alt+Reset restores it
__altReset Set to false to opt a dialog out of Alt+Cancel → Reset
__export Export As
__sizing Image Size / Canvas Size

Routing markers (dedicated Command bodies)

Key Module
__guideLayout guide_layout_ui
__fill fill_ui
__patternFill pattern_fill_ui
__stroke stroke_ui
__createShape shape_dialog
__deleteLayer delete_layer_prompt
__newLayer new_layer_ui
__rasterize rasterize_prompt
__resetAllTools tool_reset
__cropPresetNew, __cropPresetDelete crop_presets
__variables variables_ui
__web, __print file_ui
__colorPicker color_picker_ui
__colorRange color_range_ui
__prefsui prefs_ui (string sub-kind: prefs, presetsIO, …)

Nested color pickers (__colorPicker on the stack)

Same hub entry as standalone pickers; color_picker_ui::confirm branches on extra keys:

Extra key Behaviour
__layerStyleColor Layer Style child; OK writes effect colour on parent LayerStyle dialog
__textStyleColor Character/paragraph style colour (type panels)
__shapeFill Shape layer solid fill via shape.edit
__command = layer.fill.solidColor.set Solid Color fill layer (no __shapeFill)
__dialog + __field OK updates one field on a parent command dialog (e.g. Fill › Color…)

Parallel modal systems (not the main stack)

System Location When
Unsaved changes discard_ui.rs Close, Revert, Close Others/All, Exit—Save / Don't Save / Cancel
Shell dialog control.rs, ui.inspect id "shell" Workspace, custom pixel aspect, show-extras options
Camera Raw camera_raw_ui.rs Separate OS window on desktop; see control-protocol § Camera Raw
Error message DialogKind::Error e.g. move_lock—message + OK
Nested pickers color_picker_ui (+ markers in Nested color pickers) Stack on parent; parent confirm refused while child open

Rules

Architecture and commands

  1. Thin UI, engine command on OK. Run app.run or a dedicated *_ui::confirm; one history step unless the command itself batches.
  2. Register in the hub. Add owns() (or a stable marker), body(), open() if needed, and matching arms in dialogs::show, confirm, and cancel (and open_command_dialog when menu-driven).
  3. Extend existing paths first. Filters → filter_dialog; adjustments → adjust_dialog; simple param grids → __form; avoid a second modal stack unless product needs Camera Raw–level UX.
  4. fields is the automation contract. Anything agents must drive lives in named fields, not only in local egui state.
  5. Remember last-used values after successful OK where Photoshop does (filter_dialog::remember, prefs dialogs["…"], module-specific helpers).

Chrome and layout

  1. Use dialog_buttons for OK / Cancel / Apply—platform order via ButtonRole (macOS default action in the corner).
  2. Respect width rules in dialogs::show unless file_ui::dialog_width, color picker, or kind-specific mins apply (typical 380–440; Layer Style / export / picker up to 600; New Document 800).
  3. Scroll long bodies inside the content (DIALOG_CHROME for filters); title and button row stay visible.
  4. Transparent backdrop—no dimming; previews are judged at true contrast.
  5. Position: first open centered; then persist offset (place_id per command or kind); title bar drag; clamp on screen.
  6. Stacking: only the top dialog is interactive; non-top instances render disabled; raise the live dialog if an older modal steals focus.

Keyboard and focus

  1. Tab / ⇧Tab: text and number fields only. Frame calls field_tab::begin / end; each field calls field_tab::register (number_edit does this; custom TextEdit must register manually).
  2. Do not rely on egui default Tab through sliders, dropdowns, checkboxes, or preset cards.
  3. Open on first value: first_field_focused + widgets::focus_first_field for value-first dialogs (most filters; Image/Canvas Size; not Select and Mask view keys).
  4. Select-all on focus when reached via Tab or field arrows (field_tab::select_all; see value_field, new_doc_ui).
  5. Enter confirms when OK is enabled (can_ok); exceptions must be explicit (guide_layout_ui, color picker).
  6. Esc on topmost dialog with no popup open → Cancel. Click outside does not dismiss (canvas may still pan/zoom via free_pointer_over, pan_delta, free_press). Esc ignores Alt (it does not become Reset).
  7. Alt turns Cancel into Reset when the dialog opts in via dialog_alt_reset::supported (dialog stays open). Opt-in markers: __guideLayout, __fill, __variables, __colorPicker, __sizing, __adjust (all kinds), __filter, __form. Each module implements its own reset; set "__altReset": false to disable.

Canvas and preview

  1. Live preview for filters/adjustments uses filter_preview / adjust_preview; cancel() clears preview and related shell state.
  2. Under-dialog canvas respects dialog rects and open popups; Hand / Space / middle-button pan on free canvas only.

Labels and i18n

  1. Titles: command dialogs use __label (catalogued; trailing … stripped in chrome); fixed kinds use dialogs::title + tl!.
  2. User-visible strings: tl!("…"); command labels from the menu catalog where possible.

Confirm / cancel safety

  1. Child dialogs first—refuse parent confirm if a nested color picker is open; cancel parent closes children where implemented.
  2. Validate before close when params can be inconsistent (guide_layout_ui::validate in confirm).
  3. Disable OK when required input is missing (crop_presets::can_confirm, Preferences Apply when unchanged).
  4. Preferences over control: whole-dialog OK/Apply may be refused; safe single prefs via prefs.set (see control-protocol).

Testing

  1. egui_kittest: open dialog, run_steps, key presses, assert fields or accessibility roles (field_tab tests are the reference harness).
  2. Tab-order regressions: add tests when a dialog gains multiple text/number fields.
  3. Filter preview: dialogs.rs tests for preview persistence across background jobs.

Anti-patterns

  1. No panics in body / confirm on bad input—errors to status or validation.
  2. No hard-coded colours or radii—theme::Tokens and widgets.rs.
  3. Do not skip confirm dispatch—Enter and ui.dialog.confirm use it.
  4. Do not expect click-outside-to-close on egui::Modal.
  5. Do not register sliders or dropdowns for field_tab unless product explicitly changes Photoshop parity.

Adding a command dialog (checklist)

  1. Choose path: see Command routing (adjust_dialog, filter_dialog, __form, __sizing, …, or new *_ui with a unique marker).
  2. open: build fields (__command, __label, params, marker); app.ui.open_dialog(DialogKind::Command, f).
  3. body(app, ui, &mut fields)—mutate fields only; use shared widgets.
  4. confirm / cancel arms in dialogs.rs (or delegate to module confirm).
  5. Tab: register every text/number field; use value_field / number_edit where possible.
  6. Alt+Reset (optional): implement reset on the module and add an arm in dialog_alt_reset (same dispatch order as dialogs::show). Snapshot open values under a __*Orig key when Reset should restore “as opened” rather than factory defaults.
  7. Tests: at least open + confirm or Tab sequence if non-trivial; if Alt+Reset is supported, assert Reset restores fields and leaves the dialog open.
  8. If menu-visible: command already in engine; menu id opens dialog via existing menu wiring.

Known gaps

  • Tab registration for plain TextEdit is manual; generic __form string fields may not join field_tab unless wired.
  • “Dialog module” is any UI routed from dialogs::show, not only files named *dialog*.
  • Numeric field parity (units in expressions, scrubby labels) is tracked in ui-parity.md (Navigation / numeric fields), not duplicated here.

Revision history

Date Change
2026-10-11 Initial doc from dialog codebase audit
2026-10-11 Document all DialogKind values and Command routing markers
2026-10-11 Alt+Cancel → Reset hub (dialog_alt_reset); opt-in markers