From 55bfb7033e12757999d1c44b863873aa5ce86f48 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 30 Oct 2026 19:50:23 +0700 Subject: [PATCH] feat: show six Distribute Objects buttons in Control bar when 2+ objects selected (Illustrator parity) Squashed from #4561 onto main. --- crates/ui-egui/src/chrome/control_bar.rs | 6 +++ crates/ui-egui/src/chrome/tests.rs | 53 ++++++++++++++++++++++++ crates/ui-egui/src/panels/align.rs | 10 +++++ 3 files changed, 69 insertions(+) diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index f2c41b4df..c1bdfefa5 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -244,6 +244,12 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { return; } crate::panels::align::align_buttons(app, ui, 24.0); + // Two or more objects get the six Distribute Objects buttons too; a single + // object keeps the bar as it was. + if sel.len() > 1 { + ui.add_space(4.0); + crate::panels::align::distribute_buttons(app, ui, 24.0); + } ui.separator(); // The Transform link opens the whole Transform panel (reference point, rotate, // shear, options) in a popover; X/Y/W/H follow inline while the bar has room. diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 929b5a8a8..91a276e9b 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -1633,3 +1633,56 @@ fn full_screen_modes_fill_the_screen_and_normal_mode_restores_the_window() { app.ui.screen_mode = 0; assert!(sent(&mut app, true).is_empty(), "still full screen, as the user had it"); } + +/// Two or more selected objects show all six Distribute Objects buttons in the Control bar; +/// a single object keeps the bar as it was. A click distributes as the Align panel's button does. +#[test] +fn the_control_bar_shows_the_distribute_buttons_for_two_or_more_objects() { + crate::i18n::set_current(crate::i18n::Lang::EN); + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 200})).unwrap(); + let rect = |app: &mut crate::VectorcraftApp, x: f64| { + app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 30, "height": 20})).unwrap()["id"].clone() + }; + let (a, b, c) = (rect(&mut app, 10.0), rect(&mut app, 100.0), rect(&mut app, 200.0)); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 300.0)); + let frame = |app: &mut crate::VectorcraftApp, events: Vec| { + let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: Some(screen), ..Default::default() }, |ui| super::control_bar(app, ui)); + out.textures_delta.clear(); + out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default() + }; + const DISTRIBUTE: [&str; 6] = [ + "Vertical Distribute Top", + "Vertical Distribute Center", + "Vertical Distribute Bottom", + "Horizontal Distribute Left", + "Horizontal Distribute Center", + "Horizontal Distribute Right", + ]; + let rects = |app: &mut crate::VectorcraftApp| { + frame(app, vec![]); + let nodes = frame(app, vec![]); + DISTRIBUTE.map(|name| { + let n = nodes.iter().find(|(_, n)| n.label() == Some(name)).map(|(_, n)| n)?; + n.bounds().map(|b| egui::Rect::from_min_max(egui::pos2(b.x0 as f32, b.y0 as f32), egui::pos2(b.x1 as f32, b.y1 as f32))) + }) + }; + app.run("select.set", json!({"ids": [a]})).unwrap(); + assert!(rects(&mut app).iter().all(Option::is_none), "one object: no distribute buttons"); + app.run("select.set", json!({"ids": [a, b, c]})).unwrap(); + let rects = rects(&mut app); + assert!(rects.iter().all(Option::is_some), "three objects: {rects:?}"); + let r = rects[3].expect("Horizontal Distribute Left"); + let press = + |pressed| egui::Event::PointerButton { pos: r.center(), button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + for events in [vec![egui::Event::PointerMoved(r.center()), press(true)], vec![press(false)]] { + frame(&mut app, events); + } + let left = |app: &crate::VectorcraftApp, id: &serde_json::Value| { + app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().geometric_bounds().unwrap().x0 + }; + assert_eq!((left(&app, &a), left(&app, &b), left(&app, &c)), (10.0, 105.0, 200.0), "{}", app.ui.status); +} diff --git a/crates/ui-egui/src/panels/align.rs b/crates/ui-egui/src/panels/align.rs index 937a15828..0c322f0a0 100644 --- a/crates/ui-egui/src/panels/align.rs +++ b/crates/ui-egui/src/panels/align.rs @@ -192,6 +192,16 @@ pub fn align_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) { } } +/// The six Distribute Objects buttons of the Control bar, `size` points square. They distribute +/// what the Align panel's buttons do, to what its Align To says. +pub(crate) fn distribute_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) { + for (icon, tip, axis, v) in DISTRIBUTE { + if widgets::icon_button(ui, icon, tl!(tip), false, size).clicked() { + run_button(app, Button::Distribute(axis, v)).ok(); + } + } +} + /// Whether the selection has a key object or a key guide. fn has_key(app: &VectorcraftApp) -> bool { app.session.active().is_some_and(|st| st.selection.has_key())