mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-19 13:35:25 +02:00
⇧⌘N opens the New Layer dialog, as Layer › New › Layer… does in Photoshop. Photoshop's companion shortcut, the same keys with ⌥ (Ctrl+Alt+Shift+N on Windows and Linux), makes the layer at once; PhotoCraft didn't handle it, so there was no way to get 0.7.0's dialog-free behaviour from the keyboard. New Layer's shortcut with ⌥ added now runs layer.new.layer directly. It follows a rebound New Layer shortcut, and a command bound to that key itself keeps it. Fixes #4794 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
14 KiB
Markdown
220 lines
14 KiB
Markdown
# 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`](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; ⌥⇧⌘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, 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
|
||
|
||
6. **Use `dialog_buttons`** for OK / Cancel / Apply—platform order via `ButtonRole` (macOS default action in the corner).
|
||
7. **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).
|
||
8. **Scroll long bodies** inside the content (`DIALOG_CHROME` for filters); title and button row stay visible.
|
||
9. **Transparent backdrop**—no dimming; previews are judged at true contrast.
|
||
10. **Position:** first open centered; then persist offset (`place_id` per command or kind); title bar drag; clamp on screen.
|
||
11. **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
|
||
|
||
12. **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).
|
||
13. **Do not rely on egui default Tab** through sliders, dropdowns, checkboxes, or preset cards.
|
||
14. **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).
|
||
15. **Select-all on focus** when reached via Tab or field arrows (`field_tab::select_all`; see `value_field`, `new_doc_ui`).
|
||
16. **Enter** confirms when OK is enabled (`can_ok`); exceptions must be explicit (`guide_layout_ui`, color picker).
|
||
17. **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).
|
||
18. **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
|
||
|
||
19. **Live preview** for filters/adjustments uses `filter_preview` / `adjust_preview`; `cancel()` clears preview and related shell state.
|
||
20. **Under-dialog canvas** respects dialog rects and open popups; Hand / Space / middle-button pan on free canvas only.
|
||
|
||
### Labels and i18n
|
||
|
||
21. **Titles:** command dialogs use `__label` (catalogued; trailing `…` stripped in chrome); fixed kinds use `dialogs::title` + `tl!`.
|
||
22. **User-visible strings:** `tl!("…")`; command labels from the menu catalog where possible.
|
||
|
||
### Confirm / cancel safety
|
||
|
||
23. **Child dialogs first**—refuse parent confirm if a nested color picker is open; cancel parent closes children where implemented.
|
||
24. **Validate before close** when params can be inconsistent (`guide_layout_ui::validate` in `confirm`).
|
||
25. **Disable OK** when required input is missing (`crop_presets::can_confirm`, Preferences Apply when unchanged).
|
||
26. **Preferences over control:** whole-dialog OK/Apply may be refused; safe single prefs via `prefs.set` (see control-protocol).
|
||
|
||
### Testing
|
||
|
||
27. **egui_kittest:** open dialog, `run_steps`, key presses, assert `fields` or accessibility roles (`field_tab` tests are the reference harness).
|
||
28. **Tab-order regressions:** add tests when a dialog gains multiple text/number fields.
|
||
29. **Filter preview:** `dialogs.rs` tests for preview persistence across background jobs.
|
||
|
||
### Anti-patterns
|
||
|
||
30. No panics in `body` / `confirm` on bad input—errors to status or validation.
|
||
31. No hard-coded colours or radii—`theme::Tokens` and `widgets.rs`.
|
||
32. Do not skip `confirm` dispatch—Enter and `ui.dialog.confirm` use it.
|
||
33. Do not expect click-outside-to-close on `egui::Modal`.
|
||
34. 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`](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 |
|