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… |
__newGuide |
new_guide_ui.rs |
New Guide / Edit Selected Guides; typed positions and guide colours |
__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 |
__replaceSaved |
replace_prompt.rs |
Replace prompt when a save's added or lowercased extension names an existing file |
__newLayer |
new_layer_ui.rs |
Layer › New › Layer… (⇧⌘N; ⌥⇧⌘N makes the layer without it) |
__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 (reopens on the preview tab chosen last, #9174), 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
New Guide and Edit Selected Guides keep the Position field in the Tab/Shift+Tab order and select
its text on focus. Typed unit suffixes use the existing engine parser; bare numbers use the ruler
unit. An untouched edit preserves the guide's exact pixel position behind its rounded readout,
including when changing only its colour. Once Position is edited, the typed value is intentional,
even if it matches that original readout. Confirming an unchanged guide preserves Redo.
| 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 |
__replaceSaved |
replace_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.run or 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 in dialogs::show, confirm, and cancel (and open_command_dialog when 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.
fields is 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, prefs dialogs["…"], module-specific helpers).
Chrome and layout
- Use
dialog_buttons for OK / Cancel / Apply—platform order via ButtonRole (macOS default action in the corner).
- 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).
- Scroll long bodies inside the content (
DIALOG_CHROME for 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_id per 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 calls field_tab::register (number_edit does this; custom TextEdit must 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_field for 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; see value_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 own reset; set "__altReset": false to 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 use dialogs::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::validate in confirm).
- 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, assert fields or accessibility roles (field_tab tests are the reference harness).
- Tab-order regressions: add tests when a dialog gains multiple text/number fields.
- Filter preview:
dialogs.rs tests for preview persistence across background jobs.
Anti-patterns
- No panics in
body / confirm on bad input—errors to status or validation.
- No hard-coded colours or radii—
theme::Tokens and widgets.rs.
- Do not skip
confirm dispatch—Enter and ui.dialog.confirm use it.
- Do not expect click-outside-to-close on
egui::Modal.
- Do not register sliders or dropdowns for
field_tab unless product explicitly changes Photoshop parity.
Adding a command dialog (checklist)
- Choose path: see Command routing (
adjust_dialog, filter_dialog, __form, __sizing, …, or new *_ui with a unique marker).
open: build fields (__command, __label, params, marker); app.ui.open_dialog(DialogKind::Command, f).
body(app, ui, &mut fields)—mutate fields only; use shared widgets.
confirm / cancel arms in dialogs.rs (or delegate to module confirm).
- Tab: register every text/number field; use
value_field / number_edit where possible.
- 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.
- 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
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-11-06 |
New Guide / Edit Selected Guides: Position Tab focus and exact edit-position handling |
| 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 |