From 9404226235d2f78a9116d16e05c62c2370dd120c Mon Sep 17 00:00:00 2001 From: Rob Coenen <753704+rcoenen@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:35:06 -0400 Subject: [PATCH 1/3] M4: vector edit mode keeps the canvas still under the pointer With no points picked, the Design panel's hint ("Select points to edit their position, radius and mirroring") didn't wrap and made the canvas 49 points wider, so entering vector edit mode moved the view by about 25 points and picking a region moved it back. The jump came in the middle of the click that picked the region: with the smallest pointer movement the release landed elsewhere on the page and the region was dragged by that distance (about 47 units at 70%). Both edit mode hints now wrap. Regression test: the canvas rectangle stays the same entering edit mode, picking a region and switching to the width tool. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/lib.rs | 2 ++ crates/ui-egui/src/vector_panel.rs | 4 ++-- crates/ui-egui/src/vector_panel_tests.rs | 26 ++++++++++++++++++++++++ crates/ui-egui/src/widgets.rs | 6 ++++++ 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 crates/ui-egui/src/vector_panel_tests.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 613f18f9..5441390e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -148,6 +148,8 @@ mod text_enter_tests; #[cfg(test)] mod titlebar_tests; #[cfg(test)] +mod vector_panel_tests; +#[cfg(test)] mod vector_tail_ui_tests; #[cfg(test)] mod windows_qa_tests; diff --git a/crates/ui-egui/src/vector_panel.rs b/crates/ui-egui/src/vector_panel.rs index 13f1ac8d..5a327ecc 100644 --- a/crates/ui-egui/src/vector_panel.rs +++ b/crates/ui-egui/src/vector_panel.rs @@ -40,7 +40,7 @@ fn width_point(app: &mut FigApp, ui: &mut Ui, doc: &Document, ve: &VectorEdit) { let t = crate::theme::Tokens::get(ui.ctx()); ui.horizontal(|ui| { ui.add_space(PAD); - widgets::label(ui, tr("Drag the stroke to add a width point"), t.text_secondary); + widgets::label_wrapped(ui, tr("Drag the stroke to add a width point"), t.text_secondary); }); ui.add_space(8.0); return; @@ -87,7 +87,7 @@ pub fn points(app: &mut FigApp, ui: &mut Ui, doc: &Document, ve: &VectorEdit) { let t = crate::theme::Tokens::get(ui.ctx()); ui.horizontal(|ui| { ui.add_space(PAD); - widgets::label(ui, tr("Select points to edit their position, radius and mirroring"), t.text_secondary); + widgets::label_wrapped(ui, tr("Select points to edit their position, radius and mirroring"), t.text_secondary); }); ui.add_space(8.0); return; diff --git a/crates/ui-egui/src/vector_panel_tests.rs b/crates/ui-egui/src/vector_panel_tests.rs new file mode 100644 index 00000000..d0a5e72e --- /dev/null +++ b/crates/ui-egui/src/vector_panel_tests.rs @@ -0,0 +1,26 @@ +//! The Design panel in vector edit mode keeps its width, so the canvas doesn't move under the +//! pointer when the panel's contents change. +use serde_json::json; + +#[test] +fn the_canvas_stays_put_entering_vector_edit_mode_and_picking_a_region() { + let (mut h, _) = crate::live_tests::Harness::new(); + h.app.session.execute("file.newFromTemplate", &json!({"template": "mobile-app"})).unwrap(); + // A wide vector with a filled area: the panel shows its long hint until something is picked. + let id = { + let doc = h.app.session.doc().unwrap(); + doc.nodes().filter(|n| n.name == "Near ridge").max_by(|a, b| a.height.total_cmp(&b.height)).map(|n| n.id).unwrap() + }; + let canvas = |h: &mut crate::live_tests::Harness| { + h.frames(4); + h.app.inspect()["canvas"]["rect"].clone() + }; + h.app.session.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + let before = canvas(&mut h); + h.app.session.execute("vector.edit.begin", &json!({})).unwrap(); + assert_eq!(canvas(&mut h), before, "the hint for an empty point selection wraps instead of widening the panel"); + h.app.session.execute("vector.edit.select", &json!({"regions": [0]})).unwrap(); + assert_eq!(canvas(&mut h), before, "picking a region doesn't move the canvas either"); + h.app.session.execute("vector.edit.tool", &json!({"tool": "width"})).unwrap(); + assert_eq!(canvas(&mut h), before, "nor does the width tool's hint"); +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 508e6fc4..73f8f95f 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -934,6 +934,12 @@ pub fn label(ui: &mut Ui, text: &str, color: Color32) { ui.label(egui::RichText::new(text).font(regular(11.0)).color(color)); } +/// Text that wraps at the width it is given, so a long hint never widens its panel (a wider +/// panel would move the canvas under the pointer). +pub fn label_wrapped(ui: &mut Ui, text: &str, color: Color32) { + ui.add(egui::Label::new(egui::RichText::new(text).font(regular(11.0)).color(color)).wrap()); +} + /// Text shown in `rect` that turns into an editor on click (or when its editor has focus, e.g. /// after a double-click rename). Returns the new text on Enter / focus loss. pub fn text_field_inline(ui: &mut Ui, id: Id, value: &str, rect: Rect, font: egui::FontId) -> Option { From 3e777d2485833a2dfd3d5b05ca0aa1a5593f060a Mon Sep 17 00:00:00 2001 From: Rob Coenen <753704+rcoenen@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:30:10 -0400 Subject: [PATCH 2/3] M7: home screen test waits for the recent-file reader The first-run test settled the home screen with a fixed 12 frames, but the recent files are looked at on a thread of their own and a card shows no name until that thread has answered. On a busy CI runner it hadn't yet, so "ridgeline" was still a grey placeholder and the test failed (seen on this PR and on codex/issue-250-async-dialog). settle() now waits until every recent file has been looked at, as home_recents_tests does. Checked with a reader slowed to 200 ms: the old test fails at the same line as CI, the new one passes. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/home_tests.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/home_tests.rs b/crates/ui-egui/src/home_tests.rs index 1179b1c4..a8f06476 100644 --- a/crates/ui-egui/src/home_tests.rs +++ b/crates/ui-egui/src/home_tests.rs @@ -22,8 +22,18 @@ fn shapes(u: &mut Ui) -> Vec { flat(&u.frame_output(vec![]).shapes) } -/// Enough frames for every thumbnail in view (they render one a frame) and what follows a click. +/// Enough frames for every thumbnail in view (they render one a frame) and what follows a click, +/// after every recent file has been looked at (the reader is a thread of its own, and a card +/// shows no name until it has answered). fn settle(u: &mut Ui) -> Vec { + for _ in 0..400 { + u.frame(vec![]); + let files = u.app.inspect()["homeRecents"]["files"].as_array().cloned().unwrap_or_default(); + if files.iter().all(|f| !f["exists"].is_null()) { + break; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } for _ in 0..12 { u.frame(vec![]); } From 6291e436050449f5dc1757b36a77bb2a1ddd528f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 15 Oct 2026 19:20:32 +0700 Subject: [PATCH 3/3] #322: use widgets::note for the vector edit hints The wrapped hint label duplicated widgets::note (a wrapped secondary note inside the panel's side padding); the two hints use it, and the copy and the extra spacing after them go. --- crates/ui-egui/src/vector_panel.rs | 14 +++----------- crates/ui-egui/src/widgets.rs | 6 ------ 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/crates/ui-egui/src/vector_panel.rs b/crates/ui-egui/src/vector_panel.rs index 5a327ecc..e965da35 100644 --- a/crates/ui-egui/src/vector_panel.rs +++ b/crates/ui-egui/src/vector_panel.rs @@ -12,7 +12,7 @@ use crate::FigApp; use crate::i18n::tr; use crate::icons::Icon; use crate::right::{CAP_ART_W, cap_art, join_label, menu_item, menu_item_with, menu_popup, row}; -use crate::widgets::{self, FieldEdit, Lead, PAD}; +use crate::widgets::{self, FieldEdit, Lead}; /// Run a point command from a field: typed values are one step, scrubs one step when released. fn point_field(app: &mut FigApp, cmd: &str, edit: Option, params: impl Fn(f64) -> Value) { @@ -38,11 +38,7 @@ fn width_point(app: &mut FigApp, ui: &mut Ui, doc: &Document, ve: &VectorEdit) { let sel = ve.width_point.and_then(|i| Some((i, *stroke?.profile()?.points.get(i)?))); let Some((i, p)) = sel else { let t = crate::theme::Tokens::get(ui.ctx()); - ui.horizontal(|ui| { - ui.add_space(PAD); - widgets::label_wrapped(ui, tr("Drag the stroke to add a width point"), t.text_secondary); - }); - ui.add_space(8.0); + widgets::note(ui, tr("Drag the stroke to add a width point"), t.text_secondary); return; }; let weight = stroke.map_or(1.0, |s| s.weight); @@ -85,11 +81,7 @@ pub fn points(app: &mut FigApp, ui: &mut Ui, doc: &Document, ve: &VectorEdit) { widgets::section_header(ui, if pts.len() > 1 { tr("Points") } else { tr("Point") }, &[], false); if pts.is_empty() { let t = crate::theme::Tokens::get(ui.ctx()); - ui.horizontal(|ui| { - ui.add_space(PAD); - widgets::label_wrapped(ui, tr("Select points to edit their position, radius and mirroring"), t.text_secondary); - }); - ui.add_space(8.0); + widgets::note(ui, tr("Select points to edit their position, radius and mirroring"), t.text_secondary); return; } let col = widgets::col_width(ui.available_width()); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index fe9e29cb..db696a1e 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -949,12 +949,6 @@ pub fn label(ui: &mut Ui, text: &str, color: Color32) { ui.label(egui::RichText::new(text).font(regular(11.0)).color(color)); } -/// Text that wraps at the width it is given, so a long hint never widens its panel (a wider -/// panel would move the canvas under the pointer). -pub fn label_wrapped(ui: &mut Ui, text: &str, color: Color32) { - ui.add(egui::Label::new(egui::RichText::new(text).font(regular(11.0)).color(color)).wrap()); -} - /// Text shown in `rect` that turns into an editor on click (or when its editor has focus, e.g. /// after a double-click rename). Returns the new text on Enter / focus loss. pub fn text_field_inline(ui: &mut Ui, id: Id, value: &str, rect: Rect, font: egui::FontId) -> Option {