Toolbar: a second click on a drop-down's button closes it (#1522) (#1576)

The workspace switcher's menu closed on the press (a press outside the
menu) and the click reopened it; the menu now keeps the button that
opened it, a press there is not outside, and its click toggles. A
panel's label toggles its command list instead of always opening it.

Fixes #1522

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
DuBearteandClaude Opus 5.5 authored and GitHub committed 2026-10-15 22:04:42 -04:00
1 parent f08ed74617
commit 105a3146c0
2 files changed
+56 -3

No files matched your search

+16 -1
View File
@@ -179,6 +179,9 @@ pub struct MenuState {
pub over_geometry: bool,
/// The plane under the cursor when the viewport menu opened: it offers a sketch on it.
pub plane: Option<String>,
/// The button that opened the menu, when it toggles it: a press there is not a press outside,
/// so its click closes the menu instead of closing and reopening it (#1522).
opener: Option<Rect>,
}
#[derive(Clone, Debug, PartialEq)]
@@ -1349,12 +1352,24 @@ pub fn open_for(app: &mut SolveApp, at: Pos2, target: Target) {
app.menu.radial = target == Target::Viewport && app.preferences.radial_context_menu;
app.menu.target = target;
app.menu.flick = false;
app.menu.opener = None;
app.viewport.context_menu = Some(at);
}
/// A button's menu: a click opens it, and a click on the same button while it is open closes it.
pub fn toggle_for(app: &mut SolveApp, opener: Rect, at: Pos2, target: Target) {
if app.viewport.context_menu.is_some() && app.menu.target == target && app.menu.opener == Some(opener) {
close(app);
} else {
open_for(app, at, target);
app.menu.opener = Some(opener);
}
}
pub fn close(app: &mut SolveApp) {
app.viewport.context_menu = None;
app.menu.flick = false;
app.menu.opener = None;
}
/// Open the menu at a point for automation: the pointer moves there first so what is under it
@@ -1557,8 +1572,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let target = app.menu.target.clone();
let list = items(app, &target);
let mut picked: Option<Item> = None;
let mut inside = false;
let pointer = ctx.input(|i| i.pointer.latest_pos());
let mut inside = app.menu.opener.is_some_and(|r| pointer.is_some_and(|p| r.contains(p)));
let list_at = if app.menu.radial {
let c = radial_center(at, screen);
+40 -2
View File
@@ -582,7 +582,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
caret(&painter, pos2(ws.center().x + 27.0, ws.top() + 52.0), t.text);
painter.text(pos2(ws.center().x, ws.top() + 69.0), Align2::CENTER_CENTER, app.tr("ui.workspace"), FontId::proportional(10.0), t.text_dim);
if wresp.on_hover_text(app.tr("ui.change_workspace")).clicked() {
crate::context_menu::open_for(app, ws.left_bottom(), crate::context_menu::Target::Workspace);
crate::context_menu::toggle_for(app, ws, ws.left_bottom(), crate::context_menu::Target::Workspace);
}
crate::scenario::publish_rect("toolbar", r);
crate::scenario::publish_rect("workspace_switcher", ws);
@@ -830,8 +830,16 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
let gy = lr.center().y - g.size().y / 2.0;
painter.galley(pos2(gx, gy), g, lc);
caret(&painter, pos2(gx + gw / 2.0 + 6.0, lr.center().y), lc);
// The label toggles its drop-down (#1522).
if lresp.clicked() {
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new("sc_panel_open"), popup_id));
ui.ctx().data_mut(|d| {
let key = egui::Id::new("sc_panel_open");
if d.get_temp::<egui::Id>(key) == Some(popup_id) {
d.remove::<egui::Id>(key);
} else {
d.insert_temp(key, 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.
@@ -1813,3 +1821,33 @@ mod icon_accent_tests {
}
}
}
#[cfg(test)]
mod dropdown_toggle_tests {
use crate::scenario::{Harness, rect_of};
use serde_json::json;
/// A second click on the button that opened a drop-down closes it (#1522): the workspace
/// switcher's menu, and a panel's command list.
#[test]
fn a_second_click_on_its_button_closes_a_drop_down() {
let mut h = Harness::new();
h.frames(3);
let ws = rect_of("workspace_switcher").unwrap().center();
h.step(&json!({"click": [ws.x, ws.y]})).unwrap();
assert!(h.app.viewport.context_menu.is_some(), "the first click opens the workspace menu");
h.step(&json!({"click": [ws.x, ws.y]})).unwrap();
h.frames(2);
assert!(h.app.viewport.context_menu.is_none(), "the second click closes it instead of reopening it");
h.step(&json!({"click": [ws.x, ws.y]})).unwrap();
assert!(h.app.viewport.context_menu.is_some(), "and a third opens it again");
h.step(&json!({"key": "Escape"})).unwrap();
h.step(&json!({"click": {"handle": "panel:SOLID:CREATE"}})).unwrap();
h.frames(2);
assert!(rect_of("toolbar_dropdown").is_some(), "the first click opens the panel's list");
h.step(&json!({"click": {"handle": "panel:SOLID:CREATE"}})).unwrap();
h.frames(2);
assert!(rect_of("toolbar_dropdown").is_none(), "the second click closes it");
}
}