fix(ui): accept toolbar drops across panel labels and gaps (#454)

This commit is contained in:
selune authored and GitHub committed 2026-10-12 20:25:38 -04:00
1 parent 5c7149e57d
commit ddd528abaf
4 files changed
+81 -7

No files matched your search

+2
View File
@@ -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.
+55 -6
View File
@@ -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::<ToolbarDrag>().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::<ToolbarDrag>() {
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::<ToolbarDrag>(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::<ToolbarDrag>(ui.ctx())
{
drop = Some((d.id, *panel, None));
}
}
let open = ui.ctx().data(|d| d.get_temp::<egui::Id>(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();
}
}
@@ -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}}
]
+3 -1
View File
@@ -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