From 892c0659e5b03a1f2e81fb3d2eb882714dca9271 Mon Sep 17 00:00:00 2001 From: FractalEncrypt <59615752+FractalEncrypt@users.noreply.github.com> Date: Fri, 6 Nov 2026 08:48:30 -0500 Subject: [PATCH] Transform fields: Tab moves from a field to the next field, one press each MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A numeric field's label is a scrub zone laid over the label (a drag scrubs the field, a click focuses it). It was registered with `Sense::click_and_drag()`, which is focusable, so every field had a second, invisible tab stop: Tab went from X to W's label zone and only then to W. The zone is now `CLICK | DRAG`, not focusable, and the W/H Constrain Proportions link — a button between W and H in the Control bar — is no longer a tab stop either, so Tab goes from W straight to H. The preference stays reachable from Preferences › General. Test: `one_tab_moves_from_a_field_to_the_next` (Transform panel, Properties panel's Transform section and Control bar). --- crates/ui-egui/src/panels/tests_constrain.rs | 50 ++++++++++++++++++++ crates/ui-egui/src/panels/transform.rs | 7 ++- crates/ui-egui/src/scrub.rs | 5 +- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panels/tests_constrain.rs b/crates/ui-egui/src/panels/tests_constrain.rs index a90f2b55d..44ede9641 100644 --- a/crates/ui-egui/src/panels/tests_constrain.rs +++ b/crates/ui-egui/src/panels/tests_constrain.rs @@ -296,3 +296,53 @@ fn the_link_is_a_preference_agents_can_read_and_set() { run(&mut app, "object.setBounds", json!({"height": 100, "reference": 4, "proportional": true})); assert_eq!(size(&app), (200.0, 100.0)); } + +/// One Tab moves from a field to the next field: neither a field's label (its scrub zone) nor the +/// W/H link between two fields is a tab stop of its own. In the Transform panel and the Properties +/// panel the fields run X, W, Y, H (a grid, two to a row); in the Control bar X, Y, W, H. +#[test] +fn one_tab_moves_from_a_field_to_the_next() { + // From X (the selection's centre, 60 pt) the next field is W in the panels and Y in the bar. + for (name, draw, next) in [ + ("Transform panel", transform::show as Draw, "width"), + ("Properties", properties::transform_section as Draw, "width"), + ("Control bar", crate::chrome::control_bar as Draw, "y"), + ] { + let mut app = rect_app(); + tab_then_type(&mut app, draw, "60 pt", "200"); + let st = app.session.active().unwrap(); + let b = st.doc.bounds_of(&st.selection.objects, false).unwrap(); + match next { + "width" => assert_eq!(size(&app), (200.0, 50.0), "{name}: Tab went from X to W"), + _ => assert_eq!((b.width().round(), b.y0.round()), (100.0, 175.0), "{name}: Tab went from X to Y"), + } + } + // In the Control bar the W/H link sits between W and H: Tab goes from W straight to H. + let mut app = rect_app(); + tab_then_type(&mut app, crate::chrome::control_bar, "100 pt", "200"); + assert_eq!(size(&app), (100.0, 200.0), "Control bar: Tab went from W to H"); +} + +/// Type `value` into the field of what `draw` shows that shows `field` after one Tab, and press +/// Enter. The field the Tab moved to takes the value; the one pressed keeps its own. +fn tab_then_type(app: &mut VectorcraftApp, draw: Draw, field: &str, value: &str) { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut time = 0.0; + let mut frame = |app: &mut VectorcraftApp, events: Vec| { + time += 1.0; + frame_raw(&ctx, app, egui::RawInput { events, time: Some(time), ..Default::default() }, draw) + }; + frame(app, vec![]); + let shown = frame(app, vec![]); + let at = shown.iter().filter(|(s, _)| s == field).map(|(_, r)| r.center()).next().unwrap_or_else(|| panic!("no {field} field in {shown:?}")); + let button = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }; + let tab = Event::Key { key: Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }; + for events in [vec![Event::PointerMoved(at), button(at, true), button(at, false)], vec![], vec![tab], vec![]] { + frame(app, events); + } + let enter = Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }; + for events in [vec![Event::Text(value.into())], vec![enter], vec![]] { + frame(app, events); + } +} diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs index 042913ca0..caf69ec86 100644 --- a/crates/ui-egui/src/panels/transform.rs +++ b/crates/ui-egui/src/panels/transform.rs @@ -484,11 +484,14 @@ pub fn link_width(ui: &Ui) -> f32 { pub const CONSTRAIN_LINK_SIZE: f32 = 22.0; /// The link between W and H (Transform panel, Properties panel, Control bar): one toggle, the -/// `constrainProportions` preference, which the size fields pass on as `proportional`. +/// `constrainProportions` preference, which the size fields pass on as `proportional`. It is not a +/// tab stop, so Tab goes from W straight to H (it is the same preference as Preferences › General +/// › Constrain Width and Height Proportions for the keyboard). pub fn constrain_link(app: &mut VectorcraftApp, ui: &mut Ui) { let on = app.session.prefs.constrain_proportions; let tip = tl!("Constrain Width and Height Proportions"); - if widgets::icon_button(ui, if on { "link" } else { "link-2-off" }, tip, on, CONSTRAIN_LINK_SIZE).clicked() { + let (_, resp) = ui.allocate_exact_size(egui::Vec2::splat(CONSTRAIN_LINK_SIZE), egui::Sense::CLICK); + if widgets::paint_icon_button(ui, resp, if on { "link" } else { "link-2-off" }, tip, on).clicked() { set_pref(app, "constrainProportions", !on); } } diff --git a/crates/ui-egui/src/scrub.rs b/crates/ui-egui/src/scrub.rs index 70ceb5060..5a0a2ff4f 100644 --- a/crates/ui-egui/src/scrub.rs +++ b/crates/ui-egui/src/scrub.rs @@ -168,10 +168,11 @@ pub(crate) fn field(ui: &Ui, field: Id, rect: Rect, text: &str, value: Option ui.interact(label.rect, link, Sense::drag()), - None => ui.interact(label.rect, field.with("scrub-label"), Sense::click_and_drag()), + None => ui.interact(label.rect, field.with("scrub-label"), Sense::CLICK | Sense::DRAG), }; drag(ui, &resp, field, text, value, decimals, label.link.is_some()).map(|v| v.max(*bounds.start()).min(*bounds.end())) }