From ddd528abaffc7658639ad9122011419b35372037 Mon Sep 17 00:00:00 2001 From: selune <35337584+0xlillith@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:25:38 +0200 Subject: [PATCH] fix(ui): accept toolbar drops across panel labels and gaps (#454) --- ROADMAP.md | 2 + crates/ui-egui/src/toolbar.rs | 61 +++++++++++++++++-- .../scenarios/s50_toolbar_panel_drops.json | 21 +++++++ docs/book/src/guide/ui-tour.md | 4 +- 4 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 crates/ui-egui/tests/scenarios/s50_toolbar_panel_drops.json diff --git a/ROADMAP.md b/ROADMAP.md index ac796bc5..504b6e71 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -287,6 +287,8 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings. record their coordinate frame; legacy wires migrate from known provenance without changing coordinates. Broken legacy links with missing provenance can require explicit refresh or relink. See [occurrence workflows](docs/occurrence-workflows.md). +- Toolbar drops on panel labels, gaps and margins append commands with insertion feedback; + button drops keep insert-before ordering. - Parameters and Configurations have distinct actions in the SOLID toolbar's CONFIGURE panel. Parameters opens the user/model variable editor; Configurations opens the configuration table. Configure remains available in the panel dropdown. diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 266da264..9d6e0570 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -572,12 +572,6 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { let free_x = bx + drawn as f32 * 40.0; let free = Rect::from_min_size(pos2(free_x, top + 2.0), vec2((px + width - 4.0 - free_x).max(8.0), 38.0)); let fresp = ui.interact(free, ui.id().with(("panel_free", panel)), if collapsed { Sense::click() } else { Sense::hover() }); - if fresp.dnd_hover_payload::().is_some() { - painter.line_segment([pos2(free.left(), free.top() + 2.0), pos2(free.left(), free.bottom() - 2.0)], Stroke::new(2.0, t.accent)); - } - if let Some(d) = fresp.dnd_release_payload::() { - drop = Some((d.id, *panel, None)); - } let mut opened = false; if collapsed { // One button: the panel's first command's icon over a caret; it opens the @@ -691,6 +685,20 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { if lresp.clicked() { ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new("sc_panel_open"), popup_id)); } + // Labels, gaps and margins append; actual buttons keep their insert-before target. + // Respect the current layer so a dropdown covering the panel is not a drop target. + let panel_rect = Rect::from_min_max(pos2(px, top + 2.0), pos2(px + width, r.bottom())); + let over_button = ui + .input(|input| input.pointer.hover_pos()) + .is_some_and(|p| (0..drawn).any(|i| Rect::from_min_size(pos2(bx + i as f32 * 40.0, top + 2.0), vec2(38.0, 38.0)).contains(p))); + if !over_button && ui.rect_contains_pointer(panel_rect) && egui::DragAndDrop::payload::(ui.ctx()).is_some() { + painter.line_segment([pos2(px + width - 4.0, top + 4.0), pos2(px + width - 4.0, top + 38.0)], Stroke::new(2.0, t.accent)); + if ui.input(|input| input.pointer.any_released()) + && let Some(d) = egui::DragAndDrop::take_payload::(ui.ctx()) + { + drop = Some((d.id, *panel, None)); + } + } let open = ui.ctx().data(|d| d.get_temp::(egui::Id::new("sc_panel_open"))) == Some(popup_id); if open { let cmds = if collapsed { menu_commands(&cmds, &shown) } else { cmds.clone() }; @@ -1319,3 +1327,44 @@ mod dropdown_tests { assert!(menu.contains(*at), "the focused last row is out of view: {at:?} in {menu:?}"); } } + +#[cfg(test)] +mod panel_drop_tests { + use super::*; + + #[test] + fn panel_label_gap_and_margins_append_while_buttons_insert_before() { + for target in ["label", "gap", "left margin", "right margin", "button"] { + let mut h = crate::scenario::Harness::new(); + h.step(&json!({"click":{"handle":"panel:SOLID:CREATE"}})).unwrap(); + let label = crate::scenario::handle_at("panel:SOLID:CREATE").unwrap(); + let destination = match target { + "gap" => label - vec2(0.0, 10.0), + "left margin" => pos2(label.x - panel_width(4) / 2.0 + 1.0, label.y - 30.0), + "right margin" => pos2(label.x + panel_width(4) / 2.0 - 1.0, label.y - 30.0), + "button" => crate::scenario::handle_at("button:solid.extrude").unwrap(), + _ => label, + }; + h.step(&json!({"drag":[{"handle":"dropdown:solid.cylinder"}, [destination.x,destination.y]],"steps":12})).unwrap(); + h.step(&json!({"key":"Escape"})).unwrap(); + let order = if target == "button" { + json!(["button:sketch.create", "button:solid.cylinder", "button:solid.extrude", "button:solid.revolve", "button:solid.sweep"]) + } else { + json!(["button:sketch.create", "button:solid.extrude", "button:solid.revolve", "button:solid.sweep", "button:solid.cylinder"]) + }; + h.step(&json!({"expect":{"handle_order":order,"tool":null,"dialog":null,"errors":0}})).unwrap(); + assert!(h.app.session.doc.features.is_empty(), "{target}: pinning must not run the command"); + h.step(&json!({"restart":"close"})).unwrap(); + h.step(&json!({"expect":{"handle_order":order}})).unwrap(); + } + } + + #[test] + fn panel_label_drop_moves_a_button_between_panels() { + let mut h = crate::scenario::Harness::new(); + h.step(&json!({"drag":[{"handle":"button:solid.extrude"},{"handle":"panel:SOLID:MODIFY"}],"steps":12})).unwrap(); + h.step(&json!({"expect":{"handle_order":["button:solid.fillet","button:solid.chamfer","button:solid.scale","button:solid.extrude"],"tool":null,"dialog":null,"errors":0}})).unwrap(); + h.step(&json!({"restart":"close"})).unwrap(); + h.step(&json!({"expect":{"handle_order":["button:solid.scale","button:solid.extrude"]}})).unwrap(); + } +} diff --git a/crates/ui-egui/tests/scenarios/s50_toolbar_panel_drops.json b/crates/ui-egui/tests/scenarios/s50_toolbar_panel_drops.json new file mode 100644 index 00000000..46497bf6 --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s50_toolbar_panel_drops.json @@ -0,0 +1,21 @@ +[ + {"note":"#333: dropping a dropdown command on a panel label appends it; moving between labels keeps the chosen order after restart."}, + {"click":{"handle":"panel:SOLID:CREATE"}}, + {"drag":[{"handle":"dropdown:solid.cylinder"},{"handle":"panel:SOLID:CREATE"}],"steps":12}, + {"key":"Escape"}, + {"expect":{"handle_order":["button:solid.sweep","button:solid.cylinder"],"dialog":null,"tool":null,"errors":0}}, + {"click":{"handle":"panel:SOLID:CREATE"}}, + {"drag":[{"handle":"dropdown:solid.torus"},{"handle":"panel:SOLID:MODIFY"}],"steps":12}, + {"key":"Escape"}, + {"expect":{"handle_order":["button:solid.scale","button:solid.torus"],"dialog":null,"tool":null,"errors":0}}, + {"drag":[{"handle":"button:solid.torus"},{"handle":"panel:SOLID:CREATE"}],"steps":12}, + {"expect":{"handle_order":["button:solid.cylinder","button:solid.torus"]}}, + {"restart":"close"}, + {"expect":{"handle_order":["button:solid.sweep","button:solid.cylinder","button:solid.torus"],"errors":0}}, + {"call":"ui.resize","params":{"width":640,"height":720}}, + {"click":{"handle":"panel:SOLID:CREATE"}}, + {"drag":[{"handle":"dropdown:solid.box"},{"handle":"panel:SOLID:CREATE"}],"steps":12}, + {"key":"Escape"}, + {"call":"ui.resize","params":{"width":1600,"height":1000}}, + {"expect":{"handle_order":["button:solid.torus","button:solid.box"],"tool":null,"dialog":null,"errors":0}} +] diff --git a/docs/book/src/guide/ui-tour.md b/docs/book/src/guide/ui-tour.md index f5779c8a..6f3b18b2 100644 --- a/docs/book/src/guide/ui-tour.md +++ b/docs/book/src/guide/ui-tour.md @@ -7,7 +7,9 @@ undo history, selection and camera). - **Toolbar.** The workspace switcher (DESIGN) sits on the left. The tabs (SOLID, SURFACE, SHEET METAL, PLASTIC, UTILITIES, and SKETCH while you sketch) hold panels of commands. Click a - panel's name for all its commands. + panel's name for all its commands. Drag a command from the dropdown onto a panel's label + or empty space to append it; the insertion line shows its destination. Drop onto an existing + button to insert before it. Buttons can also move between panels, and the layout is saved. - **Browser** (left). The design as a tree: document settings and units, named views, the origin, bodies, sketches, construction planes, canvases, joints and components. The eye shows or hides an item, a component's radio button makes it the active component, and right-clicking any