M1: keyboard shortcut parity suite and bindings
- A shortcut parity table (`crates/engine/src/shortcut_parity_tests.rs`): one row per shortcut in
the public documentation (help-centre feature articles: keyboard layouts, text properties,
alignment, frames, components, prototypes, plugins, keyboard navigation), with the macOS and the
Windows/Linux keys, what handles it here (keymap command, held key/gesture listed in the panel,
vector edit tool key, or missing), and a note in our own words. Rows the documentation is silent
on pin today's keys, so every default binding is in the table.
- 129 documented rows: 126 bound (110 before), 1 moved off a documented conflict (Run last
plugin on Windows: the docs also give Ctrl+Alt+P to pixel preview; the plugin keeps
Ctrl+Alt+Shift+P), 2 missing (moving the rotation origin with Alt+R; placing an object with
Enter after choosing a tool from the toolbar), 2 kept by a platform (Ctrl+Space with some CJK
input methods; F6 in browsers). 48 undocumented bindings pinned.
- Tests: every row fires its command on both platforms and sits in the right shortcuts-panel tab;
every default binding has a row; no binding uses a chord the OS keeps (Cmd+H/M/Q/Tab/Space,
Ctrl+Space, screenshot keys on macOS; Alt+Space/Tab/F4, Ctrl+Esc, Ctrl+Alt+Delete, Linux
workspace and VT keys); macOS keycaps show Cmd/Option glyphs only and Windows/Linux keycaps
Ctrl/Alt words only; vector edit tool keys; counts pinned for the docs.
- Newly bound: A (Frame tool), Shift+' (pixel grid; Cmd+' kept, the articles disagree),
Alt+Cmd+\ / Ctrl+Alt+\ (others' cursors in a live session), Ctrl+F6 (toolbar focus on
Windows/Linux), Shift+Cmd+U (link text; Cmd+K kept), Cmd/Ctrl+] and [ (list levels while
editing), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection; F8 kept,
Cmd/Ctrl+arrows draw the box out), Ctrl+Shift+A / Ctrl+Alt+Shift+A (suggest auto layout),
Ctrl+Alt+Cmd+K / Ctrl+Alt+Shift+K (go to main component).
- New commands: `view.multiplayerCursors {on?}` (View menu item, menu breadth 207/207),
`view.pan {dx, dy, big?}` (arrows pan the view by 20 screen px, 200 with Shift, when nothing is
selected; the canvas and MCP `key` both use it), `text.listIndent` / `text.listOutdent`.
- Behaviour: Control held while dragging suspends snapping on macOS too (Cmd/Ctrl already did);
the presenter takes Space and N for the next frame, C for comments (Shift+C kept) and F for full
screen, after the prototype's own key triggers; the paint bucket's tooltip shows Shift+B (B still
works).
- Keyboard layouts: shortcuts follow the character typed, named as on a US keyboard. A symbol
another layout types with Shift (`/` is Shift+7 on German keyboards) now counts without the
Shift (canvas, text editing and F6 navigation share one lookup). Characters egui has no key for
already arrive as the key at that position.
- Keyboard shortcuts panel: new rows for panning with the arrows, suspending snapping, moving along
the toolbar, and the presentation keys, in the tabs the documentation groups them in (en/es
strings).
- Docs: ui-parity.md gets a Keyboard shortcuts section with the counts; target-app-parity.md,
ROADMAP.md, gaps.md (gap 4 landed list), mcp.md, parity-checklist.md (regenerated).
This commit is contained in:
parent
01bc5a9dac
commit
8d5f8a0978
13
ROADMAP.md
13
ROADMAP.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
|
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7)
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||||
|
|
||||||
FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow,
|
FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow,
|
||||||
native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through
|
native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through
|
||||||
|
|
@ -13,7 +13,7 @@ start with [`docs/gaps.md`](docs/gaps.md).
|
||||||
|
|
||||||
| | Value | Kind |
|
| | Value | Kind |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Menu/command breadth | **100%** (204 / 204) | measured, [`docs/parity-checklist.md`](docs/parity-checklist.md) (`cargo xtask parity`) |
|
| Menu/command breadth | **100%** (207 / 207) | measured, [`docs/parity-checklist.md`](docs/parity-checklist.md) (`cargo xtask parity`) |
|
||||||
| Feature breadth | **~84%** (core editor ~94%) | estimated, [`docs/target-app-parity.md`](docs/target-app-parity.md) |
|
| Feature breadth | **~84%** (core editor ~94%) | estimated, [`docs/target-app-parity.md`](docs/target-app-parity.md) |
|
||||||
| Ready for real work | **~53%** | estimated, same doc (written dimension weights and scores) |
|
| Ready for real work | **~53%** | estimated, same doc (written dimension weights and scores) |
|
||||||
| Essentials user | **~57%** | estimated: core features 77.8% × 0.92 launch/stability × 0.92 discoverability × 0.88 opening files people send ([method](docs/target-app-parity.md#essentials-user)) |
|
| Essentials user | **~57%** | estimated: core features 77.8% × 0.92 launch/stability × 0.92 discoverability × 0.88 opening files people send ([method](docs/target-app-parity.md#essentials-user)) |
|
||||||
|
|
@ -131,6 +131,14 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||||
|
|
||||||
## Progress log
|
## Progress log
|
||||||
|
|
||||||
|
- **2026-10-12** · Keyboard shortcut parity (gap 4): a table test with a row for each of the 129
|
||||||
|
shortcuts in the public documentation, macOS and Windows/Linux keys each, and the 48 bindings it
|
||||||
|
is silent on pinned; 126 bound (110 before), Run last plugin moved off a documented conflict on
|
||||||
|
Windows, 2 missing. New: others' cursors toggle, list levels on Cmd/Ctrl+] and [ while editing,
|
||||||
|
keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, Control
|
||||||
|
suspending snapping on macOS, Space/N/C/F while presenting, the Frame tool on A, pixel grid on
|
||||||
|
Shift+', link on Shift+Cmd+U, ungroup on Cmd/Ctrl+Backspace, suggest auto layout and go to main
|
||||||
|
component keys; shifted symbols on other keyboard layouts.
|
||||||
- **2026-10-12** · Plugin directory on `figcraft-server` (gap 6): publish a plugin folder with the server key
|
- **2026-10-12** · Plugin directory on `figcraft-server` (gap 6): publish a plugin folder with the server key
|
||||||
(`figcraft-cli plugin publish`), browse a server's plugins in Plugins ▸ Manage plugins ▸ Browse, install after a
|
(`figcraft-cli plugin publish`), browse a server's plugins in Plugins ▸ Manage plugins ▸ Browse, install after a
|
||||||
confirmation that shows the permissions it asks for, update when a newer version appears, remove; bundles are
|
confirmation that shows the permissions it asks for, update when a newer version appears, remove; bundles are
|
||||||
|
|
@ -237,6 +245,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding |
|
||||||
| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h |
|
| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h |
|
||||||
| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h |
|
| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h |
|
||||||
| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h |
|
| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h |
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ pub static MENU: &[MenuItem] = &[
|
||||||
m!("View", "Rulers", "view.rulers"),
|
m!("View", "Rulers", "view.rulers"),
|
||||||
m!("View", "Slices", "view.slices"),
|
m!("View", "Slices", "view.slices"),
|
||||||
m!("View", "Comments", "view.comments"),
|
m!("View", "Comments", "view.comments"),
|
||||||
|
m!("View", "Multiplayer cursors", "view.multiplayerCursors"),
|
||||||
m!("View", SEP, ""),
|
m!("View", SEP, ""),
|
||||||
m!("View/Outlines", "Show outlines", "view.outlines"),
|
m!("View/Outlines", "Show outlines", "view.outlines"),
|
||||||
m!("View/Outlines", "Show outlines for selection", "view.outlinesSelection"),
|
m!("View/Outlines", "Show outlines for selection", "view.outlinesSelection"),
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||||
spec!(
|
spec!(
|
||||||
"select.area",
|
"select.area",
|
||||||
"Select area",
|
"Select area",
|
||||||
"{x, y, width, height (page units; a marquee from x, y), add? (toggle like a Shift marquee), deep? (nested layers, like Cmd/Ctrl)} → {selection}; with no box: draw one from the keyboard in the app (arrows move it, Shift+arrows size it, Enter selects, Escape cancels)",
|
"{x, y, width, height (page units; a marquee from x, y), add? (toggle like a Shift marquee), deep? (nested layers, like Cmd/Ctrl)} → {selection}; with no box: draw one from the keyboard in the app (arrows move it, Cmd/Ctrl+arrows or Shift+arrows size it, Enter selects, Escape cancels)",
|
||||||
false,
|
false,
|
||||||
select_area
|
select_area
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,12 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||||
ok()
|
ok()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
spec!("text.listIndent", "Indent list", "{} while editing: the selected list items a level deeper", true, |s, _| {
|
||||||
|
Ok(json!({"changed": textedit::list_indent(s, false)?}))
|
||||||
|
}),
|
||||||
|
spec!("text.listOutdent", "Outdent list", "{} while editing: the selected list items a level out", true, |s, _| {
|
||||||
|
Ok(json!({"changed": textedit::list_indent(s, true)?}))
|
||||||
|
}),
|
||||||
spec!("text.edit.caret", "Set caret", "{index, extend?}", false, |s, p| {
|
spec!("text.edit.caret", "Set caret", "{index, extend?}", false, |s, p| {
|
||||||
let i = req_num("text.edit.caret", p, "index")?.max(0.0) as usize;
|
let i = req_num("text.edit.caret", p, "index")?.max(0.0) as usize;
|
||||||
textedit::set_caret(s, i, boolean(p, "extend").unwrap_or(false))?;
|
textedit::set_caret(s, i, boolean(p, "extend").unwrap_or(false))?;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,20 @@ fn zoom_to(s: &mut Session, z: f64) -> Result<Value> {
|
||||||
Ok(json!({"zoom": v.zoom}))
|
Ok(json!({"zoom": v.zoom}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How far an arrow key pans the view with nothing selected (screen pixels; Shift: ten times).
|
||||||
|
pub const PAN_STEP_PX: f64 = 20.0;
|
||||||
|
|
||||||
|
/// `view.pan`: the arrow keys with nothing selected move the view by keyboard steps.
|
||||||
|
fn pan(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
let step = if boolean(p, "big").unwrap_or(false) { PAN_STEP_PX * 10.0 } else { PAN_STEP_PX };
|
||||||
|
let dir = |k| num(p, k).filter(|v| v.is_finite()).unwrap_or(0.0).clamp(-100.0, 100.0) * step;
|
||||||
|
let (dx, dy) = (dir("dx"), dir("dy"));
|
||||||
|
let v = view(s)?;
|
||||||
|
// Scrolling moves the content; panning right shows what is to the right.
|
||||||
|
v.scroll(-dx, -dy, false);
|
||||||
|
Ok(json!({"cx": v.cx, "cy": v.cy}))
|
||||||
|
}
|
||||||
|
|
||||||
/// Figma's zoom steps.
|
/// Figma's zoom steps.
|
||||||
const STEPS: &[f64] = &[0.02, 0.03, 0.06, 0.13, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0];
|
const STEPS: &[f64] = &[0.02, 0.03, 0.06, 0.13, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0];
|
||||||
|
|
||||||
|
|
@ -192,6 +206,10 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||||
spec!("view.rulers", "Rulers", "{on?}", false, |s, p| flag(s, p, |v| &mut v.rulers)),
|
spec!("view.rulers", "Rulers", "{on?}", false, |s, p| flag(s, p, |v| &mut v.rulers)),
|
||||||
spec!("view.slices", "Slices", "{on?}", false, |s, p| flag(s, p, |v| &mut v.show_slices)),
|
spec!("view.slices", "Slices", "{on?}", false, |s, p| flag(s, p, |v| &mut v.show_slices)),
|
||||||
spec!("view.comments", "Comments", "{on?}", false, |s, p| flag(s, p, |v| &mut v.comments)),
|
spec!("view.comments", "Comments", "{on?}", false, |s, p| flag(s, p, |v| &mut v.comments)),
|
||||||
|
spec!("view.multiplayerCursors", "Multiplayer cursors", "{on?} others' cursors in a live session", false, |s, p| {
|
||||||
|
flag(s, p, |v| &mut v.multiplayer_cursors)
|
||||||
|
}),
|
||||||
|
spec!("view.pan", "Pan the view", "{dx, dy (arrow-key steps, e.g. 1 or -1), big? (ten times as far, like Shift)}", false, pan),
|
||||||
spec!("view.outlines", "Outlines", "{on?}", false, |s, p| flag(s, p, |v| &mut v.outlines)),
|
spec!("view.outlines", "Outlines", "{on?}", false, |s, p| flag(s, p, |v| &mut v.outlines)),
|
||||||
spec!("view.outlinesSelection", "Outlines for selection", "{on?} outline mode for the selected layers only", false, |s, p| {
|
spec!("view.outlinesSelection", "Outlines for selection", "{on?} outline mode for the selected layers only", false, |s, p| {
|
||||||
flag(s, p, |v| &mut v.outlines_selection)
|
flag(s, p, |v| &mut v.outlines_selection)
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ pub static KEYS: &[Binding] = &[
|
||||||
k(Tools, "tool.move", &["V"]),
|
k(Tools, "tool.move", &["V"]),
|
||||||
k(Tools, "tool.scale", &["K"]),
|
k(Tools, "tool.scale", &["K"]),
|
||||||
k(Tools, "tool.hand", &["H"]),
|
k(Tools, "tool.hand", &["H"]),
|
||||||
k(Tools, "tool.frame", &["F"]),
|
k(Tools, "tool.frame", &["F", "A"]),
|
||||||
k(Tools, "tool.section", &["Shift+S"]),
|
k(Tools, "tool.section", &["Shift+S"]),
|
||||||
k(Tools, "tool.slice", &["S"]),
|
k(Tools, "tool.slice", &["S"]),
|
||||||
k(Tools, "tool.rectangle", &["R"]),
|
k(Tools, "tool.rectangle", &["R"]),
|
||||||
|
|
@ -193,12 +193,14 @@ pub static KEYS: &[Binding] = &[
|
||||||
k(Tools, "tool.comment", &["C"]),
|
k(Tools, "tool.comment", &["C"]),
|
||||||
k(Tools, "tool.shapeBuilder", &["Shift+M"]),
|
k(Tools, "tool.shapeBuilder", &["Shift+M"]),
|
||||||
// View
|
// View
|
||||||
k(View, "view.pixelGrid", &["Cmd+'"]),
|
// The documentation gives both keys for the pixel grid in different places.
|
||||||
|
k(View, "view.pixelGrid", &["Shift+'", "Cmd+'"]),
|
||||||
k(View, "view.layoutGrids", &["Shift+G", "Ctrl+G"]).win(&["Shift+G", "Ctrl+Shift+4"]),
|
k(View, "view.layoutGrids", &["Shift+G", "Ctrl+G"]).win(&["Shift+G", "Ctrl+Shift+4"]),
|
||||||
k(View, "view.rulers", &["Shift+R"]),
|
k(View, "view.rulers", &["Shift+R"]),
|
||||||
k(View, "view.outlines", &["Shift+Cmd+O"]),
|
k(View, "view.outlines", &["Shift+Cmd+O"]),
|
||||||
k(View, "view.pixelPreview", &["Ctrl+P"]).win(&["Ctrl+Alt+P"]),
|
k(View, "view.pixelPreview", &["Ctrl+P"]).win(&["Ctrl+Alt+P"]),
|
||||||
k(View, "view.comments", &["Shift+C"]),
|
k(View, "view.comments", &["Shift+C"]),
|
||||||
|
k(View, "view.multiplayerCursors", &["Alt+Cmd+\\"]),
|
||||||
k(View, "view.snapPixel", &["Shift+Cmd+'"]),
|
k(View, "view.snapPixel", &["Shift+Cmd+'"]),
|
||||||
k(View, "view.layers", &["Alt+1"]).label("Layers panel"),
|
k(View, "view.layers", &["Alt+1"]).label("Layers panel"),
|
||||||
k(View, "view.assets", &["Alt+2"]).label("Assets panel"),
|
k(View, "view.assets", &["Alt+2"]).label("Assets panel"),
|
||||||
|
|
@ -208,7 +210,7 @@ pub static KEYS: &[Binding] = &[
|
||||||
k(View, "view.designPrototype", &["Shift+E"]),
|
k(View, "view.designPrototype", &["Shift+E"]),
|
||||||
k(View, "view.devMode", &["Shift+D"]).label("Dev Mode on/off"),
|
k(View, "view.devMode", &["Shift+D"]).label("Dev Mode on/off"),
|
||||||
k(View, "view.leftSidebar", &["Shift+Cmd+\\"]),
|
k(View, "view.leftSidebar", &["Shift+Cmd+\\"]),
|
||||||
k(View, "view.nextRegion", &["F6"]).label("Focus the next region (toolbar, layers, canvas, properties)").both(),
|
k(View, "view.nextRegion", &["F6"]).win(&["F6", "Ctrl+F6"]).label("Focus the next region (toolbar, layers, canvas, properties)").both(),
|
||||||
k(View, "view.previousRegion", &["Shift+F6"]).label("Focus the previous region").both(),
|
k(View, "view.previousRegion", &["Shift+F6"]).label("Focus the previous region").both(),
|
||||||
k(View, "view.contextMenu", &["Shift+F10"]).label("Context menu of the selection (the focused layer row or the canvas)"),
|
k(View, "view.contextMenu", &["Shift+F10"]).label("Context menu of the selection (the focused layer row or the canvas)"),
|
||||||
k(View, "layers.collapseAll", &["Alt+L"]),
|
k(View, "layers.collapseAll", &["Alt+L"]),
|
||||||
|
|
@ -229,9 +231,11 @@ pub static KEYS: &[Binding] = &[
|
||||||
k(Text, "text.italic", &["Cmd+I"]).both(),
|
k(Text, "text.italic", &["Cmd+I"]).both(),
|
||||||
k(Text, "text.underline", &["Cmd+U"]).both(),
|
k(Text, "text.underline", &["Cmd+U"]).both(),
|
||||||
k(Text, "text.strikethrough", &["Shift+Cmd+X"]).both(),
|
k(Text, "text.strikethrough", &["Shift+Cmd+X"]).both(),
|
||||||
k(Text, "text.link", &["Cmd+K"]).text(),
|
k(Text, "text.link", &["Shift+Cmd+U", "Cmd+K"]).text(),
|
||||||
k(Text, "text.bulletList", &["Shift+Cmd+8"]).both(),
|
k(Text, "text.bulletList", &["Shift+Cmd+8"]).both(),
|
||||||
k(Text, "text.numberList", &["Shift+Cmd+7"]).both(),
|
k(Text, "text.numberList", &["Shift+Cmd+7"]).both(),
|
||||||
|
k(Text, "text.listIndent", &["Cmd+]"]).text(),
|
||||||
|
k(Text, "text.listOutdent", &["Cmd+["]).text(),
|
||||||
k(Text, "text.alignLeft", &["Alt+Cmd+L"]).both(),
|
k(Text, "text.alignLeft", &["Alt+Cmd+L"]).both(),
|
||||||
k(Text, "text.alignCenter", &["Alt+Cmd+T"]).both(),
|
k(Text, "text.alignCenter", &["Alt+Cmd+T"]).both(),
|
||||||
k(Text, "text.alignRight", &["Alt+Cmd+R"]).both(),
|
k(Text, "text.alignRight", &["Alt+Cmd+R"]).both(),
|
||||||
|
|
@ -267,12 +271,15 @@ pub static KEYS: &[Binding] = &[
|
||||||
k(Selection, "select.next", &["Tab"]),
|
k(Selection, "select.next", &["Tab"]),
|
||||||
k(Selection, "select.previous", &["Shift+Tab"]),
|
k(Selection, "select.previous", &["Shift+Tab"]),
|
||||||
k(Selection, "object.group", &["Cmd+G"]),
|
k(Selection, "object.group", &["Cmd+G"]),
|
||||||
k(Selection, "object.ungroup", &["Shift+Cmd+G"]),
|
k(Selection, "object.ungroup", &["Shift+Cmd+G", "Cmd+Delete"]),
|
||||||
k(Selection, "object.frameSelection", &["Alt+Cmd+G"]),
|
k(Selection, "object.frameSelection", &["Alt+Cmd+G"]),
|
||||||
k(Selection, "object.toggleVisible", &["Shift+Cmd+H"]),
|
k(Selection, "object.toggleVisible", &["Shift+Cmd+H"]),
|
||||||
k(Selection, "object.toggleLock", &["Shift+Cmd+L"]),
|
k(Selection, "object.toggleLock", &["Shift+Cmd+L"]),
|
||||||
k(Selection, "object.rename", &["Cmd+R", "F2"]),
|
k(Selection, "object.rename", &["Cmd+R", "F2"]),
|
||||||
k(Selection, "select.area", &["F8"]).label("Select with a box from the keyboard (arrows move it, Shift+arrows size it, Enter selects)"),
|
// Alt+Space opens the window menu on Windows; the documented Windows key is Ctrl+Space.
|
||||||
|
k(Selection, "select.area", &["Alt+Space", "F8"])
|
||||||
|
.win(&["Ctrl+Space", "F8"])
|
||||||
|
.label("Select with a box from the keyboard (arrows move it, Shift+arrows size it, Enter selects)"),
|
||||||
// Edit
|
// Edit
|
||||||
k(Edit, "edit.undo", &["Cmd+Z"]),
|
k(Edit, "edit.undo", &["Cmd+Z"]),
|
||||||
k(Edit, "edit.redo", &["Shift+Cmd+Z"]),
|
k(Edit, "edit.redo", &["Shift+Cmd+Z"]),
|
||||||
|
|
@ -309,9 +316,11 @@ pub static KEYS: &[Binding] = &[
|
||||||
k(Arrange, "arrange.tidy", &["Ctrl+Alt+T"]).win(&["Ctrl+Alt+Shift+T"]),
|
k(Arrange, "arrange.tidy", &["Ctrl+Alt+T"]).win(&["Ctrl+Alt+Shift+T"]),
|
||||||
k(Arrange, "layout.add", &["Shift+A"]),
|
k(Arrange, "layout.add", &["Shift+A"]),
|
||||||
k(Arrange, "layout.remove", &["Alt+Shift+A"]),
|
k(Arrange, "layout.remove", &["Alt+Shift+A"]),
|
||||||
|
k(Arrange, "layout.suggest", &["Ctrl+Shift+A"]).win(&["Ctrl+Alt+Shift+A"]),
|
||||||
// Components
|
// Components
|
||||||
k(Components, "component.create", &["Alt+Cmd+K"]),
|
k(Components, "component.create", &["Alt+Cmd+K"]),
|
||||||
k(Components, "component.detach", &["Alt+Cmd+B"]),
|
k(Components, "component.detach", &["Alt+Cmd+B"]),
|
||||||
|
k(Components, "component.goToMain", &["Ctrl+Alt+Cmd+K"]).win(&["Ctrl+Alt+Shift+K"]),
|
||||||
k(Components, "view.assets", &["Shift+I"]).label("Insert a component (Assets)"),
|
k(Components, "view.assets", &["Shift+I"]).label("Insert a component (Assets)"),
|
||||||
// Prototype
|
// Prototype
|
||||||
k(Prototype, "proto.present", &["Alt+Cmd+Enter"]).label("Present"),
|
k(Prototype, "proto.present", &["Alt+Cmd+Enter"]).label("Present"),
|
||||||
|
|
@ -322,6 +331,7 @@ pub static KEYS: &[Binding] = &[
|
||||||
/// dispatched from here): (group, label, macOS keys, Windows/Linux keys).
|
/// dispatched from here): (group, label, macOS keys, Windows/Linux keys).
|
||||||
pub static GESTURES: &[(Group, &str, &str, &str)] = &[
|
pub static GESTURES: &[(Group, &str, &str, &str)] = &[
|
||||||
(Tools, "Pan the canvas", "Space drag", "Space+drag"),
|
(Tools, "Pan the canvas", "Space drag", "Space+drag"),
|
||||||
|
(Tools, "Pan with nothing selected (Shift: farther)", "← ↑ → ↓", "← ↑ → ↓"),
|
||||||
(Zoom, "Zoom tool: click to zoom in, drag an area", "Hold Z", "Hold Z"),
|
(Zoom, "Zoom tool: click to zoom in, drag an area", "Hold Z", "Hold Z"),
|
||||||
(Zoom, "Zoom tool: zoom out", "Hold ⌥Z", "Hold Alt+Z"),
|
(Zoom, "Zoom tool: zoom out", "Hold ⌥Z", "Hold Alt+Z"),
|
||||||
(Zoom, "Zoom around the pointer", "⌘ scroll", "Ctrl+scroll"),
|
(Zoom, "Zoom around the pointer", "⌘ scroll", "Ctrl+scroll"),
|
||||||
|
|
@ -332,7 +342,15 @@ pub static GESTURES: &[(Group, &str, &str, &str)] = &[
|
||||||
(Transform, "Resize by the nudge amount", "⌘ arrows", "Ctrl+arrows"),
|
(Transform, "Resize by the nudge amount", "⌘ arrows", "Ctrl+arrows"),
|
||||||
(Transform, "Opacity 10%…90%, 0: 100%", "1…9, 0", "1…9, 0"),
|
(Transform, "Opacity 10%…90%, 0: 100%", "1…9, 0", "1…9, 0"),
|
||||||
(Transform, "Exact opacity (4 then 5: 45%)", "Two digits", "Two digits"),
|
(Transform, "Exact opacity (4 then 5: 45%)", "Two digits", "Two digits"),
|
||||||
|
(Transform, "Suspend snapping while dragging", "Hold ⌃ or ⌘", "Hold Ctrl"),
|
||||||
|
(Prototype, "Presenting: next frame", "→, Space or N", "→, Space or N"),
|
||||||
|
(Prototype, "Presenting: previous frame", "←", "←"),
|
||||||
|
(Prototype, "Presenting: restart", "R", "R"),
|
||||||
|
(Prototype, "Presenting: cycle the size options", "Z", "Z"),
|
||||||
|
(Prototype, "Presenting: comments", "C or ⇧C", "C or Shift+C"),
|
||||||
|
(Prototype, "Presenting: full screen", "F", "F"),
|
||||||
(View, "Next or previous control in the focused region", "⇥ / ⇧⇥", "Tab / Shift+Tab"),
|
(View, "Next or previous control in the focused region", "⇥ / ⇧⇥", "Tab / Shift+Tab"),
|
||||||
|
(View, "Move along the toolbar or the layer rows", "← → ↑ ↓", "← → ↑ ↓"),
|
||||||
(View, "Back to the canvas from a focused region", "Esc", "Esc"),
|
(View, "Back to the canvas from a focused region", "Esc", "Esc"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ fn vector_edit_paint_bucket_is_b() {
|
||||||
ex(&mut s, "vector.edit.begin", json!({"id": r}));
|
ex(&mut s, "vector.edit.begin", json!({"id": r}));
|
||||||
assert!(crate::vectoredit::key(&mut s, "B", false, false, false).unwrap());
|
assert!(crate::vectoredit::key(&mut s, "B", false, false, false).unwrap());
|
||||||
assert_eq!(s.ds().unwrap().vector_edit.as_ref().unwrap().tool, VTool::Paint);
|
assert_eq!(s.ds().unwrap().vector_edit.as_ref().unwrap().tool, VTool::Paint);
|
||||||
assert_eq!(VTool::Paint.shortcut(), "B");
|
assert_eq!(VTool::Paint.shortcut(), "Shift+B");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,8 @@ pub struct ViewState {
|
||||||
pub pixel_scale: u8,
|
pub pixel_scale: u8,
|
||||||
pub show_slices: bool,
|
pub show_slices: bool,
|
||||||
pub comments: bool,
|
pub comments: bool,
|
||||||
|
/// Others' cursors in a live session (View ▸ Multiplayer cursors).
|
||||||
|
pub multiplayer_cursors: bool,
|
||||||
pub frame_outlines: bool,
|
pub frame_outlines: bool,
|
||||||
/// Soft-proof sRGB: the canvas marks the colours of a Display P3 file that sRGB can't show.
|
/// Soft-proof sRGB: the canvas marks the colours of a Display P3 file that sRGB can't show.
|
||||||
pub proof_srgb: bool,
|
pub proof_srgb: bool,
|
||||||
|
|
@ -170,6 +172,7 @@ impl Default for ViewState {
|
||||||
pixel_scale: 1,
|
pixel_scale: 1,
|
||||||
show_slices: false,
|
show_slices: false,
|
||||||
comments: true,
|
comments: true,
|
||||||
|
multiplayer_cursors: true,
|
||||||
frame_outlines: false,
|
frame_outlines: false,
|
||||||
proof_srgb: false,
|
proof_srgb: false,
|
||||||
snap_geometry: true,
|
snap_geometry: true,
|
||||||
|
|
@ -1404,6 +1407,8 @@ mod shapebuilder_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod shapes_behaviour_tests;
|
mod shapes_behaviour_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod shortcut_parity_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod slot_tests;
|
mod slot_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod stroke_style_tests;
|
mod stroke_style_tests;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,449 @@
|
||||||
|
//! Keyboard shortcut parity with the public shortcut documentation, per platform. One row per
|
||||||
|
//! documented shortcut: the action in our own words, the macOS and the Windows/Linux keys as the
|
||||||
|
//! documentation gives them, what does it here (a keymap command, a held key or gesture the panel
|
||||||
|
//! lists, a vector edit tool key, or nothing yet) and a note. Rows the documentation is silent on
|
||||||
|
//! pin today's keys, so every default binding is in this table: changing one means changing a row.
|
||||||
|
//!
|
||||||
|
//! The documentation is spread over the help centre's feature articles (keyboard layouts, text
|
||||||
|
//! properties, alignment, frames, components, prototypes, plugins, accessibility…); where two
|
||||||
|
//! articles disagree the note says so and both keys are bound when they don't collide.
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::Session;
|
||||||
|
use crate::keymap::Group::{self, *};
|
||||||
|
use crate::keymap::{Context, GESTURES, KEYS, Platform, display, lookup, parse};
|
||||||
|
use crate::params::id_of;
|
||||||
|
use crate::vectoredit::VTool;
|
||||||
|
|
||||||
|
const PLATFORMS: [Platform; 2] = [Platform::Mac, Platform::Other];
|
||||||
|
|
||||||
|
/// What answers a documented shortcut here.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum Ours {
|
||||||
|
/// The keymap binds the documented keys to this command.
|
||||||
|
Bound(&'static str),
|
||||||
|
/// Bound to this command on other keys where the documented ones collide with another
|
||||||
|
/// documented shortcut: (command, macOS keys, Windows/Linux keys; empty: the macOS keys).
|
||||||
|
Moved(&'static str, &'static [&'static str], &'static [&'static str]),
|
||||||
|
/// A held key, pointer gesture or presentation key; the shortcuts panel lists it under this label.
|
||||||
|
Gesture(&'static str),
|
||||||
|
/// A tool key in vector edit mode.
|
||||||
|
VectorTool(VTool),
|
||||||
|
/// No such behaviour yet.
|
||||||
|
Missing,
|
||||||
|
}
|
||||||
|
use Ours::*;
|
||||||
|
|
||||||
|
struct Row {
|
||||||
|
/// The shortcuts panel tab the row belongs to.
|
||||||
|
group: Group,
|
||||||
|
action: &'static str,
|
||||||
|
mac: &'static [&'static str],
|
||||||
|
/// Empty: the macOS keys with Cmd read as Ctrl.
|
||||||
|
win: &'static [&'static str],
|
||||||
|
ours: Ours,
|
||||||
|
documented: bool,
|
||||||
|
/// Who takes the keys before the app on some platform (empty: nobody).
|
||||||
|
reserved: &'static str,
|
||||||
|
note: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Row {
|
||||||
|
const fn reserved(mut self, by: &'static str) -> Self {
|
||||||
|
self.reserved = by;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn keys(&self, p: Platform) -> &'static [&'static str] {
|
||||||
|
if p == Platform::Other && !self.win.is_empty() { self.win } else { self.mac }
|
||||||
|
}
|
||||||
|
/// The keys that fire the row's command here, and the command.
|
||||||
|
fn ours(&self, p: Platform) -> Option<(&'static str, &'static [&'static str])> {
|
||||||
|
match self.ours {
|
||||||
|
Bound(id) => Some((id, self.keys(p))),
|
||||||
|
Moved(id, mac, win) => Some((id, if p == Platform::Other && !win.is_empty() { win } else { mac })),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn doc(group: Group, action: &'static str, mac: &'static [&'static str], win: &'static [&'static str], ours: Ours, note: &'static str) -> Row {
|
||||||
|
Row { group, action, mac, win, ours, documented: true, reserved: "", note }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A binding the documentation is silent on: today's keys, pinned.
|
||||||
|
const fn pin(group: Group, id: &'static str, mac: &'static [&'static str], win: &'static [&'static str]) -> Row {
|
||||||
|
Row { group, action: id, mac, win, ours: Bound(id), documented: false, reserved: "", note: "documentation silent: pinned" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const BROWSER_TAB: &str = "browsers keep it for their tabs and windows (web build)";
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
static ROWS: &[Row] = &[
|
||||||
|
// Essential
|
||||||
|
doc(Essential, "Open the actions menu", &["Cmd+K", "Cmd+/"], &[], Bound("app.actions"), "Cmd+/ is in the keyboard layout tables"),
|
||||||
|
doc(Essential, "Open the keyboard shortcuts panel", &["Ctrl+Shift+?"], &[], Bound("help.shortcuts"), "Control on macOS too (Shift+Cmd+? is the system's Help search)"),
|
||||||
|
doc(Essential, "Show or hide the interface", &["Cmd+\\"], &[], Bound("view.toggleUi"), ""),
|
||||||
|
doc(Essential, "Find layers and text", &["Cmd+F"], &[], Bound("edit.findReplace"), ""),
|
||||||
|
doc(Essential, "Chat at your cursor in a live session", &["/"], &[], Bound("collab.cursorChat"), ""),
|
||||||
|
doc(Essential, "Sample a colour", &["I", "Ctrl+C"], &["I"], Bound("edit.pickColor"), "Control+C on macOS only (Ctrl+C copies elsewhere)"),
|
||||||
|
doc(Essential, "Save to version history", &["Alt+Cmd+S"], &["Ctrl+Alt+S"], Bound("file.saveVersion"), ""),
|
||||||
|
doc(Essential, "Run the last plugin", &["Alt+Cmd+P"], &["Ctrl+Alt+P"], Moved("plugins.runLast", &["Alt+Cmd+P"], &["Ctrl+Alt+Shift+P"]),
|
||||||
|
"conflict: the documentation also gives Ctrl+Alt+P to pixel preview on Windows; pixel preview keeps it, the plugin takes Ctrl+Alt+Shift+P"),
|
||||||
|
pin(Essential, "app.preferences", &["Cmd+,"], &[]),
|
||||||
|
pin(Essential, "file.new", &["Cmd+N"], &[]).reserved(BROWSER_TAB),
|
||||||
|
pin(Essential, "file.open", &["Cmd+O"], &[]),
|
||||||
|
pin(Essential, "file.save", &["Cmd+S"], &[]),
|
||||||
|
pin(Essential, "file.saveAs", &["Shift+Cmd+S"], &[]),
|
||||||
|
pin(Essential, "file.close", &["Cmd+W"], &[]).reserved(BROWSER_TAB),
|
||||||
|
// Tools
|
||||||
|
doc(Tools, "Move tool", &["V"], &[], Bound("tool.move"), ""),
|
||||||
|
doc(Tools, "Scale tool", &["K"], &[], Bound("tool.scale"), ""),
|
||||||
|
doc(Tools, "Frame tool", &["F", "A"], &[], Bound("tool.frame"), "A was unbound before"),
|
||||||
|
doc(Tools, "Rectangle tool", &["R"], &[], Bound("tool.rectangle"), ""),
|
||||||
|
doc(Tools, "Line tool", &["L"], &[], Bound("tool.line"), ""),
|
||||||
|
doc(Tools, "Arrow tool", &["Shift+L"], &[], Bound("tool.arrow"), ""),
|
||||||
|
doc(Tools, "Ellipse tool", &["O"], &[], Bound("tool.ellipse"), ""),
|
||||||
|
doc(Tools, "Pen tool", &["P"], &[], Bound("tool.pen"), ""),
|
||||||
|
doc(Tools, "Pencil tool", &["Shift+P"], &[], Bound("tool.pencil"), ""),
|
||||||
|
doc(Tools, "Text tool", &["T"], &[], Bound("tool.text"), ""),
|
||||||
|
doc(Tools, "Comment mode", &["C"], &[], Bound("tool.comment"), ""),
|
||||||
|
doc(Tools, "Hand tool while held", &["Space"], &[], Gesture("Pan the canvas"), "held, not pressed"),
|
||||||
|
doc(Tools, "Pan with the arrows when nothing is selected (Shift: farther)", &["ArrowRight"], &[], Gesture("Pan with nothing selected (Shift: farther)"),
|
||||||
|
"view.pan; nudges move the selection when there is one"),
|
||||||
|
pin(Tools, "tool.hand", &["H"], &[]),
|
||||||
|
pin(Tools, "tool.section", &["Shift+S"], &[]),
|
||||||
|
pin(Tools, "tool.slice", &["S"], &[]),
|
||||||
|
pin(Tools, "file.placeImage", &["Shift+Cmd+K"], &[]),
|
||||||
|
pin(Tools, "tool.shapeBuilder", &["Shift+M"], &[]),
|
||||||
|
// Vector edit mode
|
||||||
|
doc(Selection, "Edit the selected shape's points", &["Enter"], &[], Bound("select.children"), "Enter on a shape edits it; on a frame or group it selects the children"),
|
||||||
|
doc(Shape, "Vector edit: move", &["V"], &[], VectorTool(VTool::Move), ""),
|
||||||
|
doc(Shape, "Vector edit: pen", &["P"], &[], VectorTool(VTool::Pen), ""),
|
||||||
|
doc(Shape, "Vector edit: cut", &["X"], &[], VectorTool(VTool::Cut), ""),
|
||||||
|
doc(Shape, "Vector edit: eraser", &["Shift+E"], &[], VectorTool(VTool::Eraser), ""),
|
||||||
|
doc(Shape, "Vector edit: lasso", &["Q"], &[], VectorTool(VTool::Lasso), ""),
|
||||||
|
doc(Shape, "Vector edit: paint bucket", &["Shift+B"], &[], VectorTool(VTool::Paint), "B alone works too (pinned)"),
|
||||||
|
// View
|
||||||
|
doc(View, "Pixel grid", &["Shift+'", "Cmd+'"], &[], Bound("view.pixelGrid"),
|
||||||
|
"articles disagree (Shift+' in most, Cmd+' in the zoom article); both bound, Shift+' first"),
|
||||||
|
doc(View, "Snap to the pixel grid", &["Shift+Cmd+'"], &[], Bound("view.snapPixel"), ""),
|
||||||
|
doc(View, "Layout guides", &["Ctrl+G", "Shift+G"], &["Ctrl+Shift+4", "Shift+G"], Bound("view.layoutGrids"), ""),
|
||||||
|
doc(View, "Outline mode", &["Shift+Cmd+O"], &[], Bound("view.outlines"), ""),
|
||||||
|
doc(View, "Pixel preview", &["Ctrl+P"], &["Ctrl+Alt+P"], Bound("view.pixelPreview"), ""),
|
||||||
|
doc(View, "Others' cursors in a live session", &["Alt+Cmd+\\"], &["Ctrl+Alt+\\"], Bound("view.multiplayerCursors"), "new command"),
|
||||||
|
doc(View, "Hide the side panels (minimize the interface)", &["Shift+Cmd+\\"], &[], Bound("view.leftSidebar"), ""),
|
||||||
|
doc(View, "Layers panel", &["Alt+1"], &[], Bound("view.layers"), "one article writes Ctrl+1 for Windows, another Alt+1; Alt+1 bound"),
|
||||||
|
doc(View, "Assets panel", &["Alt+2"], &[], Bound("view.assets"), ""),
|
||||||
|
doc(View, "Libraries", &["Alt+3"], &[], Bound("libraries.open"), ""),
|
||||||
|
doc(View, "Dev Mode on or off", &["Shift+D"], &[], Bound("view.devMode"), ""),
|
||||||
|
doc(View, "Move the keyboard to the toolbar (then the next region)", &["F6"], &["Ctrl+F6"], Bound("view.nextRegion"),
|
||||||
|
"F6 works on Windows and Linux too; F6 and F8 need Fn on many Mac keyboards").reserved("browsers move to the address bar on F6 (web build)"),
|
||||||
|
doc(View, "Move along the toolbar and the layer rows", &["ArrowRight"], &[], Gesture("Move along the toolbar or the layer rows"), ""),
|
||||||
|
pin(View, "view.rulers", &["Shift+R"], &[]),
|
||||||
|
pin(View, "view.comments", &["Shift+C"], &[]),
|
||||||
|
pin(View, "view.design", &["Alt+8"], &[]),
|
||||||
|
pin(View, "view.prototype", &["Alt+9"], &[]),
|
||||||
|
pin(View, "view.designPrototype", &["Shift+E"], &[]),
|
||||||
|
pin(View, "view.nextRegion", &["F6"], &["F6"]),
|
||||||
|
pin(View, "view.previousRegion", &["Shift+F6"], &[]),
|
||||||
|
pin(View, "view.contextMenu", &["Shift+F10"], &[]),
|
||||||
|
pin(View, "layers.collapseAll", &["Alt+L"], &[]),
|
||||||
|
pin(View, "doc.nextTab", &["Ctrl+Tab"], &[]).reserved(BROWSER_TAB),
|
||||||
|
pin(View, "doc.previousTab", &["Ctrl+Shift+Tab"], &[]).reserved(BROWSER_TAB),
|
||||||
|
pin(View, "view.prevPage", &["PageUp"], &[]),
|
||||||
|
pin(View, "view.nextPage", &["PageDown"], &[]),
|
||||||
|
// Zoom
|
||||||
|
doc(Zoom, "Zoom in", &["Cmd+=", "Shift+="], &[], Bound("view.zoomIn"), "`+` with or without Cmd/Ctrl"),
|
||||||
|
doc(Zoom, "Zoom out", &["Cmd+-", "-"], &[], Bound("view.zoomOut"), ""),
|
||||||
|
doc(Zoom, "Zoom to fit", &["Shift+1"], &[], Bound("view.zoomFit"), ""),
|
||||||
|
doc(Zoom, "Zoom to the selection", &["Shift+2"], &[], Bound("view.zoomSelection"), ""),
|
||||||
|
pin(Zoom, "view.zoomIn", &["Shift+Cmd+="], &[]),
|
||||||
|
pin(Zoom, "view.zoomOut", &["Shift+-"], &[]),
|
||||||
|
pin(Zoom, "view.zoom100", &["Shift+0", "Cmd+0"], &[]),
|
||||||
|
pin(Zoom, "view.prevFrame", &["Shift+N", "Home"], &[]),
|
||||||
|
pin(Zoom, "view.nextFrame", &["N", "End"], &[]),
|
||||||
|
// Text
|
||||||
|
doc(Text, "Bold", &["Cmd+B"], &[], Bound("text.bold"), ""),
|
||||||
|
doc(Text, "Italic", &["Cmd+I"], &[], Bound("text.italic"), ""),
|
||||||
|
doc(Text, "Underline", &["Cmd+U"], &[], Bound("text.underline"), ""),
|
||||||
|
doc(Text, "Strikethrough", &["Shift+Cmd+X"], &[], Bound("text.strikethrough"), ""),
|
||||||
|
doc(Text, "Link the selected text", &["Shift+Cmd+U"], &[], Bound("text.link"), "while editing; Cmd+K (the comment composer's link key) kept too"),
|
||||||
|
doc(Text, "Numbered list", &["Shift+Cmd+7"], &[], Bound("text.numberList"), ""),
|
||||||
|
doc(Text, "Bulleted list", &["Shift+Cmd+8"], &[], Bound("text.bulletList"), ""),
|
||||||
|
doc(Text, "List items a level deeper", &["Cmd+]"], &[], Bound("text.listIndent"), "while editing (Cmd+] brings forward otherwise); Tab too"),
|
||||||
|
doc(Text, "List items a level out", &["Cmd+["], &[], Bound("text.listOutdent"), "while editing; Shift+Tab too"),
|
||||||
|
doc(Text, "Larger text", &["Shift+Cmd+."], &[], Bound("text.sizeUp"), "written Shift+Cmd+>"),
|
||||||
|
doc(Text, "Smaller text", &["Shift+Cmd+,"], &[], Bound("text.sizeDown"), "written Shift+Cmd+<"),
|
||||||
|
doc(Text, "Heavier weight", &["Alt+Cmd+."], &[], Bound("text.weightUp"), ""),
|
||||||
|
doc(Text, "Lighter weight", &["Alt+Cmd+,"], &[], Bound("text.weightDown"), ""),
|
||||||
|
doc(Text, "More letter spacing", &["Alt+."], &[], Bound("text.trackingUp"), "the text article writes Alt; one layout table writes Cmd, which would be the font size keys"),
|
||||||
|
doc(Text, "Less letter spacing", &["Alt+,"], &[], Bound("text.trackingDown"), ""),
|
||||||
|
doc(Text, "More line height", &["Alt+Shift+."], &[], Bound("text.lineHeightUp"), ""),
|
||||||
|
doc(Text, "Less line height", &["Alt+Shift+,"], &[], Bound("text.lineHeightDown"), ""),
|
||||||
|
pin(Text, "text.link", &["Cmd+K"], &[]),
|
||||||
|
pin(Text, "text.alignLeft", &["Alt+Cmd+L"], &[]).reserved("some Linux desktops lock the screen on Ctrl+Alt+L"),
|
||||||
|
pin(Text, "text.alignCenter", &["Alt+Cmd+T"], &[]).reserved("Ubuntu opens a terminal on Ctrl+Alt+T"),
|
||||||
|
pin(Text, "text.alignRight", &["Alt+Cmd+R"], &[]),
|
||||||
|
pin(Text, "text.alignJustify", &["Alt+Cmd+J"], &[]),
|
||||||
|
// Shape
|
||||||
|
doc(Shape, "Union", &["Alt+Shift+U"], &[], Bound("bool.union"), ""),
|
||||||
|
doc(Shape, "Subtract", &["Alt+Shift+S"], &[], Bound("bool.subtract"), ""),
|
||||||
|
doc(Shape, "Intersect", &["Alt+Shift+I"], &[], Bound("bool.intersect"), ""),
|
||||||
|
doc(Shape, "Exclude", &["Alt+Shift+E"], &[], Bound("bool.exclude"), ""),
|
||||||
|
doc(Shape, "Flatten", &["Alt+Shift+F"], &[], Bound("vector.flatten"), ""),
|
||||||
|
doc(Shape, "Remove fill", &["Alt+/"], &[], Bound("object.removeFill"), "one layout table writes Alt+, for Windows; Alt+, is letter spacing"),
|
||||||
|
doc(Shape, "Remove stroke", &["Shift+/"], &[], Bound("object.removeStroke"), ""),
|
||||||
|
pin(Shape, "vector.flatten", &["Cmd+E"], &[]),
|
||||||
|
pin(Shape, "vector.outlineStroke", &["Alt+Cmd+O"], &[]),
|
||||||
|
pin(Shape, "object.swapFillStroke", &["Shift+X"], &[]),
|
||||||
|
pin(Shape, "vector.join", &["Cmd+J"], &[]),
|
||||||
|
pin(Shape, "vector.smoothJoin", &["Shift+Cmd+J"], &[]),
|
||||||
|
pin(Shape, "vector.deleteHeal", &["Shift+Delete"], &[]),
|
||||||
|
// Selection
|
||||||
|
doc(Selection, "Select all", &["Cmd+A"], &[], Bound("select.all"), ""),
|
||||||
|
doc(Selection, "Invert the selection", &["Shift+Cmd+A"], &[], Bound("select.inverse"), ""),
|
||||||
|
doc(Selection, "Select matching layers", &["Alt+Cmd+A"], &["Ctrl+Alt+A"], Bound("select.matching"), ""),
|
||||||
|
doc(Selection, "Deselect", &["Escape"], &[], Bound("select.none"), "steps out of editing first"),
|
||||||
|
doc(Selection, "Select the parent", &["Shift+Enter", "\\"], &[], Bound("select.parent"), ""),
|
||||||
|
doc(Selection, "Next sibling", &["Tab"], &[], Bound("select.next"), ""),
|
||||||
|
doc(Selection, "Previous sibling", &["Shift+Tab"], &[], Bound("select.previous"), ""),
|
||||||
|
doc(Selection, "Group", &["Cmd+G"], &[], Bound("object.group"), ""),
|
||||||
|
doc(Selection, "Ungroup", &["Shift+Cmd+G", "Cmd+Delete"], &[], Bound("object.ungroup"), "Cmd/Ctrl+Backspace was unbound before"),
|
||||||
|
doc(Selection, "Frame the selection", &["Alt+Cmd+G"], &["Ctrl+Alt+G"], Bound("object.frameSelection"), ""),
|
||||||
|
doc(Selection, "Hide or show", &["Shift+Cmd+H"], &[], Bound("object.toggleVisible"), ""),
|
||||||
|
doc(Selection, "Lock or unlock", &["Shift+Cmd+L"], &[], Bound("object.toggleLock"), ""),
|
||||||
|
doc(Selection, "Rename (several layers: batch rename)", &["Cmd+R"], &[], Bound("object.rename"), ""),
|
||||||
|
doc(Selection, "Keyboard box selection", &["Alt+Space"], &["Ctrl+Space"], Bound("select.area"),
|
||||||
|
"then the arrows move it, Cmd/Ctrl+arrows draw it out (Shift: farther), Enter selects, Escape closes; F8 kept")
|
||||||
|
.reserved("Ctrl+Space switches the input method with some Chinese, Japanese and Korean input methods on Windows and Linux"),
|
||||||
|
doc(Selection, "Select nested layers", &["Cmd"], &[], Gesture("Deep select"), "held while clicking or dragging"),
|
||||||
|
pin(Selection, "object.rename", &["F2"], &[]),
|
||||||
|
pin(Selection, "select.area", &["F8"], &[]),
|
||||||
|
// Edit
|
||||||
|
doc(Edit, "Undo", &["Cmd+Z"], &[], Bound("edit.undo"), ""),
|
||||||
|
doc(Edit, "Copy", &["Cmd+C"], &[], Bound("edit.copy"), ""),
|
||||||
|
doc(Edit, "Paste", &["Cmd+V"], &[], Bound("edit.paste"), ""),
|
||||||
|
doc(Edit, "Duplicate", &["Cmd+D"], &[], Bound("edit.duplicate"), ""),
|
||||||
|
doc(Edit, "Delete", &["Delete"], &[], Bound("edit.delete"), "Backspace or Delete"),
|
||||||
|
doc(Edit, "Paste over the selection", &["Shift+Cmd+V"], &[], Bound("edit.pasteOver"), ""),
|
||||||
|
doc(Edit, "Paste to replace", &["Shift+Cmd+R"], &[], Bound("edit.pasteReplace"), ""),
|
||||||
|
doc(Edit, "Copy as PNG", &["Shift+Cmd+C"], &[], Bound("edit.copyAsPng"), ""),
|
||||||
|
doc(Edit, "Copy all properties", &["Alt+Cmd+C"], &["Ctrl+Alt+C"], Bound("edit.copyProperties"), ""),
|
||||||
|
doc(Edit, "Paste all properties", &["Alt+Cmd+V"], &["Ctrl+Alt+V"], Bound("edit.pasteProperties"), ""),
|
||||||
|
doc(Edit, "Export", &["Shift+Cmd+E"], &[], Bound("file.export"), ""),
|
||||||
|
doc(Edit, "Duplicate by dragging", &["Alt"], &[], Gesture("Duplicate by dragging"), "held while dragging"),
|
||||||
|
pin(Edit, "edit.redo", &["Shift+Cmd+Z"], &[]),
|
||||||
|
pin(Edit, "edit.cut", &["Cmd+X"], &[]),
|
||||||
|
pin(Edit, "file.import", &["Shift+Cmd+I"], &[]),
|
||||||
|
// Transform
|
||||||
|
doc(Transform, "Flip horizontally", &["Shift+H"], &[], Bound("object.flipH"), ""),
|
||||||
|
doc(Transform, "Flip vertically", &["Shift+V"], &[], Bound("object.flipV"), ""),
|
||||||
|
doc(Transform, "Use as mask (again: release)", &["Ctrl+Cmd+M"], &["Ctrl+Alt+M"], Bound("object.mask"), ""),
|
||||||
|
doc(Transform, "Resize a frame to fit its contents", &["Alt+Shift+Cmd+R"], &["Ctrl+Alt+Shift+R"], Bound("object.resizeToFit"), ""),
|
||||||
|
doc(Transform, "Nudge (Shift: big nudge)", &["ArrowRight"], &[], Gesture("Nudge (Shift: big nudge)"), ""),
|
||||||
|
doc(Transform, "Suspend snapping while dragging", &["Ctrl"], &[], Gesture("Suspend snapping while dragging"),
|
||||||
|
"Control on macOS (Cmd keeps working); Ctrl elsewhere"),
|
||||||
|
doc(Transform, "Show the rotation origin to move it", &["Alt+R"], &[], Missing, "layers turn about their centre; no movable origin yet"),
|
||||||
|
// Arrange
|
||||||
|
doc(Arrange, "Bring forward", &["Cmd+]"], &[], Bound("arrange.forward"), ""),
|
||||||
|
doc(Arrange, "Send backward", &["Cmd+["], &[], Bound("arrange.backward"), ""),
|
||||||
|
doc(Arrange, "Bring to front", &["Alt+Cmd+]"], &["Ctrl+Shift+]"], Bound("arrange.front"), "one layout table shows only the bracket"),
|
||||||
|
doc(Arrange, "Send to back", &["Alt+Cmd+["], &["Ctrl+Shift+["], Bound("arrange.back"), ""),
|
||||||
|
doc(Arrange, "Align left", &["Alt+A"], &[], Bound("align.left"), ""),
|
||||||
|
doc(Arrange, "Align horizontal centres", &["Alt+H"], &[], Bound("align.hcenter"), ""),
|
||||||
|
doc(Arrange, "Align right", &["Alt+D"], &[], Bound("align.right"), ""),
|
||||||
|
doc(Arrange, "Align top", &["Alt+W"], &[], Bound("align.top"), ""),
|
||||||
|
doc(Arrange, "Align vertical centres", &["Alt+V"], &[], Bound("align.vcenter"), ""),
|
||||||
|
doc(Arrange, "Align bottom", &["Alt+S"], &[], Bound("align.bottom"), ""),
|
||||||
|
doc(Arrange, "Add auto layout", &["Shift+A"], &[], Bound("layout.add"), ""),
|
||||||
|
doc(Arrange, "Remove auto layout", &["Alt+Shift+A"], &[], Bound("layout.remove"), ""),
|
||||||
|
doc(Arrange, "Suggest auto layout", &["Ctrl+Shift+A"], &["Ctrl+Alt+Shift+A"], Bound("layout.suggest"), "was unbound"),
|
||||||
|
pin(Arrange, "distribute.horizontal", &["Ctrl+Alt+H"], &["Alt+Shift+H"]),
|
||||||
|
pin(Arrange, "distribute.vertical", &["Ctrl+Alt+V"], &["Alt+Shift+V"]),
|
||||||
|
pin(Arrange, "arrange.tidy", &["Ctrl+Alt+T"], &["Ctrl+Alt+Shift+T"]),
|
||||||
|
// Components
|
||||||
|
doc(Components, "Create a component", &["Alt+Cmd+K"], &["Ctrl+Alt+K"], Bound("component.create"), ""),
|
||||||
|
doc(Components, "Detach an instance", &["Alt+Cmd+B"], &["Ctrl+Alt+B"], Bound("component.detach"), ""),
|
||||||
|
doc(Components, "Go to the main component", &["Ctrl+Alt+Cmd+K"], &["Ctrl+Alt+Shift+K"], Bound("component.goToMain"), "was unbound"),
|
||||||
|
doc(Components, "Quick insert", &["Shift+I"], &[], Bound("view.assets"), "opens the Assets panel's search"),
|
||||||
|
// Prototype
|
||||||
|
doc(Prototype, "Preview in the canvas", &["Shift+Space"], &[], Bound("proto.preview"), ""),
|
||||||
|
doc(Prototype, "Present", &["Alt+Cmd+Enter"], &["Ctrl+Alt+Enter"], Bound("proto.present"), ""),
|
||||||
|
doc(Prototype, "Presenting: restart", &["R"], &[], Gesture("Presenting: restart"), "the inline preview too"),
|
||||||
|
doc(Prototype, "Presenting: next frame", &["ArrowRight", "Space", "N"], &[], Gesture("Presenting: next frame"), "Space and N were unbound"),
|
||||||
|
doc(Prototype, "Presenting: previous frame", &["ArrowLeft"], &[], Gesture("Presenting: previous frame"), ""),
|
||||||
|
doc(Prototype, "Presenting: cycle the size options", &["Z"], &[], Gesture("Presenting: cycle the size options"), ""),
|
||||||
|
doc(Prototype, "Presenting: comments", &["C"], &[], Gesture("Presenting: comments"), "Shift+C kept; keys the prototype uses go to it first"),
|
||||||
|
doc(Prototype, "Presenting: full screen", &["F"], &[], Gesture("Presenting: full screen"), "new"),
|
||||||
|
doc(View, "Place an object with the tool chosen from the toolbar", &["Enter"], &[], Missing, "Enter picks the tool; nothing is placed until the canvas is clicked"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Chords the operating system takes before any app sees them; no default binding may use one.
|
||||||
|
const OS_RESERVED: &[(Platform, &str, &str)] = &[
|
||||||
|
(Platform::Mac, "Cmd+H", "hide the app"),
|
||||||
|
(Platform::Mac, "Cmd+M", "minimize"),
|
||||||
|
(Platform::Mac, "Cmd+Q", "quit"),
|
||||||
|
(Platform::Mac, "Cmd+Tab", "app switcher"),
|
||||||
|
(Platform::Mac, "Cmd+Space", "Spotlight"),
|
||||||
|
(Platform::Mac, "Ctrl+Space", "next input source"),
|
||||||
|
(Platform::Mac, "Alt+Cmd+Escape", "force quit"),
|
||||||
|
(Platform::Mac, "Shift+Cmd+3", "screenshot"),
|
||||||
|
(Platform::Mac, "Shift+Cmd+4", "screenshot"),
|
||||||
|
(Platform::Mac, "Shift+Cmd+5", "screenshot"),
|
||||||
|
(Platform::Mac, "Shift+Cmd+/", "Help menu search"),
|
||||||
|
(Platform::Mac, "Ctrl+ArrowLeft", "Mission Control spaces"),
|
||||||
|
(Platform::Mac, "Ctrl+ArrowRight", "Mission Control spaces"),
|
||||||
|
(Platform::Mac, "Ctrl+ArrowUp", "Mission Control"),
|
||||||
|
(Platform::Mac, "Ctrl+ArrowDown", "App Exposé"),
|
||||||
|
(Platform::Other, "Alt+Space", "the window menu"),
|
||||||
|
(Platform::Other, "Alt+Tab", "window switcher"),
|
||||||
|
(Platform::Other, "Alt+F4", "close the window"),
|
||||||
|
(Platform::Other, "Ctrl+Escape", "Start menu"),
|
||||||
|
(Platform::Other, "Ctrl+Shift+Escape", "Task Manager"),
|
||||||
|
(Platform::Other, "Ctrl+Alt+Delete", "security screen"),
|
||||||
|
(Platform::Other, "Ctrl+Alt+ArrowLeft", "Linux workspaces"),
|
||||||
|
(Platform::Other, "Ctrl+Alt+ArrowRight", "Linux workspaces"),
|
||||||
|
(Platform::Other, "Ctrl+Alt+F1", "Linux virtual terminals"),
|
||||||
|
(Platform::Other, "Ctrl+Alt+F2", "Linux virtual terminals"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A chord as the platform sees it (Cmd and Ctrl are one key off macOS).
|
||||||
|
fn chord(p: Platform, spec: &str) -> (bool, bool, bool, bool, String) {
|
||||||
|
let (m, k) = parse(spec).unwrap_or_else(|| panic!("`{spec}` doesn't parse"));
|
||||||
|
let (cmd, ctrl) = match p {
|
||||||
|
Platform::Mac => (m.cmd, m.ctrl),
|
||||||
|
Platform::Other => (m.cmd || m.ctrl, false),
|
||||||
|
};
|
||||||
|
(m.shift, m.alt, cmd, ctrl, k.to_ascii_uppercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The commands a chord fires on the canvas and while editing text.
|
||||||
|
fn fires(p: Platform, spec: &str) -> [Option<&'static str>; 2] {
|
||||||
|
let (m, k) = parse(spec).unwrap_or_else(|| panic!("`{spec}` doesn't parse"));
|
||||||
|
[Context::Canvas, Context::Text].map(|c| lookup(p, c, k, m).map(|b| b.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keys of gesture rows name a key, not a chord to look up.
|
||||||
|
fn is_chord(spec: &str) -> bool {
|
||||||
|
!matches!(spec, "Cmd" | "Alt" | "Ctrl")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn documented_shortcuts_fire_their_commands_on_both_platforms() {
|
||||||
|
for row in ROWS {
|
||||||
|
for p in PLATFORMS {
|
||||||
|
assert!(row.keys(p).iter().all(|k| !is_chord(k) || parse(k).is_some()), "{}: keys don't parse", row.action);
|
||||||
|
if let Some((id, keys)) = row.ours(p) {
|
||||||
|
assert!(!keys.is_empty(), "{}", row.action);
|
||||||
|
for k in keys {
|
||||||
|
assert!(fires(p, k).contains(&Some(id)), "{p:?}: `{k}` should run {id} ({}), runs {:?}", row.action, fires(p, k));
|
||||||
|
}
|
||||||
|
// The panel lists the binding in the row's tab.
|
||||||
|
assert!(KEYS.iter().any(|b| b.id == id && b.group == row.group), "{id} isn't in the {:?} tab", row.group);
|
||||||
|
}
|
||||||
|
if let Moved(id, ..) = row.ours {
|
||||||
|
let ours = row.ours(p).map(|o| o.1).unwrap_or_default();
|
||||||
|
for k in row.keys(p).iter().filter(|k| !ours.contains(k)) {
|
||||||
|
assert!(!fires(p, k).contains(&Some(id)), "{p:?}: `{k}` was moved off {id}");
|
||||||
|
assert!(fires(p, k).iter().any(Option::is_some), "{p:?}: `{k}` should be the conflicting documented command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Gesture(label) = row.ours {
|
||||||
|
assert!(GESTURES.iter().any(|g| g.0 == row.group && g.1 == label), "the panel's {:?} tab doesn't list `{label}`", row.group);
|
||||||
|
}
|
||||||
|
assert!(row.documented || !row.note.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vector_edit_tool_keys() {
|
||||||
|
for row in ROWS {
|
||||||
|
let VectorTool(tool) = row.ours else { continue };
|
||||||
|
let mut s = Session::new();
|
||||||
|
let r = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||||
|
s.execute("vector.edit.begin", &json!({"id": id_of(&r["id"]).unwrap()})).unwrap();
|
||||||
|
for k in row.mac {
|
||||||
|
let (m, key) = parse(k).unwrap();
|
||||||
|
assert!(crate::vectoredit::key(&mut s, key, m.shift, m.alt, m.cmd).unwrap(), "`{k}` taken");
|
||||||
|
assert_eq!(s.ds().unwrap().vector_edit.as_ref().unwrap().tool, tool, "`{k}`");
|
||||||
|
}
|
||||||
|
assert_eq!(tool.shortcut(), row.mac[0], "the tooltip shows the documented key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_default_binding_is_in_the_table() {
|
||||||
|
for b in KEYS {
|
||||||
|
for p in PLATFORMS {
|
||||||
|
for k in b.keys(p) {
|
||||||
|
let listed = ROWS.iter().filter_map(|r| r.ours(p)).any(|(id, keys)| id == b.id && keys.iter().any(|x| chord(p, x) == chord(p, k)));
|
||||||
|
assert!(listed, "{p:?}: `{k}` ({}) has no row: add it, documented or pinned", b.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn counts_match_the_docs() {
|
||||||
|
let documented: Vec<&Row> = ROWS.iter().filter(|r| r.documented).collect();
|
||||||
|
let bound = documented.iter().filter(|r| matches!(r.ours, Bound(_) | Gesture(_) | VectorTool(_))).count();
|
||||||
|
let moved = documented.iter().filter(|r| matches!(r.ours, Moved(..))).count();
|
||||||
|
let missing = documented.iter().filter(|r| matches!(r.ours, Missing)).count();
|
||||||
|
let reserved = documented.iter().filter(|r| !r.reserved.is_empty()).count();
|
||||||
|
let pinned = ROWS.len() - documented.len();
|
||||||
|
// docs/ui-parity.md quotes these.
|
||||||
|
assert_eq!((documented.len(), bound, moved, missing, reserved, pinned), (129, 126, 1, 2, 2, 48));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_binding_takes_a_chord_the_system_keeps() {
|
||||||
|
for (p, spec, what) in OS_RESERVED {
|
||||||
|
for b in KEYS {
|
||||||
|
assert!(b.keys(*p).iter().all(|k| chord(*p, k) != chord(*p, spec)), "{p:?}: {} uses `{spec}` ({what})", b.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modifiers_read_the_platform_way() {
|
||||||
|
for b in KEYS {
|
||||||
|
for k in b.keys(Platform::Mac) {
|
||||||
|
let shown = display(k, Platform::Mac);
|
||||||
|
assert!(!shown.contains("Ctrl") && !shown.contains("Alt"), "{}: macOS shows `{shown}`", b.id);
|
||||||
|
}
|
||||||
|
for k in b.keys(Platform::Other) {
|
||||||
|
let shown = display(k, Platform::Other);
|
||||||
|
assert!(!shown.contains(['⌘', '⌥', '⌃', '⇧']), "{}: Windows/Linux shows `{shown}`", b.id);
|
||||||
|
// Windows and Linux have no Command key: Cmd and Ctrl both mean Ctrl there.
|
||||||
|
assert!(!(k.contains("Cmd") && k.contains("Ctrl")), "{}: `{k}` names Ctrl twice off macOS", b.id);
|
||||||
|
}
|
||||||
|
assert!(b.keys(Platform::Mac).iter().chain(b.keys(Platform::Other)).all(|k| !k.contains("Option") && !k.contains("Win")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_held_while_dragging_suspends_snapping() {
|
||||||
|
use crate::tools::{PointerEvent, PointerKind};
|
||||||
|
let mut s = Session::new();
|
||||||
|
for x in [0, 400] {
|
||||||
|
s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 100, "height": 100})).unwrap();
|
||||||
|
}
|
||||||
|
let b = id_of(&s.execute("shape.rectangle", &json!({"x": 200, "y": 300, "width": 100, "height": 100})).unwrap()["id"]).unwrap();
|
||||||
|
s.execute("select.set", &json!({"ids": [b]})).unwrap();
|
||||||
|
let ev = |kind, x, y, ctrl| {
|
||||||
|
let mut e = PointerEvent::at(kind, x, y, 1.0);
|
||||||
|
e.ctrl = ctrl;
|
||||||
|
e
|
||||||
|
};
|
||||||
|
// Grabbed at its centre and dropped 2 px off equal gaps between the other two.
|
||||||
|
for (ctrl, want) in [(false, (200.0, 0.0)), (true, (198.0, 2.0))] {
|
||||||
|
s.pointer(&ev(PointerKind::Down, 250.0, 350.0, ctrl)).unwrap();
|
||||||
|
s.pointer(&ev(PointerKind::Move, 248.0, 52.0, ctrl)).unwrap();
|
||||||
|
s.pointer(&ev(PointerKind::Up, 248.0, 52.0, ctrl)).unwrap();
|
||||||
|
let r = s.doc().unwrap().world_bounds(b).unwrap();
|
||||||
|
assert_eq!((r.x0, r.y0), want, "Control held: {ctrl}");
|
||||||
|
s.execute("edit.undo", &json!({})).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -269,6 +269,9 @@ fn every_command_survives_junk_params() {
|
||||||
// Prototype expressions and gamepad keys as junk.
|
// Prototype expressions and gamepad keys as junk.
|
||||||
json!({"id": -5, "expression": "x[\u{0}", "expr": {"op": {"op": "add", "args": [1]}}, "trigger": {"type": "keyPress", "key": "Gamepad \u{0}"}}),
|
json!({"id": -5, "expression": "x[\u{0}", "expr": {"op": {"op": "add", "args": [1]}}, "trigger": {"type": "keyPress", "key": "Gamepad \u{0}"}}),
|
||||||
json!({"id": 1, "expr": {"inMode": {"variable": [], "mode": 1e308}}, "trigger": {"type": "keyPress", "key": 7}}),
|
json!({"id": 1, "expr": {"inMode": {"variable": [], "mode": 1e308}}, "trigger": {"type": "keyPress", "key": 7}}),
|
||||||
|
// Keyboard steps (view.pan) and toggles as junk.
|
||||||
|
json!({"dx": 1e308, "dy": "NaN", "big": "yes", "on": 3}),
|
||||||
|
json!({"dx": -1e308, "dy": [1], "big": null, "on": null}),
|
||||||
];
|
];
|
||||||
for spec in all_commands() {
|
for spec in all_commands() {
|
||||||
if matches!(
|
if matches!(
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,12 @@ fn list_key(s: &mut Session, key: &str, shift: bool) -> Result<bool> {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cmd/Ctrl+] and Cmd/Ctrl+[ while editing: the selected list items a level deeper or out, like
|
||||||
|
/// Tab and Shift+Tab. Returns whether anything changed (text that isn't a list stays as it is).
|
||||||
|
pub fn list_indent(s: &mut Session, out: bool) -> Result<bool> {
|
||||||
|
list_key(s, "Tab", out)
|
||||||
|
}
|
||||||
|
|
||||||
/// The kind of word char `i` belongs to: 0 for spaces and punctuation, else its script, so a word
|
/// The kind of word char `i` belongs to: 0 for spaces and punctuation, else its script, so a word
|
||||||
/// ends where the script changes (Han, hiragana, katakana and Hangul runs are words of their own in
|
/// ends where the script changes (Han, hiragana, katakana and Hangul runs are words of their own in
|
||||||
/// text without spaces). Combining marks and voicing marks belong to the char before them.
|
/// text without spaces). Combining marks and voicing marks belong to the char before them.
|
||||||
|
|
|
||||||
|
|
@ -1064,7 +1064,8 @@ impl Session {
|
||||||
d.x = 0.0;
|
d.x = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let snap_on = self.ds().map(|ds| ds.view.snap_objects).unwrap_or(true) && !ev.cmd;
|
// Cmd/Ctrl (and Control on macOS) held while dragging suspends snapping.
|
||||||
|
let snap_on = self.ds().map(|ds| ds.view.snap_objects).unwrap_or(true) && !ev.cmd && !ev.ctrl;
|
||||||
let mut guides = Vec::new();
|
let mut guides = Vec::new();
|
||||||
if snap_on {
|
if snap_on {
|
||||||
let (c, g2) = targets.snap_move(d, SNAP_PX / ev.zoom.max(1e-6));
|
let (c, g2) = targets.snap_move(d, SNAP_PX / ev.zoom.max(1e-6));
|
||||||
|
|
@ -1266,7 +1267,7 @@ impl Session {
|
||||||
|
|
||||||
fn resize_drag(&mut self, ev: &PointerEvent, start: Point, r: &ResizeStart, scale_tool: bool) -> Result<()> {
|
fn resize_drag(&mut self, ev: &PointerEvent, start: Point, r: &ResizeStart, scale_tool: bool) -> Result<()> {
|
||||||
let page = self.page()?;
|
let page = self.page()?;
|
||||||
let snap = self.ds().map(|d| d.view.snap_objects).unwrap_or(true) && !ev.cmd;
|
let snap = self.ds().map(|d| d.view.snap_objects).unwrap_or(true) && !ev.cmd && !ev.ctrl;
|
||||||
let ids: Vec<NodeId> = r.nodes.iter().map(|n| n.0).collect();
|
let ids: Vec<NodeId> = r.nodes.iter().map(|n| n.0).collect();
|
||||||
let targets = if snap { Some(Targets::gather(self.doc()?, page, &ids)) } else { None };
|
let targets = if snap { Some(Targets::gather(self.doc()?, page, &ids)) } else { None };
|
||||||
let mut p = ev.p();
|
let mut p = ev.p();
|
||||||
|
|
@ -1432,7 +1433,7 @@ impl Session {
|
||||||
let tool = self.tool.tool;
|
let tool = self.tool.tool;
|
||||||
let mut p = ev.p();
|
let mut p = ev.p();
|
||||||
let mut guides = Vec::new();
|
let mut guides = Vec::new();
|
||||||
if self.ds().map(|d| d.view.snap_objects).unwrap_or(true) && !ev.cmd {
|
if self.ds().map(|d| d.view.snap_objects).unwrap_or(true) && !ev.cmd && !ev.ctrl {
|
||||||
let (dx, dy, g) = targets.snap_point(p.x, p.y, SNAP_PX / ev.zoom.max(1e-6));
|
let (dx, dy, g) = targets.snap_point(p.x, p.y, SNAP_PX / ev.zoom.max(1e-6));
|
||||||
p.x += dx;
|
p.x += dx;
|
||||||
p.y += dy;
|
p.y += dy;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ impl VTool {
|
||||||
VTool::Move => "V",
|
VTool::Move => "V",
|
||||||
VTool::Pen => "P",
|
VTool::Pen => "P",
|
||||||
VTool::Bend => "Cmd",
|
VTool::Bend => "Cmd",
|
||||||
VTool::Paint => "B",
|
VTool::Paint => "Shift+B",
|
||||||
VTool::Lasso => "Q",
|
VTool::Lasso => "Q",
|
||||||
VTool::Cut => "X",
|
VTool::Cut => "X",
|
||||||
VTool::Eraser => "Shift+E",
|
VTool::Eraser => "Shift+E",
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,9 @@ impl Headless {
|
||||||
"ArrowUp" => (0, -1),
|
"ArrowUp" => (0, -1),
|
||||||
_ => (0, 1),
|
_ => (0, 1),
|
||||||
};
|
};
|
||||||
self.execute("transform.nudge", &json!({"dx": dx, "dy": dy, "big": m.shift}))
|
// With nothing selected the arrows pan the view.
|
||||||
|
let cmd = if self.session.selection().is_empty() { "view.pan" } else { "transform.nudge" };
|
||||||
|
self.execute(cmd, &json!({"dx": dx, "dy": dy, "big": m.shift}))
|
||||||
}
|
}
|
||||||
// Number keys set the selection's opacity.
|
// Number keys set the selection's opacity.
|
||||||
d if m.is_none() && d.len() == 1 && d.chars().all(|c| c.is_ascii_digit()) && self.session.prefs.number_keys_opacity => {
|
d if m.is_none() && d.len() == 1 && d.chars().all(|c| c.is_ascii_digit()) && self.session.prefs.number_keys_opacity => {
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,8 @@ name = "English"
|
||||||
"Brush size jitter" = "Brush size jitter"
|
"Brush size jitter" = "Brush size jitter"
|
||||||
"Brush spacing" = "Brush spacing"
|
"Brush spacing" = "Brush spacing"
|
||||||
"Bulleted list" = "Bulleted list"
|
"Bulleted list" = "Bulleted list"
|
||||||
|
"C or Shift+C" = "C or Shift+C"
|
||||||
|
"C or ⇧C" = "C or ⇧C"
|
||||||
"CMYK colour" = "CMYK colour"
|
"CMYK colour" = "CMYK colour"
|
||||||
"Color profile: {name}" = "Color profile: {name}"
|
"Color profile: {name}" = "Color profile: {name}"
|
||||||
"CSS custom properties…" = "CSS custom properties…"
|
"CSS custom properties…" = "CSS custom properties…"
|
||||||
|
|
@ -431,6 +433,7 @@ name = "English"
|
||||||
"Extend collection" = "Extend collection"
|
"Extend collection" = "Extend collection"
|
||||||
"Extends \"{name}\": overridden values are marked" = "Extends \"{name}\": overridden values are marked"
|
"Extends \"{name}\": overridden values are marked" = "Extends \"{name}\": overridden values are marked"
|
||||||
"Eye" = "Eye"
|
"Eye" = "Eye"
|
||||||
|
"F" = "F"
|
||||||
"Fade" = "Fade"
|
"Fade" = "Fade"
|
||||||
"False" = "False"
|
"False" = "False"
|
||||||
"Fetching from the server…" = "Fetching from the server…"
|
"Fetching from the server…" = "Fetching from the server…"
|
||||||
|
|
@ -522,7 +525,9 @@ name = "English"
|
||||||
"Highlights" = "Highlights"
|
"Highlights" = "Highlights"
|
||||||
"Hold Alt" = "Hold Alt"
|
"Hold Alt" = "Hold Alt"
|
||||||
"Hold Alt+Z" = "Hold Alt+Z"
|
"Hold Alt+Z" = "Hold Alt+Z"
|
||||||
|
"Hold Ctrl" = "Hold Ctrl"
|
||||||
"Hold Z" = "Hold Z"
|
"Hold Z" = "Hold Z"
|
||||||
|
"Hold ⌃ or ⌘" = "Hold ⌃ or ⌘"
|
||||||
"Hold ⌥" = "Hold ⌥"
|
"Hold ⌥" = "Hold ⌥"
|
||||||
"Hold ⌥Z" = "Hold ⌥Z"
|
"Hold ⌥Z" = "Hold ⌥Z"
|
||||||
"Home" = "Home"
|
"Home" = "Home"
|
||||||
|
|
@ -701,6 +706,7 @@ name = "English"
|
||||||
"Mouse enter" = "Mouse enter"
|
"Mouse enter" = "Mouse enter"
|
||||||
"Mouse leave" = "Mouse leave"
|
"Mouse leave" = "Mouse leave"
|
||||||
"Mouse up" = "Mouse up"
|
"Mouse up" = "Mouse up"
|
||||||
|
"Move along the toolbar or the layer rows" = "Move along the toolbar or the layer rows"
|
||||||
"Move" = "Move"
|
"Move" = "Move"
|
||||||
"Move column left" = "Move column left"
|
"Move column left" = "Move column left"
|
||||||
"Move column right" = "Move column right"
|
"Move column right" = "Move column right"
|
||||||
|
|
@ -835,6 +841,7 @@ name = "English"
|
||||||
"Pages: {pages}" = "Pages: {pages}"
|
"Pages: {pages}" = "Pages: {pages}"
|
||||||
"Paint bucket" = "Paint bucket"
|
"Paint bucket" = "Paint bucket"
|
||||||
"Pan the canvas" = "Pan the canvas"
|
"Pan the canvas" = "Pan the canvas"
|
||||||
|
"Pan with nothing selected (Shift: farther)" = "Pan with nothing selected (Shift: farther)"
|
||||||
"Panels" = "Panels"
|
"Panels" = "Panels"
|
||||||
"Paper" = "Paper"
|
"Paper" = "Paper"
|
||||||
"Paragraph indent" = "Paragraph indent"
|
"Paragraph indent" = "Paragraph indent"
|
||||||
|
|
@ -880,6 +887,12 @@ name = "English"
|
||||||
"Present or preview" = "Present or preview"
|
"Present or preview" = "Present or preview"
|
||||||
"Present this flow" = "Present this flow"
|
"Present this flow" = "Present this flow"
|
||||||
"Presentation" = "Presentation"
|
"Presentation" = "Presentation"
|
||||||
|
"Presenting: comments" = "Presenting: comments"
|
||||||
|
"Presenting: cycle the size options" = "Presenting: cycle the size options"
|
||||||
|
"Presenting: full screen" = "Presenting: full screen"
|
||||||
|
"Presenting: next frame" = "Presenting: next frame"
|
||||||
|
"Presenting: previous frame" = "Presenting: previous frame"
|
||||||
|
"Presenting: restart" = "Presenting: restart"
|
||||||
"Preserve scroll position" = "Preserve scroll position"
|
"Preserve scroll position" = "Preserve scroll position"
|
||||||
"Presets" = "Presets"
|
"Presets" = "Presets"
|
||||||
"Press a key…" = "Press a key…"
|
"Press a key…" = "Press a key…"
|
||||||
|
|
@ -905,6 +918,7 @@ name = "English"
|
||||||
"Push" = "Push"
|
"Push" = "Push"
|
||||||
"Push changes to main component" = "Push changes to main component"
|
"Push changes to main component" = "Push changes to main component"
|
||||||
"Put text on path" = "Put text on path"
|
"Put text on path" = "Put text on path"
|
||||||
|
"R" = "R"
|
||||||
"Radial" = "Radial"
|
"Radial" = "Radial"
|
||||||
"Radius" = "Radius"
|
"Radius" = "Radius"
|
||||||
"Ratio" = "Ratio"
|
"Ratio" = "Ratio"
|
||||||
|
|
@ -1143,6 +1157,7 @@ name = "English"
|
||||||
"Subtract" = "Subtract"
|
"Subtract" = "Subtract"
|
||||||
"Subtracts" = "Subtracts"
|
"Subtracts" = "Subtracts"
|
||||||
"Suffix" = "Suffix"
|
"Suffix" = "Suffix"
|
||||||
|
"Suspend snapping while dragging" = "Suspend snapping while dragging"
|
||||||
"Swap instance" = "Swap instance"
|
"Swap instance" = "Swap instance"
|
||||||
"Swap library: use another library's assets of the same names" = "Swap library: use another library's assets of the same names"
|
"Swap library: use another library's assets of the same names" = "Swap library: use another library's assets of the same names"
|
||||||
"Swap orientation" = "Swap orientation"
|
"Swap orientation" = "Swap orientation"
|
||||||
|
|
@ -1303,6 +1318,7 @@ name = "English"
|
||||||
"Your name" = "Your name"
|
"Your name" = "Your name"
|
||||||
"Your name in comments" = "Your name in comments"
|
"Your name in comments" = "Your name in comments"
|
||||||
"Your system asks for reduced motion: animations stay off." = "Your system asks for reduced motion: animations stay off."
|
"Your system asks for reduced motion: animations stay off." = "Your system asks for reduced motion: animations stay off."
|
||||||
|
"Z" = "Z"
|
||||||
"Zoom" = "Zoom"
|
"Zoom" = "Zoom"
|
||||||
"Zoom around the pointer" = "Zoom around the pointer"
|
"Zoom around the pointer" = "Zoom around the pointer"
|
||||||
"Zoom device to fill window" = "Zoom device to fill window"
|
"Zoom device to fill window" = "Zoom device to fill window"
|
||||||
|
|
@ -1398,6 +1414,9 @@ name = "English"
|
||||||
"{value} isn't a colour" = "{value} isn't a colour"
|
"{value} isn't a colour" = "{value} isn't a colour"
|
||||||
"{when} · edited" = "{when} · edited"
|
"{when} · edited" = "{when} · edited"
|
||||||
"← ↑ → ↓" = "← ↑ → ↓"
|
"← ↑ → ↓" = "← ↑ → ↓"
|
||||||
|
"← → ↑ ↓" = "← → ↑ ↓"
|
||||||
|
"←" = "←"
|
||||||
|
"→, Space or N" = "→, Space or N"
|
||||||
"⇥ / ⇧⇥" = "⇥ / ⇧⇥"
|
"⇥ / ⇧⇥" = "⇥ / ⇧⇥"
|
||||||
"⌘ arrows" = "⌘ arrows"
|
"⌘ arrows" = "⌘ arrows"
|
||||||
"⌘ click" = "⌘ click"
|
"⌘ click" = "⌘ click"
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,8 @@ status = "draft — needs native review"
|
||||||
"Brush size jitter" = "Variación de tamaño del pincel"
|
"Brush size jitter" = "Variación de tamaño del pincel"
|
||||||
"Brush spacing" = "Espaciado del pincel"
|
"Brush spacing" = "Espaciado del pincel"
|
||||||
"Bulleted list" = "Lista con viñetas"
|
"Bulleted list" = "Lista con viñetas"
|
||||||
|
"C or Shift+C" = "C o Shift+C"
|
||||||
|
"C or ⇧C" = "C o ⇧C"
|
||||||
"CMYK colour" = "Color CMYK"
|
"CMYK colour" = "Color CMYK"
|
||||||
"Color profile: {name}" = "Perfil de color: {name}"
|
"Color profile: {name}" = "Perfil de color: {name}"
|
||||||
"CSS custom properties…" = "Propiedades personalizadas CSS…"
|
"CSS custom properties…" = "Propiedades personalizadas CSS…"
|
||||||
|
|
@ -432,6 +434,7 @@ status = "draft — needs native review"
|
||||||
"Extend collection" = "Extender colección"
|
"Extend collection" = "Extender colección"
|
||||||
"Extends \"{name}\": overridden values are marked" = "Extiende \"{name}\": los valores modificados están marcados"
|
"Extends \"{name}\": overridden values are marked" = "Extiende \"{name}\": los valores modificados están marcados"
|
||||||
"Eye" = "Ojo"
|
"Eye" = "Ojo"
|
||||||
|
"F" = "F"
|
||||||
"Fade" = "Desvanecido"
|
"Fade" = "Desvanecido"
|
||||||
"False" = "Falso"
|
"False" = "Falso"
|
||||||
"Fetching from the server…" = "Obteniendo del servidor…"
|
"Fetching from the server…" = "Obteniendo del servidor…"
|
||||||
|
|
@ -523,7 +526,9 @@ status = "draft — needs native review"
|
||||||
"Highlights" = "Luces"
|
"Highlights" = "Luces"
|
||||||
"Hold Alt" = "Mantén Alt"
|
"Hold Alt" = "Mantén Alt"
|
||||||
"Hold Alt+Z" = "Mantén Alt+Z"
|
"Hold Alt+Z" = "Mantén Alt+Z"
|
||||||
|
"Hold Ctrl" = "Mantén Ctrl"
|
||||||
"Hold Z" = "Mantén Z"
|
"Hold Z" = "Mantén Z"
|
||||||
|
"Hold ⌃ or ⌘" = "Mantén ⌃ o ⌘"
|
||||||
"Hold ⌥" = "Mantén ⌥"
|
"Hold ⌥" = "Mantén ⌥"
|
||||||
"Hold ⌥Z" = "Mantén ⌥Z"
|
"Hold ⌥Z" = "Mantén ⌥Z"
|
||||||
"Home" = "Inicio"
|
"Home" = "Inicio"
|
||||||
|
|
@ -702,6 +707,7 @@ status = "draft — needs native review"
|
||||||
"Mouse enter" = "Al entrar el puntero"
|
"Mouse enter" = "Al entrar el puntero"
|
||||||
"Mouse leave" = "Al salir el puntero"
|
"Mouse leave" = "Al salir el puntero"
|
||||||
"Mouse up" = "Al soltar el botón"
|
"Mouse up" = "Al soltar el botón"
|
||||||
|
"Move along the toolbar or the layer rows" = "Recorrer la barra de herramientas o las filas de capas"
|
||||||
"Move" = "Mover"
|
"Move" = "Mover"
|
||||||
"Move column left" = "Mover columna a la izquierda"
|
"Move column left" = "Mover columna a la izquierda"
|
||||||
"Move column right" = "Mover columna a la derecha"
|
"Move column right" = "Mover columna a la derecha"
|
||||||
|
|
@ -836,6 +842,7 @@ status = "draft — needs native review"
|
||||||
"Pages: {pages}" = "Páginas: {pages}"
|
"Pages: {pages}" = "Páginas: {pages}"
|
||||||
"Paint bucket" = "Bote de pintura"
|
"Paint bucket" = "Bote de pintura"
|
||||||
"Pan the canvas" = "Desplazar el lienzo"
|
"Pan the canvas" = "Desplazar el lienzo"
|
||||||
|
"Pan with nothing selected (Shift: farther)" = "Desplazar sin nada seleccionado (Mayús: más lejos)"
|
||||||
"Panels" = "Paneles"
|
"Panels" = "Paneles"
|
||||||
"Paper" = "Papel"
|
"Paper" = "Papel"
|
||||||
"Paragraph indent" = "Sangría de párrafo"
|
"Paragraph indent" = "Sangría de párrafo"
|
||||||
|
|
@ -881,6 +888,12 @@ status = "draft — needs native review"
|
||||||
"Present or preview" = "Presentar o previsualizar"
|
"Present or preview" = "Presentar o previsualizar"
|
||||||
"Present this flow" = "Presentar este flujo"
|
"Present this flow" = "Presentar este flujo"
|
||||||
"Presentation" = "Presentación"
|
"Presentation" = "Presentación"
|
||||||
|
"Presenting: comments" = "Presentando: comentarios"
|
||||||
|
"Presenting: cycle the size options" = "Presentando: alternar las opciones de tamaño"
|
||||||
|
"Presenting: full screen" = "Presentando: pantalla completa"
|
||||||
|
"Presenting: next frame" = "Presentando: siguiente marco"
|
||||||
|
"Presenting: previous frame" = "Presentando: marco anterior"
|
||||||
|
"Presenting: restart" = "Presentando: reiniciar"
|
||||||
"Preserve scroll position" = "Conservar posición de desplazamiento"
|
"Preserve scroll position" = "Conservar posición de desplazamiento"
|
||||||
"Presets" = "Ajustes predefinidos"
|
"Presets" = "Ajustes predefinidos"
|
||||||
"Press a key…" = "Pulsa una tecla…"
|
"Press a key…" = "Pulsa una tecla…"
|
||||||
|
|
@ -906,6 +919,7 @@ status = "draft — needs native review"
|
||||||
"Push" = "Empujar"
|
"Push" = "Empujar"
|
||||||
"Push changes to main component" = "Enviar cambios al componente principal"
|
"Push changes to main component" = "Enviar cambios al componente principal"
|
||||||
"Put text on path" = "Poner texto en trazado"
|
"Put text on path" = "Poner texto en trazado"
|
||||||
|
"R" = "R"
|
||||||
"Radial" = "Radial"
|
"Radial" = "Radial"
|
||||||
"Radius" = "Radio"
|
"Radius" = "Radio"
|
||||||
"Ratio" = "Proporción"
|
"Ratio" = "Proporción"
|
||||||
|
|
@ -1144,6 +1158,7 @@ status = "draft — needs native review"
|
||||||
"Subtract" = "Restar"
|
"Subtract" = "Restar"
|
||||||
"Subtracts" = "Restas"
|
"Subtracts" = "Restas"
|
||||||
"Suffix" = "Sufijo"
|
"Suffix" = "Sufijo"
|
||||||
|
"Suspend snapping while dragging" = "Suspender el ajuste al arrastrar"
|
||||||
"Swap instance" = "Intercambiar instancia"
|
"Swap instance" = "Intercambiar instancia"
|
||||||
"Swap library: use another library's assets of the same names" = "Intercambiar biblioteca: usar los recursos con los mismos nombres de otra biblioteca"
|
"Swap library: use another library's assets of the same names" = "Intercambiar biblioteca: usar los recursos con los mismos nombres de otra biblioteca"
|
||||||
"Swap orientation" = "Cambiar orientación"
|
"Swap orientation" = "Cambiar orientación"
|
||||||
|
|
@ -1304,6 +1319,7 @@ status = "draft — needs native review"
|
||||||
"Your name" = "Tu nombre"
|
"Your name" = "Tu nombre"
|
||||||
"Your name in comments" = "Tu nombre en los comentarios"
|
"Your name in comments" = "Tu nombre en los comentarios"
|
||||||
"Your system asks for reduced motion: animations stay off." = "Tu sistema pide reducir el movimiento: las animaciones siguen desactivadas."
|
"Your system asks for reduced motion: animations stay off." = "Tu sistema pide reducir el movimiento: las animaciones siguen desactivadas."
|
||||||
|
"Z" = "Z"
|
||||||
"Zoom" = "Zoom"
|
"Zoom" = "Zoom"
|
||||||
"Zoom around the pointer" = "Zoom alrededor del puntero"
|
"Zoom around the pointer" = "Zoom alrededor del puntero"
|
||||||
"Zoom device to fill window" = "Ampliar el dispositivo hasta llenar la ventana"
|
"Zoom device to fill window" = "Ampliar el dispositivo hasta llenar la ventana"
|
||||||
|
|
@ -1399,6 +1415,9 @@ status = "draft — needs native review"
|
||||||
"{value} isn't a colour" = "{value} no es un color"
|
"{value} isn't a colour" = "{value} no es un color"
|
||||||
"{when} · edited" = "{when} · editado"
|
"{when} · edited" = "{when} · editado"
|
||||||
"← ↑ → ↓" = "← ↑ → ↓"
|
"← ↑ → ↓" = "← ↑ → ↓"
|
||||||
|
"← → ↑ ↓" = "← → ↑ ↓"
|
||||||
|
"←" = "←"
|
||||||
|
"→, Space or N" = "→, Espacio o N"
|
||||||
"⇥ / ⇧⇥" = "⇥ / ⇧⇥"
|
"⇥ / ⇧⇥" = "⇥ / ⇧⇥"
|
||||||
"⌘ arrows" = "⌘ flechas"
|
"⌘ arrows" = "⌘ flechas"
|
||||||
"⌘ click" = "⌘ clic"
|
"⌘ click" = "⌘ clic"
|
||||||
|
|
@ -1774,6 +1793,8 @@ status = "draft — needs native review"
|
||||||
"text.lineHeightDown" = "Reducir interlineado"
|
"text.lineHeightDown" = "Reducir interlineado"
|
||||||
"text.lineHeightUp" = "Aumentar interlineado"
|
"text.lineHeightUp" = "Aumentar interlineado"
|
||||||
"text.link" = "Crear enlace"
|
"text.link" = "Crear enlace"
|
||||||
|
"text.listIndent" = "Aumentar sangría de la lista"
|
||||||
|
"text.listOutdent" = "Reducir sangría de la lista"
|
||||||
"text.listStyle" = "Estilo de lista"
|
"text.listStyle" = "Estilo de lista"
|
||||||
"text.maxLines" = "Máximo de líneas"
|
"text.maxLines" = "Máximo de líneas"
|
||||||
"text.numberList" = "Lista numerada"
|
"text.numberList" = "Lista numerada"
|
||||||
|
|
@ -1900,11 +1921,13 @@ status = "draft — needs native review"
|
||||||
"view.layoutGrids" = "Cuadrículas de diseño"
|
"view.layoutGrids" = "Cuadrículas de diseño"
|
||||||
"view.leftSidebar" = "Mostrar/ocultar barra lateral izquierda"
|
"view.leftSidebar" = "Mostrar/ocultar barra lateral izquierda"
|
||||||
"view.maskOutlines" = "Contornos de máscaras"
|
"view.maskOutlines" = "Contornos de máscaras"
|
||||||
|
"view.multiplayerCursors" = "Cursores de otros participantes"
|
||||||
"view.nextFrame" = "Zoom al siguiente marco"
|
"view.nextFrame" = "Zoom al siguiente marco"
|
||||||
"view.nextPage" = "Página siguiente"
|
"view.nextPage" = "Página siguiente"
|
||||||
"view.nextRegion" = "Enfocar la siguiente región"
|
"view.nextRegion" = "Enfocar la siguiente región"
|
||||||
"view.outlines" = "Contornos"
|
"view.outlines" = "Contornos"
|
||||||
"view.outlinesSelection" = "Contornos de la selección"
|
"view.outlinesSelection" = "Contornos de la selección"
|
||||||
|
"view.pan" = "Desplazar la vista"
|
||||||
"view.pixelGrid" = "Cuadrícula de píxeles"
|
"view.pixelGrid" = "Cuadrícula de píxeles"
|
||||||
"view.pixelPreview" = "Vista previa de píxeles"
|
"view.pixelPreview" = "Vista previa de píxeles"
|
||||||
"view.prevFrame" = "Zoom al marco anterior"
|
"view.prevFrame" = "Zoom al marco anterior"
|
||||||
|
|
|
||||||
|
|
@ -450,11 +450,10 @@ fn keys(app: &mut FigApp, ctx: &Context) {
|
||||||
|
|
||||||
/// The first of the commands `ids` a key pressed this frame is bound to (canvas keys), with the key.
|
/// The first of the commands `ids` a key pressed this frame is bound to (canvas keys), with the key.
|
||||||
fn binding_pressed(ctx: &Context, ids: &[&str]) -> Option<(&'static str, Key, Modifiers)> {
|
fn binding_pressed(ctx: &Context, ids: &[&str]) -> Option<(&'static str, Key, Modifiers)> {
|
||||||
let platform = crate::shortcuts::platform();
|
|
||||||
ctx.input(|i| {
|
ctx.input(|i| {
|
||||||
i.events.iter().find_map(|e| match e {
|
i.events.iter().find_map(|e| match e {
|
||||||
Event::Key { key, pressed: true, modifiers, .. } => {
|
Event::Key { key, physical_key, pressed: true, modifiers, .. } => {
|
||||||
keymap::lookup(platform, keymap::Context::Canvas, key.name(), crate::shortcuts::mods(*modifiers))
|
crate::shortcuts::lookup(keymap::Context::Canvas, *key, *physical_key, *modifiers)
|
||||||
.filter(|b| ids.contains(&b.id))
|
.filter(|b| ids.contains(&b.id))
|
||||||
.map(|b| (b.id, *key, *modifiers))
|
.map(|b| (b.id, *key, *modifiers))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,8 @@ mod proto_reorder_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod proto_responsive_tests;
|
mod proto_responsive_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod shortcut_keys_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod shortcuts_tests;
|
mod shortcuts_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod titlebar_tests;
|
mod titlebar_tests;
|
||||||
|
|
|
||||||
|
|
@ -229,9 +229,11 @@ pub fn draw(app: &mut FigApp, painter: &egui::Painter, ctx: &egui::Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cursors: on your page of the session's document; before its welcome arrives (or with
|
// Cursors (unless View ▸ Multiplayer cursors is off): on your page of the session's
|
||||||
// another document in front) they show in page coordinates on whatever page you look at.
|
// document; before its welcome arrives (or with another document in front) they show in
|
||||||
|
// page coordinates on whatever page you look at.
|
||||||
if let Some((pg, x, y)) = p.cursor
|
if let Some((pg, x, y)) = p.cursor
|
||||||
|
&& ds.view.multiplayer_cursors
|
||||||
&& (pg == page || !shared)
|
&& (pg == page || !shared)
|
||||||
{
|
{
|
||||||
cursors.push((*id, peer.name.clone(), fill, (x, y)));
|
cursors.push((*id, peer.name.clone(), fill, (x, y)));
|
||||||
|
|
|
||||||
|
|
@ -1596,6 +1596,16 @@ pub fn show(app: &mut FigApp, ui: &mut Ui) {
|
||||||
egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| {
|
egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| {
|
||||||
action = player(&mut pres, ui, ui.max_rect(), now, Chrome { keys: !typing, ..FULL }).or(action);
|
action = player(&mut pres, ui, ui.max_rect(), now, Chrome { keys: !typing, ..FULL }).or(action);
|
||||||
});
|
});
|
||||||
|
// Keys the prototype left alone: C shows the comments, F fills the screen.
|
||||||
|
if !typing {
|
||||||
|
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::C)) {
|
||||||
|
action = Some(BarAction::Comments);
|
||||||
|
}
|
||||||
|
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::F)) {
|
||||||
|
let full = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(!full));
|
||||||
|
}
|
||||||
|
}
|
||||||
match action {
|
match action {
|
||||||
Some(BarAction::Comments) => pres.view.comments = !pres.view.comments,
|
Some(BarAction::Comments) => pres.view.comments = !pres.view.comments,
|
||||||
Some(BarAction::Inspect) => pres.view.inspect = !pres.view.inspect,
|
Some(BarAction::Inspect) => pres.view.inspect = !pres.view.inspect,
|
||||||
|
|
@ -2440,14 +2450,21 @@ fn input(p: &mut Presenter, ctx: &egui::Context, l: &Layout, over_area: bool, ke
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Arrow keys step through top-level frames when nothing else uses them.
|
// The arrows (and Space or N forward) step through top-level frames when nothing else
|
||||||
if matches!(key, egui::Key::ArrowLeft | egui::Key::ArrowRight)
|
// uses them.
|
||||||
|
let forward = match key {
|
||||||
|
egui::Key::ArrowRight => Some(true),
|
||||||
|
egui::Key::Space | egui::Key::N if mods.is_none() => Some(true),
|
||||||
|
egui::Key::ArrowLeft => Some(false),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(forward) = forward
|
||||||
&& p.overlays.is_empty()
|
&& p.overlays.is_empty()
|
||||||
&& let Some(base) = p.doc.as_ref()
|
&& let Some(base) = p.doc.as_ref()
|
||||||
{
|
{
|
||||||
let frames = base.top_frames(base.page_of(p.current).unwrap_or(p.current));
|
let frames = base.top_frames(base.page_of(p.current).unwrap_or(p.current));
|
||||||
if let Some(i) = frames.iter().position(|f| *f == p.current) {
|
if let Some(i) = frames.iter().position(|f| *f == p.current) {
|
||||||
let next = if *key == egui::Key::ArrowRight { frames.get(i + 1) } else { i.checked_sub(1).and_then(|j| frames.get(j)) };
|
let next = if forward { frames.get(i + 1) } else { i.checked_sub(1).and_then(|j| frames.get(j)) };
|
||||||
if let Some(n) = next.copied() {
|
if let Some(n) = next.copied() {
|
||||||
p.go(n, Transition::Instant, 0.0, Easing::Linear, now, true, false);
|
p.go(n, Transition::Instant, 0.0, Easing::Linear, now, true, false);
|
||||||
consume_key(ctx, *key);
|
consume_key(ctx, *key);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
//! The documented shortcuts the keymap table can't check by lookup alone, driven through egui
|
||||||
|
//! headless: arrows pan with nothing selected, the keyboard box selection key and Cmd/Ctrl+arrows
|
||||||
|
//! drawing it out, list indentation while editing text, symbols another keyboard layout types with
|
||||||
|
//! Shift, and the presentation keys (Space and N forward, C comments, F full screen).
|
||||||
|
|
||||||
|
use egui::{Event, Key, Modifiers, ViewportCommand, ViewportId};
|
||||||
|
use figcraft_doc::NodeId;
|
||||||
|
use figcraft_engine::Session;
|
||||||
|
use figcraft_engine::keymap::Platform;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::layers_drag_tests::Ui;
|
||||||
|
|
||||||
|
fn id(v: serde_json::Value) -> NodeId {
|
||||||
|
NodeId(v["id"].as_u64().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press_with(u: &mut Ui, key: Key, physical_key: Option<Key>, modifiers: Modifiers) -> egui::FullOutput {
|
||||||
|
let ev = |pressed| Event::Key { key, physical_key, pressed, repeat: false, modifiers };
|
||||||
|
u.frame_with(vec![ev(true), ev(false)], modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press(u: &mut Ui, key: Key, modifiers: Modifiers) -> egui::FullOutput {
|
||||||
|
press_with(u, key, None, modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cmd on macOS, Ctrl elsewhere, as egui reports it.
|
||||||
|
fn cmd() -> Modifiers {
|
||||||
|
if crate::shortcuts::platform() == Platform::Mac { Modifiers::MAC_CMD | Modifiers::COMMAND } else { Modifiers::CTRL | Modifiers::COMMAND }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(u: &Ui) -> (f64, f64, f64) {
|
||||||
|
let v = &u.app.session.ds().unwrap().view;
|
||||||
|
(v.cx, v.cy, v.zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrows_pan_the_view_when_nothing_is_selected() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
let r = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 40, "height": 40})).unwrap());
|
||||||
|
let mut u = Ui::new(s);
|
||||||
|
u.app.run("select.set", json!({"ids": []}));
|
||||||
|
u.app.run("view.set", json!({"zoom": 2.0, "cx": 0, "cy": 0}));
|
||||||
|
press(&mut u, Key::ArrowRight, Modifiers::NONE);
|
||||||
|
assert_eq!(view(&u), (10.0, 0.0, 2.0), "20 screen pixels at 200%");
|
||||||
|
press(&mut u, Key::ArrowUp, Modifiers::SHIFT);
|
||||||
|
assert_eq!(view(&u), (10.0, -100.0, 2.0), "Shift: ten times as far");
|
||||||
|
// With a selection the arrows nudge it and the view stays.
|
||||||
|
u.app.run("select.set", json!({"ids": [r]}));
|
||||||
|
press(&mut u, Key::ArrowRight, Modifiers::NONE);
|
||||||
|
assert_eq!(view(&u), (10.0, -100.0, 2.0));
|
||||||
|
assert_eq!(u.app.session.doc().unwrap().world_bounds(r).unwrap().x0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyboard_box_selection_key_and_cmd_arrows() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 40, "height": 40})).unwrap();
|
||||||
|
let mut u = Ui::new(s);
|
||||||
|
// Alt+Space on macOS, Ctrl+Space on Windows and Linux (Alt+Space is the window menu there).
|
||||||
|
let m = if crate::shortcuts::platform() == Platform::Mac { Modifiers::ALT } else { cmd() };
|
||||||
|
press(&mut u, Key::Space, m);
|
||||||
|
let b = u.app.ui.key_box.expect("the box shows");
|
||||||
|
let zoom = view(&u).2;
|
||||||
|
press(&mut u, Key::ArrowRight, cmd());
|
||||||
|
let grown = u.app.ui.key_box.unwrap();
|
||||||
|
assert_eq!((grown[0], grown[2]), (b[0], b[2] + 10.0 / zoom), "Cmd/Ctrl+arrows draw the box out");
|
||||||
|
press(&mut u, Key::ArrowRight, cmd() | Modifiers::SHIFT);
|
||||||
|
assert_eq!(u.app.ui.key_box.unwrap()[2], grown[2] + 50.0 / zoom, "with Shift farther");
|
||||||
|
press(&mut u, Key::ArrowRight, Modifiers::NONE);
|
||||||
|
assert_eq!(u.app.ui.key_box.unwrap()[0], b[0] + 10.0 / zoom, "plain arrows move it");
|
||||||
|
press(&mut u, Key::Escape, Modifiers::NONE);
|
||||||
|
assert!(u.app.ui.key_box.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmd_brackets_indent_lists_while_editing() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
let t = id(s.execute("text.create", &json!({"text": "One", "x": 0, "y": 0})).unwrap());
|
||||||
|
s.execute("select.set", &json!({"ids": [t]})).unwrap();
|
||||||
|
let mut u = Ui::new(s);
|
||||||
|
press(&mut u, Key::Enter, Modifiers::NONE);
|
||||||
|
press(&mut u, Key::Num8, cmd() | Modifiers::SHIFT);
|
||||||
|
let indent = |u: &Ui| u.app.session.doc().unwrap().node(t).unwrap().kind.text().unwrap().para_list(0).indent;
|
||||||
|
assert_eq!(indent(&u), 0);
|
||||||
|
press(&mut u, Key::CloseBracket, cmd());
|
||||||
|
assert_eq!(indent(&u), 1, "Cmd/Ctrl+] nests the item");
|
||||||
|
press(&mut u, Key::OpenBracket, cmd());
|
||||||
|
assert_eq!(indent(&u), 0, "Cmd/Ctrl+[ moves it out");
|
||||||
|
assert!(u.app.session.ds().unwrap().text_edit.is_some(), "still editing");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn symbols_typed_with_shift_on_other_layouts_count_without_it() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
let f = id(s.execute("node.create", &json!({"type": "frame", "x": 0, "y": 0, "width": 100, "height": 100})).unwrap());
|
||||||
|
let r = id(s.execute("node.create", &json!({"type": "rectangle", "parent": f, "x": 10, "y": 10, "width": 10, "height": 10})).unwrap());
|
||||||
|
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||||
|
let mut u = Ui::new(s);
|
||||||
|
// `\` typed as Shift+7 (the key's own symbol differs): Select parent, as `\` on a US keyboard.
|
||||||
|
press_with(&mut u, Key::Backslash, Some(Key::Num7), Modifiers::SHIFT);
|
||||||
|
assert_eq!(u.app.session.selection(), vec![f]);
|
||||||
|
// On the key that is `\` everywhere, Shift+\ stays another chord (unbound).
|
||||||
|
u.app.run("select.set", json!({"ids": [r]}));
|
||||||
|
press_with(&mut u, Key::Backslash, Some(Key::Backslash), Modifiers::SHIFT);
|
||||||
|
assert_eq!(u.app.session.selection(), vec![r]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn presentation_keys() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
let a = id(s.execute("node.create", &json!({"type": "frame", "name": "A", "x": 0, "y": 0, "width": 300, "height": 200})).unwrap());
|
||||||
|
let b = id(s.execute("node.create", &json!({"type": "frame", "name": "B", "x": 400, "y": 0, "width": 300, "height": 200})).unwrap());
|
||||||
|
let mut u = Ui::new(s);
|
||||||
|
u.app.run("proto.present", json!({"start": a}));
|
||||||
|
u.frame(vec![]);
|
||||||
|
let current = |u: &Ui| u.app.inspect()["presenter"]["current"].clone();
|
||||||
|
assert_eq!(current(&u), json!(a));
|
||||||
|
press(&mut u, Key::N, Modifiers::NONE);
|
||||||
|
assert_eq!(current(&u), json!(b), "N: next frame");
|
||||||
|
press(&mut u, Key::ArrowLeft, Modifiers::NONE);
|
||||||
|
assert_eq!(current(&u), json!(a));
|
||||||
|
press(&mut u, Key::Space, Modifiers::NONE);
|
||||||
|
assert_eq!(current(&u), json!(b), "Space: next frame");
|
||||||
|
let out = press(&mut u, Key::F, Modifiers::NONE);
|
||||||
|
let cmds = out.viewport_output.get(&ViewportId::ROOT).map(|v| v.commands.clone()).unwrap_or_default();
|
||||||
|
assert!(cmds.contains(&ViewportCommand::Fullscreen(true)), "F: full screen, {cmds:?}");
|
||||||
|
let comments = u.app.ui.present_view.comments;
|
||||||
|
press(&mut u, Key::C, Modifiers::NONE);
|
||||||
|
assert_eq!(u.app.ui.present_view.comments, !comments, "C: comments");
|
||||||
|
assert!(u.app.ui.present.is_some(), "still presenting");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiplayer_cursors_key_hides_and_shows_others_cursors() {
|
||||||
|
use crate::live_tests::{Harness, cursor};
|
||||||
|
use figcraft_engine::live::Event as LiveEvent;
|
||||||
|
let (mut h, _) = Harness::new();
|
||||||
|
h.frames(3);
|
||||||
|
h.joined();
|
||||||
|
let page = h.page();
|
||||||
|
h.send(vec![LiveEvent::Presence(1, cursor(page, 400.0, 300.0))]);
|
||||||
|
h.frames(2);
|
||||||
|
assert_eq!(h.app.ui.live.shown(), vec![1], "Hana's cursor shows");
|
||||||
|
let m =
|
||||||
|
if crate::shortcuts::platform() == Platform::Mac { Modifiers::ALT | Modifiers::MAC_CMD | Modifiers::COMMAND } else { Modifiers::ALT | cmd() };
|
||||||
|
let ev = |pressed| Event::Key { key: Key::Backslash, physical_key: None, pressed, repeat: false, modifiers: m };
|
||||||
|
h.frame(vec![ev(true), ev(false)]);
|
||||||
|
h.frames(1);
|
||||||
|
assert!(h.app.ui.live.shown().is_empty(), "Alt+Cmd+backslash hides it");
|
||||||
|
h.app.run("view.multiplayerCursors", json!({}));
|
||||||
|
h.frames(2);
|
||||||
|
assert_eq!(h.app.ui.live.shown(), vec![1], "and shows it again");
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,33 @@ pub fn mods(m: Modifiers) -> keymap::Mods {
|
||||||
keymap::Mods { shift: m.shift, alt: m.alt, cmd: m.command || m.mac_cmd, ctrl: m.ctrl }
|
keymap::Mods { shift: m.shift, alt: m.alt, cmd: m.command || m.mac_cmd, ctrl: m.ctrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Symbols a US keyboard types without Shift.
|
||||||
|
const UNSHIFTED_SYMBOLS: [Key; 11] = [
|
||||||
|
Key::Slash,
|
||||||
|
Key::Backslash,
|
||||||
|
Key::Minus,
|
||||||
|
Key::Equals,
|
||||||
|
Key::Period,
|
||||||
|
Key::Comma,
|
||||||
|
Key::Quote,
|
||||||
|
Key::Semicolon,
|
||||||
|
Key::OpenBracket,
|
||||||
|
Key::CloseBracket,
|
||||||
|
Key::Backtick,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The binding a key fires in `context`. Shortcuts follow the character typed, named as on a US
|
||||||
|
/// keyboard: a symbol another layout types with Shift (`/` is Shift+7 on a German keyboard, where
|
||||||
|
/// the key's position differs from the symbol) counts without the Shift. Characters egui has no key
|
||||||
|
/// for (`ü`, `ж`) already arrive as the key at that position.
|
||||||
|
pub fn lookup(context: keymap::Context, key: Key, physical: Option<Key>, m: Modifiers) -> Option<&'static keymap::Binding> {
|
||||||
|
let mut held = mods(m);
|
||||||
|
if held.shift && physical.is_some_and(|p| p != key) && UNSHIFTED_SYMBOLS.contains(&key) {
|
||||||
|
held.shift = false;
|
||||||
|
}
|
||||||
|
keymap::lookup(platform(), context, key.name(), held)
|
||||||
|
}
|
||||||
|
|
||||||
/// The key a menu shows for a command.
|
/// The key a menu shows for a command.
|
||||||
pub fn shortcut(id: &str) -> Option<&'static str> {
|
pub fn shortcut(id: &str) -> Option<&'static str> {
|
||||||
keymap::primary(id, platform())
|
keymap::primary(id, platform())
|
||||||
|
|
@ -94,7 +121,9 @@ pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool) {
|
||||||
Event::Copy => crate::menus::invoke(app, "edit.copy"),
|
Event::Copy => crate::menus::invoke(app, "edit.copy"),
|
||||||
Event::Cut => crate::menus::invoke(app, "edit.cut"),
|
Event::Cut => crate::menus::invoke(app, "edit.cut"),
|
||||||
Event::Paste(text) => crate::menus::paste(app, Some(text)),
|
Event::Paste(text) => crate::menus::paste(app, Some(text)),
|
||||||
Event::Key { key, pressed: true, modifiers, repeat, .. } if key_event(app, *key, *modifiers, *repeat, now, ctx) => {
|
Event::Key { key, physical_key, pressed: true, modifiers, repeat }
|
||||||
|
if key_event(app, *key, *physical_key, *modifiers, *repeat, now, ctx) =>
|
||||||
|
{
|
||||||
// handled
|
// handled
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -120,7 +149,7 @@ pub fn run_binding(app: &mut FigApp, id: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_event(app: &mut FigApp, key: Key, m: Modifiers, repeat: bool, now: f64, ctx: &Context) -> bool {
|
fn key_event(app: &mut FigApp, key: Key, physical: Option<Key>, m: Modifiers, repeat: bool, now: f64, ctx: &Context) -> bool {
|
||||||
if key_box(app, key, m) {
|
if key_box(app, key, m) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +167,7 @@ fn key_event(app: &mut FigApp, key: Key, m: Modifiers, repeat: bool, now: f64, c
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(b) = keymap::lookup(platform(), keymap::Context::Canvas, key.name(), mods(m)) {
|
if let Some(b) = lookup(keymap::Context::Canvas, key, physical, m) {
|
||||||
// Copy/cut/paste arrive as clipboard events on most platforms.
|
// Copy/cut/paste arrive as clipboard events on most platforms.
|
||||||
if matches!(b.id, "edit.copy" | "edit.cut" | "edit.paste") {
|
if matches!(b.id, "edit.copy" | "edit.cut" | "edit.paste") {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -152,7 +181,7 @@ fn key_event(app: &mut FigApp, key: Key, m: Modifiers, repeat: bool, now: f64, c
|
||||||
if app.ui.layers_focus && !m.command && !m.alt && crate::layers::key_nav(app, key, m.shift) {
|
if app.ui.layers_focus && !m.command && !m.alt && crate::layers::key_nav(app, key, m.shift) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Arrow nudges.
|
// Arrow nudges; with nothing selected the arrows pan the view.
|
||||||
let (dx, dy) = match key {
|
let (dx, dy) = match key {
|
||||||
Key::ArrowLeft => (-1.0, 0.0),
|
Key::ArrowLeft => (-1.0, 0.0),
|
||||||
Key::ArrowRight => (1.0, 0.0),
|
Key::ArrowRight => (1.0, 0.0),
|
||||||
|
|
@ -161,13 +190,13 @@ fn key_event(app: &mut FigApp, key: Key, m: Modifiers, repeat: bool, now: f64, c
|
||||||
_ => (0.0, 0.0),
|
_ => (0.0, 0.0),
|
||||||
};
|
};
|
||||||
if (dx, dy) != (0.0, 0.0) && !m.alt {
|
if (dx, dy) != (0.0, 0.0) && !m.alt {
|
||||||
if !app.session.selection().is_empty() {
|
if app.session.selection().is_empty() {
|
||||||
if m.command {
|
app.run("view.pan", json!({"dx": dx, "dy": dy, "big": m.shift}));
|
||||||
// Cmd/Ctrl+arrows resize: right/down grow, left/up shrink.
|
} else if m.command {
|
||||||
app.run("transform.nudgeSize", json!({"dw": dx, "dh": dy, "big": m.shift}));
|
// Cmd/Ctrl+arrows resize: right/down grow, left/up shrink.
|
||||||
} else {
|
app.run("transform.nudgeSize", json!({"dw": dx, "dh": dy, "big": m.shift}));
|
||||||
app.run("transform.nudge", json!({"dx": dx, "dy": dy, "big": m.shift}));
|
} else {
|
||||||
}
|
app.run("transform.nudge", json!({"dx": dx, "dy": dy, "big": m.shift}));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -182,12 +211,14 @@ fn key_event(app: &mut FigApp, key: Key, m: Modifiers, repeat: bool, now: f64, c
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keys while a selection box drawn from the keyboard shows (`select.area`, F8): the arrows move
|
/// Keys while a selection box drawn from the keyboard shows (`select.area`, Alt+Space or Ctrl+Space,
|
||||||
/// it (with Shift they move its far corner, sizing it) by 10 screen pixels, Enter selects what it
|
/// F8): the arrows move it, Cmd/Ctrl+arrows or Shift+arrows move its far corner, sizing it (Shift
|
||||||
/// covers like a marquee (Shift toggles, Cmd/Ctrl takes nested layers), Escape drops it.
|
/// with Cmd/Ctrl: five times as far), by 10 screen pixels; Enter selects what it covers like a
|
||||||
|
/// marquee (Shift toggles, Cmd/Ctrl takes nested layers), Escape drops it.
|
||||||
fn key_box(app: &mut FigApp, key: Key, m: Modifiers) -> bool {
|
fn key_box(app: &mut FigApp, key: Key, m: Modifiers) -> bool {
|
||||||
let Some(mut b) = app.ui.key_box else { return false };
|
let Some(mut b) = app.ui.key_box else { return false };
|
||||||
let step = 10.0 / app.session.ds().map_or(1.0, |d| d.view.zoom.max(1e-6));
|
let size = m.shift || m.command;
|
||||||
|
let step = if m.shift && m.command { 50.0 } else { 10.0 } / app.session.ds().map_or(1.0, |d| d.view.zoom.max(1e-6));
|
||||||
let (dx, dy) = match key {
|
let (dx, dy) = match key {
|
||||||
Key::ArrowLeft => (-step, 0.0),
|
Key::ArrowLeft => (-step, 0.0),
|
||||||
Key::ArrowRight => (step, 0.0),
|
Key::ArrowRight => (step, 0.0),
|
||||||
|
|
@ -196,7 +227,7 @@ fn key_box(app: &mut FigApp, key: Key, m: Modifiers) -> bool {
|
||||||
_ => (0.0, 0.0),
|
_ => (0.0, 0.0),
|
||||||
};
|
};
|
||||||
if (dx, dy) != (0.0, 0.0) {
|
if (dx, dy) != (0.0, 0.0) {
|
||||||
if !m.shift {
|
if !size {
|
||||||
b[0] += dx;
|
b[0] += dx;
|
||||||
b[1] += dy;
|
b[1] += dy;
|
||||||
}
|
}
|
||||||
|
|
@ -328,8 +359,8 @@ fn text_keys(app: &mut FigApp, ctx: &Context, events: &[Event]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Key { key, pressed: true, modifiers: m, .. } => {
|
Event::Key { key, physical_key, pressed: true, modifiers: m, .. } => {
|
||||||
if (m.command || m.alt || m.ctrl) && text_shortcut(app, *key, *m) {
|
if (m.command || m.alt || m.ctrl) && text_shortcut(app, *key, *physical_key, *m) {
|
||||||
skip_text = true;
|
skip_text = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -382,8 +413,8 @@ fn text_keys(app: &mut FigApp, ctx: &Context, events: &[Event]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shortcuts live while editing text (Bold, lists, alignment, size/weight/spacing steps, Save…).
|
/// Shortcuts live while editing text (Bold, lists, alignment, size/weight/spacing steps, Save…).
|
||||||
fn text_shortcut(app: &mut FigApp, key: Key, m: Modifiers) -> bool {
|
fn text_shortcut(app: &mut FigApp, key: Key, physical: Option<Key>, m: Modifiers) -> bool {
|
||||||
match keymap::lookup(platform(), keymap::Context::Text, key.name(), mods(m)) {
|
match lookup(keymap::Context::Text, key, physical, m) {
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
crate::menus::invoke(app, b.id);
|
crate::menus::invoke(app, b.id);
|
||||||
true
|
true
|
||||||
|
|
|
||||||
12
docs/gaps.md
12
docs/gaps.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Where FigCraft falls short of Figma
|
# Where FigCraft falls short of Figma
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
|
||||||
|
|
||||||
Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from
|
Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from
|
||||||
doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The
|
doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The
|
||||||
|
|
@ -89,13 +89,16 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||||
craftrules `standards/test-corpora.md`).
|
craftrules `standards/test-corpora.md`).
|
||||||
|
|
||||||
### 4. Interaction fidelity not compared tool by tool
|
### 4. Interaction fidelity not compared tool by tool
|
||||||
- **Missing:** a side-by-side pass of every tool and panel: cursors, modifier keys while dragging
|
- **Missing:** a side-by-side pass of every tool and panel: cursors, modifier keys while dragging,
|
||||||
(e.g. Ctrl to ignore snapping on macOS), hover states, popovers, menus, snapping thresholds.
|
hover states, popovers, menus, snapping thresholds.
|
||||||
- **Landed (2026-10-11):** behaviour suites written from the public documentation for auto layout,
|
- **Landed (2026-10-11):** behaviour suites written from the public documentation for auto layout,
|
||||||
selection and transform, text, and canvas navigation with rulers, guides, layout grids, outline
|
selection and transform, text, and canvas navigation with rulers, guides, layout grids, outline
|
||||||
mode and measuring, then (2026-10-12) comments and version history, and the layers and Assets
|
mode and measuring, then (2026-10-12) comments and version history, and the layers and Assets
|
||||||
panels (`crates/engine/src/*_behaviour_tests.rs`, `crates/ui-egui/src/layers_behaviour_tests.rs`),
|
panels (`crates/engine/src/*_behaviour_tests.rs`, `crates/ui-egui/src/layers_behaviour_tests.rs`),
|
||||||
each with its fixes.
|
each with its fixes; then keyboard shortcuts per platform (`shortcut_parity_tests.rs`: 129
|
||||||
|
documented rows, 126 bound, 1 moved off a documented conflict, 2 missing, 48 undocumented
|
||||||
|
bindings pinned), with 16 rows newly bound, Control suspending snapping on macOS and shifted
|
||||||
|
symbols on other keyboard layouts.
|
||||||
- **Evidence:** ROADMAP item 7 ("Left: tool by tool, panel by panel against the incumbent").
|
- **Evidence:** ROADMAP item 7 ("Left: tool by tool, panel by panel against the incumbent").
|
||||||
- **Impact:** Figma users notice the feel within minutes; it decides whether they stay.
|
- **Impact:** Figma users notice the feel within minutes; it decides whether they stay.
|
||||||
- **Estimate:** 40–60 h. Detail in [`ui-parity.md`](ui-parity.md).
|
- **Estimate:** 40–60 h. Detail in [`ui-parity.md`](ui-parity.md).
|
||||||
|
|
@ -352,6 +355,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-12 | minor | Gap 4: keyboard shortcut parity suite per platform (129 documented rows: 126 bound, 1 moved, 2 missing; 48 undocumented bindings pinned) added to the landed list; estimate and ranking unchanged |
|
||||||
| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Gap 5 31–54 h → 29–51 h, rank unchanged |
|
| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Gap 5 31–54 h → 29–51 h, rank unchanged |
|
||||||
| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Gap 5 32–55 h → 31–54 h, rank unchanged |
|
| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Gap 5 32–55 h → 31–54 h, rank unchanged |
|
||||||
| 2026-10-12 | minor | Gap 15: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes and `proto.evaluate` landed; gap 15 now media plus controllers off Windows, 3–6 h → 2–5 h; ranking unchanged |
|
| 2026-10-12 | minor | Gap 15: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes and `proto.evaluate` landed; gap 15 now media plus controllers off Windows, 3–6 h → 2–5 h; ranking unchanged |
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ Or in a project's `.mcp.json`:
|
||||||
| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. |
|
| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. |
|
||||||
| `undo` / `redo` | Step through history. |
|
| `undo` / `redo` | Step through history. |
|
||||||
| `pointer {kind, x, y, to?, steps?, tool?, shift, alt, cmd, pressure?, pressureTo?}` | Mouse gestures in page coordinates with the current tool: `click`, `double`, `drag`, `down`, `move`, `up`, `hover`. A dragged layer drops into the frame or auto layout slot under the pointer, except one inside a group, a boolean group or an instance (outside its slots), which only moves. `pressure` (0–1) is pen pressure; a drag goes from `pressure` to `pressureTo` (see Brushes and pen pressure). |
|
| `pointer {kind, x, y, to?, steps?, tool?, shift, alt, cmd, pressure?, pressureTo?}` | Mouse gestures in page coordinates with the current tool: `click`, `double`, `drag`, `down`, `move`, `up`, `hover`. A dragged layer drops into the frame or auto layout slot under the pointer, except one inside a group, a boolean group or an instance (outside its slots), which only moves. `pressure` (0–1) is pen pressure; a drag goes from `pressure` to `pressureTo` (see Brushes and pen pressure). |
|
||||||
| `key {key, shift, alt, cmd}` | A shortcut or key as if typed on the canvas (every default shortcut, tool keys, Escape, Delete, arrows, number keys for opacity; while editing text, the text shortcuts and then text editing keys). Keys are names like `A`, `0`, `Enter`, `ArrowLeft`, `=`, `/`, or egui's (`Plus`, `Questionmark`); `cmd` is Cmd on macOS and Ctrl elsewhere. |
|
| `key {key, shift, alt, cmd}` | A shortcut or key as if typed on the canvas (every default shortcut, tool keys, Escape, Delete, arrows, number keys for opacity, arrows that pan the view when nothing is selected; while editing text, the text shortcuts and then text editing keys). Keys are names like `A`, `0`, `Enter`, `ArrowLeft`, `=`, `/`, or egui's (`Plus`, `Questionmark`); `cmd` is Cmd on macOS and Ctrl elsewhere. |
|
||||||
| `type_text {text, id?}` | Type into the text being edited, or replace the contents of text layer `id`. |
|
| `type_text {text, id?}` | Type into the text being edited, or replace the contents of text layer `id`. |
|
||||||
| `inspect_ui {}` | Tool, view and status (and panels, timings and the app's `windows` when attached). |
|
| `inspect_ui {}` | Tool, view and status (and panels, timings and the app's `windows` when attached). |
|
||||||
|
|
||||||
|
|
@ -239,6 +239,11 @@ on; `key` resolves keys the way the canvas does. Commands behind keys:
|
||||||
- `view.designPrototype` (`Shift+E`), `view.devMode` (`Shift+D`) and `view.leftSidebar`
|
- `view.designPrototype` (`Shift+E`), `view.devMode` (`Shift+D`) and `view.leftSidebar`
|
||||||
(`Shift+Cmd+\`) switch the right panel's tab or hide the left sidebar in the app (no-ops headless).
|
(`Shift+Cmd+\`) switch the right panel's tab or hide the left sidebar in the app (no-ops headless).
|
||||||
- `doc.nextTab` / `doc.previousTab` (`Ctrl+Tab` / `Ctrl+Shift+Tab`): the next or previous open file.
|
- `doc.nextTab` / `doc.previousTab` (`Ctrl+Tab` / `Ctrl+Shift+Tab`): the next or previous open file.
|
||||||
|
- `view.pan {dx, dy, big?}`: the arrow keys with nothing selected (`key ArrowRight` does the same)
|
||||||
|
pan the view by 20 screen pixels a step, 200 with `big` (Shift).
|
||||||
|
- `view.multiplayerCursors {on?}` (`Alt+Cmd+\`): show or hide others' cursors in a live session.
|
||||||
|
- `text.listIndent` / `text.listOutdent` (`Cmd+]` / `Cmd+[` while editing text): the selected list
|
||||||
|
items a level deeper or out, like Tab and Shift+Tab.
|
||||||
- `view.nextRegion` / `view.previousRegion` (`F6` / `Shift+F6`, live while editing text too): move
|
- `view.nextRegion` / `view.previousRegion` (`F6` / `Shift+F6`, live while editing text too): move
|
||||||
the keyboard between the app's regions (toolbar, layers, canvas, properties). In the app, Tab and
|
the keyboard between the app's regions (toolbar, layers, canvas, properties). In the app, Tab and
|
||||||
Shift+Tab then move between the region's controls, Enter or Space activate one, the arrows move
|
Shift+Tab then move between the region's controls, Enter or Space activate one, the arrows move
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Parity checklist
|
# Parity checklist
|
||||||
|
|
||||||
> **Generated:** 2026-10-11 by `cargo xtask parity`. Do not edit by hand.
|
> **Generated:** 2026-10-12 by `cargo xtask parity`. Do not edit by hand.
|
||||||
|
|
||||||
FigCraft commands registered for the incumbent's main menu and toolbar (499 commands in the engine).
|
FigCraft commands registered for the incumbent's main menu and toolbar (528 commands in the engine).
|
||||||
|
|
||||||
**206 / 206 menu items (100.0%)**
|
**207 / 207 menu items (100.0%)**
|
||||||
|
|
||||||
Every catalog item has a command.
|
Every catalog item has a command.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# FigCraft parity with Figma
|
# FigCraft parity with Figma
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
|
||||||
|
|
||||||
The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md)
|
The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md)
|
||||||
summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is
|
summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is
|
||||||
|
|
@ -15,7 +15,7 @@ summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu c
|
||||||
|
|
||||||
| Number | Value | Kind |
|
| Number | Value | Kind |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Menu/command breadth | **100%** (204 / 204 catalogued menu items have a live command) | **measured** (`cargo xtask parity`, 2026-10-11) |
|
| Menu/command breadth | **100%** (207 / 207 catalogued menu items have a live command) | **measured** (`cargo xtask parity`, 2026-10-12) |
|
||||||
| Feature breadth (does each Figma Design feature exist, weighted) | **~84%** (core editor areas 1–16: ~94%) | estimated from the area table below |
|
| Feature breadth (does each Figma Design feature exist, weighted) | **~84%** (core editor areas 1–16: ~94%) | estimated from the area table below |
|
||||||
| Ready for real work | **~53%** | estimated (dimension table below: written weights × written per-dimension scores) |
|
| Ready for real work | **~53%** | estimated (dimension table below: written weights × written per-dimension scores) |
|
||||||
| Essentials user | **~57%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.92 × 0.88) |
|
| Essentials user | **~57%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.92 × 0.88) |
|
||||||
|
|
@ -447,11 +447,17 @@ Hours in this narrative are the builders' estimates; the tables above supersede
|
||||||
(Cmd/Ctrl+K: link while typing, Actions on the canvas), Cmd/Ctrl+/ for Actions, `+`/`-` zoom,
|
(Cmd/Ctrl+K: link while typing, Actions on the canvas), Cmd/Ctrl+/ for Actions, `+`/`-` zoom,
|
||||||
Shift+0/1/2 from any layout, Cmd/Ctrl+0, hold Z for the zoom tool (Alt zooms out, drag an area),
|
Shift+0/1/2 from any layout, Cmd/Ctrl+0, hold Z for the zoom tool (Alt zooms out, drag an area),
|
||||||
two quick digits for an exact opacity, boolean keys (Alt+Shift+U/S/I/E), remove fill/stroke
|
two quick digits for an exact opacity, boolean keys (Alt+Shift+U/S/I/E), remove fill/stroke
|
||||||
(Alt+/, Shift+/), outlines on Shift+Cmd+O, pixel grid on Cmd+', Shift+E / Shift+D and the other panel keys, Ctrl+Tab between files, B for the paint bucket in vector edit mode; the Keyboard
|
(Alt+/, Shift+/), outlines on Shift+Cmd+O, pixel grid on Shift+' (and Cmd+'), Shift+E / Shift+D and the other panel keys, Ctrl+Tab between files, Shift+B (and B) for the paint bucket in vector edit mode; the Keyboard
|
||||||
shortcuts panel docks at the bottom with a tab per group and search, keycaps per platform.
|
shortcuts panel docks at the bottom with a tab per group and search, keycaps per platform.
|
||||||
|
A shortcut parity suite (`shortcut_parity_tests.rs`) checks every documented shortcut on both
|
||||||
|
platforms (129 rows: 126 bound, 1 moved off a documented conflict, 2 missing: moving the
|
||||||
|
rotation origin and placing an object with Enter from the toolbar) and pins the 48 undocumented
|
||||||
|
bindings; others' cursors (`Alt+Cmd+\`), list levels while editing (Cmd/Ctrl+] and [), the
|
||||||
|
keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected,
|
||||||
|
Control to suspend snapping on macOS and Space/N/C/F while presenting landed with it
|
||||||
|
([ui-parity](ui-parity.md#keyboard-shortcuts)).
|
||||||
*Left:* tool by tool, panel by panel against the incumbent (cursors, modifier keys while
|
*Left:* tool by tool, panel by panel against the incumbent (cursors, modifier keys while
|
||||||
dragging such as Ctrl to ignore snapping on macOS, hover states, menus, popovers); multiplayer
|
dragging, hover states, menus, popovers); per-keyboard-layout shortcut tables. 40–60 h.
|
||||||
cursor keys wait for multiplayer. 40–60 h.
|
|
||||||
8. **Collaboration (local):** comments landed: pins pinned to frames and layers that move with them
|
8. **Collaboration (local):** comments landed: pins pinned to frames and layers that move with them
|
||||||
(to other pages too) and stay where they were when the layer is deleted, your pins dragged onto
|
(to other pages too) and stay where they were when the layer is deleted, your pins dragged onto
|
||||||
another frame, crowded pins clustered when zoomed out (a click zooms them apart), Shift+C to show
|
another frame, crowded pins clustered when zoomed out (a click zooms them apart), Shift+C to show
|
||||||
|
|
@ -619,6 +625,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
|
||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding |
|
||||||
| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h |
|
| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h |
|
||||||
| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h |
|
| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h |
|
||||||
| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h |
|
| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h |
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# UI and interaction parity
|
# UI and interaction parity
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7)
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7)
|
||||||
|
|
||||||
Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys,
|
Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys,
|
||||||
drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status:
|
drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status:
|
||||||
|
|
@ -22,7 +22,7 @@ interaction feel not yet compared.
|
||||||
| Actions palette (Cmd+K, Cmd+/) | done | commands; no assets search parity |
|
| Actions palette (Cmd+K, Cmd+/) | done | commands; no assets search parity |
|
||||||
| Context menus | done | canvas and layers; Shift+F10 opens them from the keyboard (at the focused layer row or below the canvas selection) and the menu takes the keyboard; the Menu key isn't delivered by the windowing layer (egui 0.36 has no key for it) |
|
| Context menus | done | canvas and layers; Shift+F10 opens them from the keyboard (at the focused layer row or below the canvas selection) and the menu takes the keyboard; the Menu key isn't delivered by the windowing layer (egui 0.36 has no key for it) |
|
||||||
| Main menu from the logo | done | generated from `catalog.rs`; popups sized to content |
|
| Main menu from the logo | done | generated from `catalog.rs`; popups sized to content |
|
||||||
| Keyboard shortcuts panel | done | docked, tabbed by group, searchable, per-platform keycaps |
|
| Keyboard shortcuts panel | done | docked, tabbed by group like the documentation, searchable, per-platform keycaps; held keys, gestures and presentation keys listed beside the bindings |
|
||||||
| Several windows | partial | File ▸ New window, Open in new window on a tab, a tab dragged off the strip; screen-reader tree in the main window only; positions not restored |
|
| Several windows | partial | File ▸ New window, Open in new window on a tab, a tab dragged off the strip; screen-reader tree in the main window only; positions not restored |
|
||||||
| Multiplayer avatars, Share button | missing | waits for multiplayer |
|
| Multiplayer avatars, Share button | missing | waits for multiplayer |
|
||||||
| Blend mode menus: the layer's (Design panel) and each paint's (colour picker) | done | Pass through and Normal are distinct (Normal isolates a group's or frame's children); paints have no pass-through; plus darker drawn exactly on the canvas |
|
| Blend mode menus: the layer's (Design panel) and each paint's (colour picker) | done | Pass through and Normal are distinct (Normal isolates a group's or frame's children); paints have no pass-through; plus darker drawn exactly on the canvas |
|
||||||
|
|
@ -56,13 +56,13 @@ interaction feel not yet compared.
|
||||||
| Scale tool (K): handles about an anchor, live factor, panel | done | |
|
| Scale tool (K): handles about an anchor, live factor, panel | done | |
|
||||||
| Nudge 1 / Shift 10 (preference), Cmd+arrows resize | done | arrows reorder auto layout children |
|
| Nudge 1 / Shift 10 (preference), Cmd+arrows resize | done | arrows reorder auto layout children |
|
||||||
| Smart guides: edges, centres, equal spacing (between two layers or continuing a row's gap, labelled), spacing labels, parent, layout grid, ruler guides | done | red, `snap.rs`; thresholds not yet compared with Figma's |
|
| Smart guides: edges, centres, equal spacing (between two layers or continuing a row's gap, labelled), spacing labels, parent, layout grid, ruler guides | done | red, `snap.rs`; thresholds not yet compared with Figma's |
|
||||||
| Ctrl to suspend snapping while dragging (macOS) | missing | |
|
| Ctrl to suspend snapping while dragging (macOS) | done | Control on macOS (Cmd keeps working), Ctrl elsewhere; moving, resizing and drawing |
|
||||||
| Measure (Alt hover, Cmd+Alt nested, Shift+Alt padding/gaps) | done | labels never overlap |
|
| Measure (Alt hover, Cmd+Alt nested, Shift+Alt padding/gaps) | done | labels never overlap |
|
||||||
| Pixel grid snapping, pixel preview, 2x preview | done | moves, resizes and new shapes land on whole pixels; the pixel grid shows from 400% (`view_behaviour_tests`) |
|
| Pixel grid snapping, pixel preview, 2x preview | done | moves, resizes and new shapes land on whole pixels; the pixel grid shows from 400% (`view_behaviour_tests`) |
|
||||||
| Numeric fields: arithmetic, scrubbing by dragging the label, mixed values | partial | check scrub speed and Shift/Alt steps |
|
| Numeric fields: arithmetic, scrubbing by dragging the label, mixed values | partial | check scrub speed and Shift/Alt steps |
|
||||||
| Transform box at small sizes | done | a press inside a layer smaller than its handles moves it, the nearest handle wins, handles answer just outside and are hidden under 16 px on screen; cursors follow each handle and the box's rotation |
|
| Transform box at small sizes | done | a press inside a layer smaller than its handles moves it, the nearest handle wins, handles answer just outside and are hidden under 16 px on screen; cursors follow each handle and the box's rotation |
|
||||||
| Locked layers | done | selectable from the layers panel; no handles, not dragged with the rest of a selection |
|
| Locked layers | done | selectable from the layers panel; no handles, not dragged with the rest of a selection |
|
||||||
| Keyboard box selection | done | F8 (`select.area`): a box around the selection or in the middle of the view; arrows move it, Shift+arrows size it, Enter selects like a marquee (Shift+Enter toggles, Cmd/Ctrl+Enter nested); our addition for keyboard-only users |
|
| Keyboard box selection | done | Alt+Space on macOS, Ctrl+Space on Windows and Linux, or F8 (`select.area`): a box around the selection or in the middle of the view; arrows move it, Cmd/Ctrl+arrows or Shift+arrows size it (Shift with Cmd/Ctrl: farther), Enter selects like a marquee (Shift+Enter toggles, Cmd/Ctrl+Enter nested), Escape closes |
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
|
|
@ -103,6 +103,48 @@ interaction feel not yet compared.
|
||||||
| Cursors per tool and modifier | partial | not compared |
|
| Cursors per tool and modifier | partial | not compared |
|
||||||
| Multiplayer cursors, cursor chat, spotlight | partial | in live sessions: named cursors, chat bubbles that fade, `/` to chat, spotlight from your avatar's menu with a banner; no audio or reactions |
|
| Multiplayer cursors, cursor chat, spotlight | partial | in live sessions: named cursors, chat bubbles that fade, `/` to chat, spotlight from your avatar's menu with a banner; no audio or reactions |
|
||||||
|
|
||||||
|
## Keyboard shortcuts
|
||||||
|
|
||||||
|
One keymap (`figcraft_engine::keymap`) drives the menus, the canvas, the shortcuts panel, the
|
||||||
|
control channel and MCP `key`. `crates/engine/src/shortcut_parity_tests.rs` holds one row per
|
||||||
|
shortcut in the public documentation (the help centre's feature articles: keyboard layouts, text
|
||||||
|
properties, alignment, frames, components, prototypes, plugins, keyboard navigation…), with the
|
||||||
|
macOS and Windows/Linux keys, what does it here and a note in our words, and checks every row on
|
||||||
|
both platforms; rows the documentation is silent on pin today's keys, so every default binding is
|
||||||
|
in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a lookup can't prove.
|
||||||
|
|
||||||
|
| | Count | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Documented shortcuts (rows) | 129 | each with macOS and Windows/Linux keys |
|
||||||
|
| Bound as documented | 126 (110 before this suite) | 107 keymap bindings, 13 held keys, gestures and presentation keys, 6 vector edit tool keys |
|
||||||
|
| Bound on other keys (documented conflict) | 1 | Run last plugin: the documentation also gives Ctrl+Alt+P to pixel preview on Windows; the plugin takes Ctrl+Alt+Shift+P there |
|
||||||
|
| Missing | 2 | moving the rotation origin (Alt+R); placing an object with Enter after choosing a tool from the toolbar |
|
||||||
|
| Documented keys a platform keeps | 2 | Ctrl+Space (Windows/Linux keyboard box selection) switches some CJK input methods; F6 goes to a browser's address bar in the web build |
|
||||||
|
| Undocumented bindings pinned | 48 | of which Cmd/Ctrl+W, Cmd/Ctrl+N and Ctrl+Tab are taken by browsers (web build) and Ctrl+Alt+T / Ctrl+Alt+L by some Linux desktops |
|
||||||
|
|
||||||
|
- **Newly bound:** A for the Frame tool, Shift+' for the pixel grid (Cmd+' kept: the articles
|
||||||
|
disagree), Alt+Cmd+\ / Ctrl+Alt+\ for others' cursors (`view.multiplayerCursors`, new), Ctrl+F6
|
||||||
|
to the toolbar on Windows/Linux, Shift+Cmd+U to link text (Cmd+K kept), Cmd/Ctrl+] and [ to
|
||||||
|
nest list items while editing (`text.listIndent` / `text.listOutdent`, new), Cmd/Ctrl+Backspace
|
||||||
|
to ungroup, Alt+Space / Ctrl+Space for the keyboard box selection (F8 kept) with Cmd/Ctrl+arrows
|
||||||
|
drawing it out, Ctrl+Shift+A / Ctrl+Alt+Shift+A to suggest auto layout, Ctrl+Alt+Cmd+K /
|
||||||
|
Ctrl+Alt+Shift+K to go to the main component, the arrows panning the view when nothing is
|
||||||
|
selected (`view.pan`, new), Control suspending snapping on macOS, and Space, N, C and F while
|
||||||
|
presenting. The paint bucket's tooltip shows Shift+B (B still works).
|
||||||
|
- **Where the documentation disagrees with itself** (noted per row): pixel grid (Shift+' or
|
||||||
|
Cmd+'), letter spacing (Alt or Cmd with < >; Cmd would be the font size keys, Alt bound),
|
||||||
|
Windows remove fill (Alt+/ or Alt+,; Alt+, is letter spacing), the Layers panel on Windows (Alt+1
|
||||||
|
or Ctrl+1), send to back / bring to front (one table shows only the bracket).
|
||||||
|
- **Platform keys:** macOS rows use Cmd and Option, Windows/Linux rows Ctrl and Alt (tested: no
|
||||||
|
macOS glyph on Windows/Linux keycaps, no "Ctrl"/"Alt" words on macOS); no default binding uses a
|
||||||
|
chord the system keeps (Cmd+H/M/Q/Tab/Space, Ctrl+Space and the screenshot keys on macOS;
|
||||||
|
Alt+Space/Tab/F4, Ctrl+Esc, Ctrl+Alt+Delete and Linux workspace and terminal-switch keys).
|
||||||
|
- **Keyboard layouts:** keys follow the character typed, named as on a US keyboard. Characters
|
||||||
|
egui has no key for (`ü`, `ж`) arrive as the key at that position (egui's fallback); a symbol
|
||||||
|
another layout types with Shift (`/` is Shift+7 on a German keyboard) now counts without the
|
||||||
|
Shift. Not done: per-layout tables like the documentation's (a German Shift+' types `"` and
|
||||||
|
reaches Shift+2).
|
||||||
|
|
||||||
## Accessibility (ours vs Figma's)
|
## Accessibility (ours vs Figma's)
|
||||||
|
|
||||||
| Item | Status | Notes |
|
| Item | Status | Notes |
|
||||||
|
|
@ -118,6 +160,7 @@ interaction feel not yet compared.
|
||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings: a table of the 129 documented shortcuts per platform (126 bound, 1 moved for a documented conflict, 2 missing, 2 kept by a platform) plus 48 pinned undocumented bindings; 16 rows newly bound (see Keyboard shortcuts); Ctrl to suspend snapping on macOS done; keyboard box selection on Alt+Space / Ctrl+Space |
|
||||||
| 2026-10-12 | minor | Multiplayer cursors, cursor chat and spotlight: missing → partial (live sessions: chat bubbles at the pointer that fade, `/` to chat, spotlight from your avatar's menu with a banner to stop or follow again) |
|
| 2026-10-12 | minor | Multiplayer cursors, cursor chat and spotlight: missing → partial (live sessions: chat bubbles at the pointer that fade, `/` to chat, spotlight from your avatar's menu with a banner to stop or follow again) |
|
||||||
| 2026-10-12 | minor | Layers and Assets panels behaviour suite and fixes: several layers dropped together land together, Collapse layers keeps the selection's rows open, dragging across eyes or padlocks sets several layers in one step, the toggles stay under the pointer, Rename layers dialog for several layers, pages dragged to reorder, dividers, "Copy of" duplicates, Assets folders by page, frame and slash names |
|
| 2026-10-12 | minor | Layers and Assets panels behaviour suite and fixes: several layers dropped together land together, Collapse layers keeps the selection's rows open, dragging across eyes or padlocks sets several layers in one step, the toggles stay under the pointer, Rename layers dialog for several layers, pages dragged to reorder, dividers, "Copy of" duplicates, Assets folders by page, frame and slash names |
|
||||||
| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes |
|
| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue