* 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>
14 KiB
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
- Thin UI, engine command on OK. Run
app.runor a dedicated*_ui::confirm; one history step unless the command itself batches. - Register in the hub. Add
owns()(or a stable marker),body(),open()if needed, and matching arms indialogs::show,confirm, andcancel(andopen_command_dialogwhen menu-driven). - 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. fieldsis the automation contract. Anything agents must drive lives in named fields, not only in local egui state.- Remember last-used values after successful OK where Photoshop does (
filter_dialog::remember, prefsdialogs["…"], module-specific helpers).
Chrome and layout
- Use
dialog_buttonsfor OK / Cancel / Apply—platform order viaButtonRole(macOS default action in the corner). - Respect width rules in
dialogs::showunlessfile_ui::dialog_width, color picker, or kind-specific mins apply (typical 380–440; Layer Style / export / picker up to 600; New Document 800). - Scroll long bodies inside the content (
DIALOG_CHROMEfor filters); title and button row stay visible. - Transparent backdrop—no dimming; previews are judged at true contrast.
- Position: first open centered; then persist offset (
place_idper command or kind); title bar drag; clamp on screen. - 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
- Tab / ⇧Tab: text and number fields only. Frame calls
field_tab::begin/end; each field callsfield_tab::register(number_editdoes this; customTextEditmust register manually). - Do not rely on egui default Tab through sliders, dropdowns, checkboxes, or preset cards.
- Open on first value:
first_field_focused+widgets::focus_first_fieldfor value-first dialogs (most filters; Image/Canvas Size; not Select and Mask view keys). - Select-all on focus when reached via Tab or field arrows (
field_tab::select_all; seevalue_field,new_doc_ui). - Enter confirms when OK is enabled (
can_ok); exceptions must be explicit (guide_layout_ui, color picker). - 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). - 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 ownreset; set"__altReset": falseto disable.
Canvas and preview
- Live preview for filters/adjustments uses
filter_preview/adjust_preview;cancel()clears preview and related shell state. - Under-dialog canvas respects dialog rects and open popups; Hand / Space / middle-button pan on free canvas only.
Labels and i18n
- Titles: command dialogs use
__label(catalogued; trailing…stripped in chrome); fixed kinds usedialogs::title+tl!. - User-visible strings:
tl!("…"); command labels from the menu catalog where possible.
Confirm / cancel safety
- Child dialogs first—refuse parent confirm if a nested color picker is open; cancel parent closes children where implemented.
- Validate before close when params can be inconsistent (
guide_layout_ui::validateinconfirm). - Disable OK when required input is missing (
crop_presets::can_confirm, Preferences Apply when unchanged). - Preferences over control: whole-dialog OK/Apply may be refused; safe single prefs via
prefs.set(see control-protocol).
Testing
- egui_kittest: open dialog,
run_steps, key presses, assertfieldsor accessibility roles (field_tabtests are the reference harness). - Tab-order regressions: add tests when a dialog gains multiple text/number fields.
- Filter preview:
dialogs.rstests for preview persistence across background jobs.
Anti-patterns
- No panics in
body/confirmon bad input—errors to status or validation. - No hard-coded colours or radii—
theme::Tokensandwidgets.rs. - Do not skip
confirmdispatch—Enter andui.dialog.confirmuse it. - Do not expect click-outside-to-close on
egui::Modal. - Do not register sliders or dropdowns for
field_tabunless product explicitly changes Photoshop parity.
Adding a command dialog (checklist)
- Choose path: see Command routing (
adjust_dialog,filter_dialog,__form,__sizing, …, or new*_uiwith a unique marker). open: buildfields(__command,__label, params, marker);app.ui.open_dialog(DialogKind::Command, f).body(app, ui, &mut fields)—mutatefieldsonly; use shared widgets.confirm/cancelarms indialogs.rs(or delegate to moduleconfirm).- Tab: register every text/number field; use
value_field/number_editwhere possible. - Alt+Reset (optional): implement
reseton the module and add an arm indialog_alt_reset(same dispatch order asdialogs::show). Snapshot open values under a__*Origkey when Reset should restore “as opened” rather than factory defaults. - 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.
- If menu-visible: command already in engine; menu id opens dialog via existing menu wiring.
Known gaps
- Tab registration for plain
TextEditis manual; generic__formstring fields may not joinfield_tabunless 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 |