mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 10:11:58 +01:00
200 lines
17 KiB
Markdown
200 lines
17 KiB
Markdown
# UI design system
|
||
|
||
## Themes
|
||
|
||
| Theme | Intent |
|
||
|---|---|
|
||
| Pro | Photoshop-style Spectrum dark: flat charcoal panels (#323232), dark tab strips, Spectrum blue accent (#378ef0), pill buttons, checkboxes, compact 12 px type |
|
||
| Pro Medium | Photoshop-style medium-gray panels and a dark canvas |
|
||
| Studio | Dark studio style: near-black, rounded cards, pill tabs, violet accent, toggles |
|
||
| Studio Light | Studio on light surfaces |
|
||
| Classic | Windows-2000 bevels, square corners, navy selection |
|
||
| Adwaita | GNOME's libadwaita light palette on the Studio layout: white header bar and cards on a grey sidebar, GNOME blue accent (#3584e4), 6 / 9 / 12 px radii, grey (not red) close button |
|
||
| Adwaita Dark | The libadwaita dark palette on the same layout |
|
||
| Solarized Dark | Ethan Schoonover's Solarized palette: base03 canvas, base02 panels, base0 text, Solarized blue accent |
|
||
|
||
Edit → Preferences → Interface shows an Appearance Mode selector (Sync with system, Dark, Light) above separate light and dark theme cards with PhotoCraft editor previews and radio choices. Auto is opt-in and follows the operating system while the app is running. On Linux it reads the desktop portal once and then listens for its `SettingChanged` signal (no polling, and the UI repaints only when the value changes); `gsettings` runs at most once at start-up, by absolute path, when the portal gives no answer. A missing system appearance falls back to Dark. New installs keep Photoshop's default: Dark mode with Pro Medium (Studio Light is the saved light theme). Existing saved single-theme preferences migrate to a fixed Dark or Light mode with their chosen theme. The header appearance button cycles Auto → Light → Dark; its icon shows a monitor, moon or sun for the selected mode. Window → Theme and `ui.set {"theme":"classic"}` select a theme and fix the mode to its light or dark family.
|
||
|
||
Window → Theme and macOS PhotoCraft → Appearance also offer Sync with system. Its checkmark follows the saved Auto mode rather than the displayed palette; choosing a manual theme selects its fixed appearance mode.
|
||
|
||
The toolbar's foreground and background colour chips follow the `Tokens::round_chips` flag. Pro and Classic leave it off and draw Photoshop's overlapping squares with Default Colors and Switch Colors above them. Studio, Studio Light, Solarized Dark, Adwaita and Adwaita Dark (the Studio-layout themes) turn it on and draw large round chips with a curved Switch Colors arrow centred right under them, its heads touching the chips: stacked vertically in one tool column, with Default Colors at their top-right, and side by side in two tool columns, with Default Colors at the toolbar's left edge. Another non-Pro theme can opt in by setting the flag.
|
||
|
||
## Rules
|
||
|
||
- **Colours and radii come from tokens.** Read them with `Tokens::get(ctx)`; never hard-code a colour in a widget.
|
||
- **Shared widgets live in `widgets.rs`:** `card` (panel group; Pro renders a Photoshop tab strip), `value_field`, `slider`/`slider_row`, `toggle`/`checkbox`, `primary_button`/`secondary_button`, `dropdown` (with a chevron icon), `hairline`/`vline`.
|
||
- **Icons:** Lucide SVGs in `assets/icons` (ISC licence), embedded via `icon_data.rs`. Regenerate that file when you add icons.
|
||
- **Fonts:** Inter (UI) and JetBrains Mono (numbers), both OFL. Named families `medium` and `semibold` are available via `theme::medium()` and `theme::semibold()`.
|
||
- **Photoshop layout grammar (Pro):**
|
||
- Essentials dock order: Color | Swatches, then Properties | Adjustments, then Layers | Channels | Paths (Layers fills the remaining height).
|
||
- Options-bar labels end with a colon ("Size:").
|
||
- Document tabs read "name @ 12.5% (RGB/8)".
|
||
- Toolbar tool groups carry a corner triangle.
|
||
- **Verify every visual change** with `ui.screenshot`, at several window sizes and in every theme.
|
||
|
||
## Adding icons
|
||
|
||
```sh
|
||
curl -sfL -o assets/icons/<name>.svg https://raw.githubusercontent.com/lucide-icons/lucide/main/icons/<name>.svg
|
||
# regenerate the embedded table
|
||
{ printf '%s\n' '//! Lucide icons (ISC licence), embedded and tinted at runtime.' '' 'pub static ICONS: &[(&str, &[u8])] = &['; \
|
||
for f in assets/icons/*.svg; do n=$(basename $f .svg); echo " (\"$n\", include_bytes!(\"../../../assets/icons/$n.svg\")),"; done; echo '];'; } \
|
||
> crates/ui-egui/src/icon_data.rs
|
||
```
|
||
|
||
## Interaction models (match Photoshop CC)
|
||
|
||
| Feature | Module | Behaviour |
|
||
|---|---|---|
|
||
| Type tool | `type_tool.rs` | Click: point text with the placeholder "Lorem Ipsum" selected. Drag: paragraph box. Inline caret and selection drawn from the text engine layout. ⌥/⌘ word and line navigation, ↩ newline, ⌘↩ or Esc commits, a click outside commits. One history step per session (`coalesce`). A new layer is named after its text; an empty one is deleted. |
|
||
| Free Transform | `transform_tool.rs` | ⌘T. Corners and edges scale proportionally (⇧ frees them: an edge then stretches one axis); ⌥ scales about the reference point; ⌘-corner distorts; dragging outside rotates (⇧ snaps to 15°); dragging inside moves. Preview = document without the moving pixels + a textured 24×24 mesh. ↩ or a double-click commits via `edit.transform {rect, quad}`. |
|
||
| Quick layer pick | `quick_pick.rs` | macOS: ⌘⌥⌃-click with any tool selects the topmost visible layer with pixels under the pointer (`layer.pickAt`) without switching tools; the drag and release are swallowed. Windows/Linux have no third modifier distinct from the ⌃⌥ brush resize, so the gesture is a no-op there (use the Move tool's Auto-Select). A failed pick is a status-bar error. |
|
||
| Layers rows | `panels.rs` | Double-click: on the name renames in place; on the Background makes it a normal layer; on an adjustment or fill thumbnail opens its Properties; on a Smart Object thumbnail opens its contents (Edit Contents); anywhere else on the row opens Layer Style. |
|
||
| Layer masks | `panels.rs` | Clicking the mask thumbnail targets the mask (corner-bracket frame; the tab reads "Layer, Layer Mask/8"). Brush, eraser (paints background colour), gradient, bucket and moving the selected pixels (⌘-drag, `select.float`; the hole takes the background colour) then send `"target": "mask"`. Adjustment and fill layers target their mask automatically. |
|
||
| Levels / Curves | `tone.rs` | Histogram of the image *below* the adjustment. Curves: click to add a point, drag out to delete. Every change is a coalesced `layer.setAdjustment`, so one drag = one undo step and the canvas updates at full resolution on the GPU. |
|
||
|
||
While editing type, hold Ctrl (Windows/Linux) or Command (macOS) for an oriented transform
|
||
frame. Drag inside to move, a corner to scale (Shift keeps proportions; Alt/Option scales about
|
||
the reference point), a middle handle to skew, or just outside the frame to rotate (Shift snaps
|
||
to 15°). Drag the reference point to move the rotation centre. These Type gestures follow
|
||
[Adobe's type guide](https://helpx.adobe.com/photoshop/using/creating-type.html), independently
|
||
of the Free Transform preference: Ctrl/Cmd never distorts a Type corner.
|
||
|
||
Without Ctrl/Cmd, paragraph handles resize the container and reflow its text. Temporary
|
||
transforms keep its logical dimensions and all character/paragraph styles. A drag begun with
|
||
Ctrl/Cmd owns the pointer until button release, even if the modifier is released first. Its
|
||
preview does not change the document; release applies one coalesced `type.edit`, and typing
|
||
continues in the same layer. Escape during a drag cancels that preview; focus loss or changes
|
||
to its source also discard it. Holding the modifier, moving the reference point, or returning
|
||
the pointer to its start creates no history entry. Ctrl/Cmd+T while typing still toggles the
|
||
Character panel.
|
||
|
||
Where the font lacks a symbol (e.g. ∠ ↦ ▔), draw it with the painter or use a Lucide icon; never ship
|
||
missing-glyph boxes. Check every new panel with the offscreen snapshot tool (`docs/development.md`).
|
||
|
||
## Title bar
|
||
|
||
The app's top bar (`panels::title_bar`) starts with the brand mark (the app icon, `brand.rs`), then the menus, the document title and the workspace controls, like Photoshop's Ps tile and menu row.
|
||
|
||
- **Windows and Linux:** the window has no OS decorations, so there is one bar, not two stacked. The top bar is the title bar (`titlebar.rs`): Minimize, Maximize/Restore and Close (red on hover, `caption_close` tokens) sit flush in the window's top-right corner, the free gap between the menus and the controls drags the window and a double-click there maximizes it, and invisible 5 pt edges (12 pt corners) resize it. Close runs File › Exit, so unsaved documents are asked about first.
|
||
- **macOS:** the traffic lights sit over the integrated title strip (`integrated_titlebar`).
|
||
- **Narrow windows** drop controls that are also in a menu before anything overlaps: Discord (Help › Discord), then the theme toggle, then search (Edit › Search), then the workspace switcher narrows (Window › Workspace).
|
||
|
||
## Menus
|
||
|
||
`menu_catalog.rs` holds Photoshop's menu tree (standard command names, order, separators, default shortcuts). Items whose id matches an engine or UI command are live; others render disabled until implemented. Give new commands the catalogue's id (for example `image.imageSize`) and they light up in the right place automatically.
|
||
|
||
Menus never run off the window: the menu bar's menus and submenus scroll with arrows (`menu_nav::level`), and long right-click menus (Layers, canvas tools, Channels, Paths, document tabs) wrap their rows in `widgets::menu_scroll`, so they move up to fit and scroll only when taller than the visible window.
|
||
|
||
## Automation for visual checks
|
||
|
||
Use `ui.click {x,y}`, `ui.move`, `ui.key` and `ui.type` (synthetic input in screen points) to open menus, popups and context menus, then `ui.screenshot`.
|
||
|
||
## Font menus
|
||
|
||
The Type options bar, Character panel, Character/Paragraph Style editors and Glyphs panel
|
||
share a searchable family picker. Each visible row shows an `AaBbCc` sample rendered by
|
||
PhotoCraft's text engine in that family (script/symbol fonts use characters they support).
|
||
Samples are cached with a bounded cache and follow display scale and theme text colour.
|
||
Builds without system fonts preview the bundled fonts.
|
||
While the menu is open, Up/Down applies the previous/next matching family and scrolls it into
|
||
view; Enter accepts the selection and closes the menu. Escape closes it, keeping already
|
||
applied changes (Edit › Undo restores text-layer font changes).
|
||
|
||
## Preferences
|
||
|
||
Preferences has **Apply**, **OK** and **Cancel**. Apply saves the edited sections and keeps the
|
||
dialog open; it is disabled when the values match the saved preferences. OK saves and closes.
|
||
Cancel discards only changes made since the last successful Apply. A failed Apply leaves the
|
||
draft open for correction. Settings marked for the next launch still require a restart.
|
||
On Windows and Linux, System Title Bar displays “Applies at next launch.” beneath its checkbox,
|
||
including before editing and after Apply. Saving this preference does not restart the app or
|
||
change the current window's decorations; the next launch reads the saved choice.
|
||
|
||
## High DPI and 4K displays
|
||
|
||
Edit → Preferences → Interface → UI Scale applies immediately. Auto follows the operating
|
||
system's display scale, including fractional scales such as 125%, 150% and 175% (on Linux,
|
||
Wayland's fractional scale or X11's `Xft.dpi`). Only when the system reports no scaling (100%) on
|
||
a 4K or larger monitor does Auto use 200%, so text and controls remain readable. Detection uses the current monitor,
|
||
including portrait displays, and updates when the window moves between monitors. If monitor
|
||
size is unavailable, Auto follows system DPI. The fixed choices (75% to 300%) set an absolute UI
|
||
scale, allowing large 4K displays to use smaller controls when desired. Canvas zoom shortcuts
|
||
continue to control the document independently of UI scaling.
|
||
|
||
Interface → UI Font Size changes interface text independently of UI Scale and canvas zoom.
|
||
Tiny, Small (the default), Medium and Large use 10/12, 1, 14/12 and 16/12 of the theme's original
|
||
font sizes, preserving the relative sizes of headings, captions and numeric fields. Apply or OK
|
||
updates text immediately; the choice persists across launches and theme changes. CJK fallback
|
||
fonts use the same size, including fonts loaded after the preference changes.
|
||
|
||
## Localisation
|
||
|
||
Strings in code stay English and are the default lookup keys. `crates/ui-egui/src/i18n` maps them
|
||
to display text at render time from one catalog per language (`i18n/<code>.tsv`; the format is
|
||
documented in the header of `ja.tsv`). Command ids, menu paths used for logic, the control channel,
|
||
the CLI and MCP always use the English ids and labels.
|
||
|
||
Languages shipped (`complete_menus` marks a catalog that covers every menu string and `tl!` literal;
|
||
the tests enforce it):
|
||
|
||
- English (`en`), the source language.
|
||
- Japanese (`ja`), complete.
|
||
- Simplified Chinese (`zh-hans`; see [`localization-zh-hans.md`](localization-zh-hans.md)), complete.
|
||
- Traditional Chinese (`zh-hant`), complete, in the vocabulary used in Taiwan; `zh-TW`, `zh-HK`,
|
||
`zh-MO` and `zh-Hant-*` locales all resolve to it. The resolver distinguishes the two Chinese
|
||
scripts, so neither catalog is shown to the other script's locales.
|
||
- Spanish (`es`), complete.
|
||
- Russian (`ru`), complete, with three plural forms (`one|few|many`, see `plural_russian`).
|
||
- Korean (`ko`), complete, with one plural form.
|
||
- French (`fr`), complete, with two plural forms (0 and 1 use the singular).
|
||
- Czech (`cs`), complete, with three plural forms.
|
||
- Indonesian (`id`), complete, with one plural form.
|
||
|
||
- `tr(lang, s)` plain strings; `tr_ctx` when one English word needs different translations;
|
||
`tr_id(lang, command_id, label)` for menu items (keyed by command id, English label as the
|
||
fallback); `trn(lang, n, one, other)` for plurals; `fmt` fills `{name}` placeholders, which
|
||
translators may reorder.
|
||
- The language is Preferences › Interface › Language (`interface.language`: `auto` or a language
|
||
code; `auto` follows the system locale, an unknown code falls back to `auto`).
|
||
- Language changes apply without a restart; Preferences previews the chosen language until
|
||
Apply or OK commits it. Cancel restores the committed language. Regional preference tags
|
||
resolve through the same locale matcher. See [`localization.md`](localization.md).
|
||
- To add a language: add `<code>.tsv` and one row in `i18n::LANGUAGES` (code, native name, catalog,
|
||
plural rule). The dropdown, locale matching and the catalog tests (well-formed, no duplicates,
|
||
placeholders and ellipses agree, command ids exist) pick it up. Set `complete_menus` once every
|
||
menu string is translated; a test then enforces it. A language with more plural forms lists them
|
||
all in `@plural` entries (Russian: `one|few|many`); the row's plural rule picks the form.
|
||
- Translations are clean-room: written from the meaning of the English text in ordinary vocabulary,
|
||
never from another product's localisation resources.
|
||
|
||
Localised so far: menus, the command palette, dialogs and panels (literals wrapped in `tl!("…")`;
|
||
widgets such as `checkbox`, `slider_row`, `dropdown` and the buttons translate their labels
|
||
themselves). A test fails when a `tl!` literal, a menu string, a blend mode name, brush section name or a generated
|
||
preference label has no entry in a language marked `complete_menus`. Not translated: status-bar
|
||
messages and errors (they stay English, also for agents), names that are user data (layers, styles,
|
||
documents), strings assembled with `format!` that were not converted to `fmt`/`trn`. Not done yet:
|
||
right-to-left layout,
|
||
locale-aware number and date formats, automatic language detection on the web build (native
|
||
builds read `LANG`/`LC_*`, the macOS preferred languages and the Windows user locale).
|
||
|
||
Camera Raw has explicit coverage for all ten available languages, including Korean and
|
||
Simplified Chinese. Its contextual `cameraRaw` entries distinguish tonal regions,
|
||
colour-band names and font-weight Light. The title uses a `{layer}` placeholder; user layer
|
||
names and the universal RGB/Lab channel symbols stay unchanged. Collapse IDs use English
|
||
source keys, so switching languages preserves open sections. Mixer tabs wrap within the
|
||
panel width. Catalog coverage and actual shell language-switch tests enforce these rules.
|
||
|
||
Native CJK font fallback follows the selected UI script and resets its cache when switching
|
||
languages; font delivery on the web remains separate work.
|
||
|
||
## Arithmetic in numeric fields
|
||
|
||
Click a numeric value and type an expression, then press Enter or move focus to finish.
|
||
Numeric fields accept `+`, `-`, `*`, `/`, remainder `%`, powers `^` or `**`, parentheses,
|
||
scientific notation, and `pi` / `tau`. For example, `1920/2` gives `960`, and `(30+15)*2`
|
||
gives `90`. Shared value fields apply plain numbers live and arithmetic when editing
|
||
finishes, preserving rounded/integer workflows. Field limits and integer rounding
|
||
still apply; invalid expressions, division by zero, and non-finite results are rejected.
|
||
Expressions are evaluated locally as arithmetic, never as scripts.
|