From ecd900ab375cc75b533f29021a6e60c2181c8385 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Sun, 1 Nov 2026 23:47:48 -0500 Subject: [PATCH 1/3] Control channel: ui.key arrows pan the view when nothing is selected, as the window does Co-Authored-By: Claude Fable 5.1 --- crates/ui-egui/src/control.rs | 5 ++++- crates/ui-egui/src/inspect_fields_tests.rs | 25 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index fd5a2a4e9..aa79bec78 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -996,7 +996,10 @@ fn ui_key(app: &mut FigApp, ctx: &Context, p: &Value) -> Result { "ArrowUp" => (0, -1), _ => (0, 1), }; - let (cmd, params) = if m.cmd || m.ctrl { + // With nothing selected the arrows pan the view, as the window's own key path does. + let (cmd, params) = if app.session.selection().is_empty() { + ("view.pan", json!({"dx": dx, "dy": dy, "big": m.shift})) + } else if m.cmd || m.ctrl { ("transform.nudgeSize", json!({"dw": dx, "dh": dy, "big": m.shift})) } else { ("transform.nudge", json!({"dx": dx, "dy": dy, "big": m.shift})) diff --git a/crates/ui-egui/src/inspect_fields_tests.rs b/crates/ui-egui/src/inspect_fields_tests.rs index 102e232b3..9ea3223fd 100644 --- a/crates/ui-egui/src/inspect_fields_tests.rs +++ b/crates/ui-egui/src/inspect_fields_tests.rs @@ -46,3 +46,28 @@ fn control_inspection_rejects_invalid_options_consistently() { } } } + +/// `ui.key` with an arrow and nothing selected pans the view, as the window's own key path does +/// (before, it answered that nothing was selected); with a selection it nudges (Shift: farther). +#[test] +fn control_key_arrows_pan_with_nothing_selected() { + let mut u = Ui::new(Session::new()); + let r = + call(&mut u, "engine.execute", json!({"command": "shape.rectangle", "params": {"x": 0, "y": 0, "width": 40, "height": 40}})).unwrap()["id"] + .clone(); + u.app.run("select.set", json!({"ids": []})); + u.app.run("view.set", json!({"zoom": 2.0, "cx": 0, "cy": 0})); + let view = |u: &Ui| { + let v = &u.app.session.ds().unwrap().view; + (v.cx, v.cy) + }; + call(&mut u, "ui.key", json!({"key": "ArrowRight"})).unwrap(); + assert_eq!(view(&u), (10.0, 0.0), "20 screen pixels at 200%"); + call(&mut u, "ui.key", json!({"key": "ArrowUp", "shift": true})).unwrap(); + assert_eq!(view(&u), (10.0, -100.0), "Shift: ten times as far"); + u.app.run("select.set", json!({"ids": [r]})); + call(&mut u, "ui.key", json!({"key": "ArrowRight"})).unwrap(); + assert_eq!(view(&u), (10.0, -100.0), "with a selection the view stays"); + let id = figcraft_engine::params::id_of(&r).unwrap(); + assert_eq!(u.app.session.doc().unwrap().world_bounds(id).unwrap().x0, 1.0, "the selection moved instead"); +} From 5851d9b9780887a544a3bc448c8c3530740086d9 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Tue, 3 Nov 2026 00:54:34 -0500 Subject: [PATCH 2/3] Control channel arrows: test the crosshair before the pan With a shape tool and nothing selected, ui.key ArrowRight moves the placement crosshair and leaves the view. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/inspect_fields_tests.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/ui-egui/src/inspect_fields_tests.rs b/crates/ui-egui/src/inspect_fields_tests.rs index 9ea3223fd..bdd0abcd8 100644 --- a/crates/ui-egui/src/inspect_fields_tests.rs +++ b/crates/ui-egui/src/inspect_fields_tests.rs @@ -70,4 +70,12 @@ fn control_key_arrows_pan_with_nothing_selected() { assert_eq!(view(&u), (10.0, -100.0), "with a selection the view stays"); let id = figcraft_engine::params::id_of(&r).unwrap(); assert_eq!(u.app.session.doc().unwrap().world_bounds(id).unwrap().x0, 1.0, "the selection moved instead"); + // With a shape tool chosen and nothing selected the arrows move the placement crosshair + // first (#1869), and the view stays. + u.app.run("select.set", json!({"ids": []})); + u.app.run("tool.rectangle", json!({})); + let before = view(&u); + call(&mut u, "ui.key", json!({"key": "ArrowRight"})).unwrap(); + assert_eq!(view(&u), before, "the crosshair, not a pan"); + assert!(u.app.session.crosshair().is_some()); } From 45ef37fc610efaddc19207ed661e13862e7ee4fc Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 7 Nov 2026 11:07:35 +0700 Subject: [PATCH 3/3] Docs: one row per item after the merge --- docs/ui-parity.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 9963c9019..205f73f79 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -188,7 +188,6 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | 2026-11-02 | minor | M8: layer hierarchy backgrounds compute each visible row's selection root once, plus its immediate offscreen neighbors, rather than walking ancestors three times per row. Selection membership uses a set instead of a linear search. The scrolling regression keeps the internal seams square at viewport boundaries. Numbers unchanged. | | 2026-11-02 | minor | M8: selected layer hierarchies have a continuous background across row spacing, with rounded outer corners; nested selections stay in the same block and separate selected trees keep their own corners. Regressions cover continuous fills, the selected-row highlight above them, scrolling and the original palette's dim-text contrast. Numbers unchanged. | | 2026-11-06 | patch | M7, #2032: closing a tab whose file has unsaved changes, by its X, a middle click, File ▸ Close tab or its shortcut, only showed a toast ("save first, or close again with Alt to discard") and left the tab open, so closing seemed not to work. It now asks the unsaved-changes question the window asks (`windows::TabClose`): Don't Save closes the tab, Cancel or Escape keeps it, Save saves the file and then closes it (a file without a path, or saved as .fig, asks where with Save as first and stays open if that is cancelled); Alt+close still drops the changes at once, and `file.close`'s `dialog.unsaved` request asks too. The three toasts left the catalogs. Tests: `titlebar_tests::closing_a_tab_with_unsaved_changes_asks`, `titlebar_tests::save_in_the_tab_question_saves_then_closes`. Reordering tabs by dragging, the issue's other half, landed on 2026-10-19 (`dragging_a_tab_along_the_bar_reorders_it_without_detaching`) | -| 2026-11-02 | patch | M6, #1839: with a shape or frame tool chosen and nothing selected, the arrows show a placement crosshair in the middle of the view and move it by the nudge amounts (`tool.crosshair`), Enter places the object there (`tool.place`), Escape or a tool change takes it away; the canvas draws the crosshair and `ui.inspect` reports it (`tool.crosshair`), the Keyboard shortcuts panel lists the gesture ("Use the arrow keys to activate the crosshair cursor...", Accessibility at Figma). The board Shapes tool places on it too (its 160 px shape centred, as the tool centres a press), and the view pans by the overshoot when the crosshair is nudged past its edge. Tests: `place_crosshair_tests`, `shortcut_keys_tests::arrows_show_and_move_the_placement_crosshair_with_a_shape_tool`. | | 2026-11-05 | minor | M4, #1830: a paragraph's base direction can be forced (`TextDirection`: Auto, Ltr, Rtl on `ParaList`): Text ▸ Text direction ▸ Auto / Left to right / Right to left (`text.directionAuto` / `Ltr` / `Rtl`) and `text.direction {direction, ids?}` set the edited paragraphs while editing, else the layer's; `node.set textDirection`; the Type settings control once a right-to-left script is in the text; inspect `textDirection` / `lineDirections`; CSS `direction`; `.fig` lines' `sourceDirectionality` in and out. Layout gives each paragraph its base level (`Bidi::of` runs the algorithm per paragraph, as it starts over at each). Before, direction came from the letters only ("controls are available to override text direction per paragraph", Add right-to-left text). Tests: `text_behaviour_tests::text_direction_is_forced_per_paragraph`, `fig::text_direction_tests::a_lines_source_directionality_is_its_paragraphs_direction_both_ways`. | | 2026-11-05 | patch | M1, #2003: the Design panel's boolean operation menu leads Union, Subtract, Intersect and Exclude with original icons drawn in code (two offset squares, the result filled at reduced alpha). Numbers unchanged. | | 2026-11-05 | patch | M1, #1758: a mouse's side buttons (4 and 5) pan the canvas as the middle button does, the pan ending on release over the canvas or off it; the grab cursor shows while they're held. |