Fix #44: right-clicking a grouped tool opens its sub-tools flyout

A right-click on a toolbar button with sub-tools (the small triangle) opens
the flyout and it stays open until a tool in it is chosen or the user clicks
elsewhere. A long press opens it too, and releasing that press no longer
closes it or picks the button's tool.

Root cause: the right-click did open the flyout, but the flyout closes on a
click outside itself, and the release of that same right-click (on the tool
button, outside the flyout) closed it in the same frame. The release of a
long press shorter than egui's click time did the same, and also selected
the button's tool. Clicks on the flyout's own tool button now leave it to
that button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-06 00:13:06 +07:00
parent 831d916d3d
commit 25a194cc00
1 changed files with 52 additions and 1 deletions

View File

@ -109,6 +109,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
// The tools and the controls under them scroll in a window too short for them.
widgets::strip_scroll(ui, "toolbar", |ui| {
let active = app.session.tool_id();
// The tool button whose long press opened its flyout, until the next press.
let held_id = egui::Id::new("toolbar-held");
if ui.input(|inp| inp.pointer.any_pressed()) {
ui.data_mut(|d| d.remove::<egui::Id>(held_id));
}
let held: Option<egui::Id> = ui.data(|d| d.get_temp(held_id));
let mut open_flyout: Option<(Vec<&'static str>, egui::Rect)> = None;
let mut i = 0;
while i < all.len() {
@ -160,6 +166,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let alt = ui.input(|inp| inp.modifiers.alt);
if (resp.secondary_clicked() || long_press) && slot.len() > 1 {
open_flyout = Some((slot.clone(), rect));
if long_press {
ui.data_mut(|d| d.insert_temp(held_id, resp.id));
}
} else if resp.clicked() && held == Some(resp.id) {
// Releasing the long press that opened the flyout leaves it open.
} else if resp.clicked() && alt && slot.len() > 1 {
let idx = slot.iter().position(|x| *x == shown.id).unwrap_or(0);
app.select_tool(slot[(idx + 1) % slot.len()]);
@ -312,7 +323,9 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) {
);
if let Some(id) = chosen {
app.select_tool(id);
} else if resp.response.clicked_elsewhere() {
} else if resp.response.clicked_elsewhere() && !ctx.input(|i| i.pointer.interact_pos().is_some_and(|p| anchor.contains(p))) {
// A click on the flyout's own tool button (the right-click that opened it, or the end of a
// long press) leaves it to that button.
app.ui.flyout = None;
}
let _ = theme::semibold;
@ -409,4 +422,42 @@ pub(crate) mod tests {
assert!(o.pick_up.appearance.transparency && !o.apply.appearance.transparency && o.apply.appearance.fill.color);
assert!(app.run("tool.options", json!({"tool": "zoom"})).is_err());
}
#[test]
fn right_clicking_a_tool_group_opens_its_flyout() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
let ctx = egui::Context::default();
let buttons = frame(&mut app, &ctx, 0.0, vec![]);
// Selection, Direct Selection, Lasso, then the Shapes group (Rectangle and its sub-tools).
let at = buttons[3].center();
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed, modifiers: Default::default() };
frame(&mut app, &ctx, 1.0, vec![Event::PointerMoved(at), b(true)]);
frame(&mut app, &ctx, 1.05, vec![b(false)]);
frame(&mut app, &ctx, 1.1, vec![]);
assert!(app.ui.flyout.is_some(), "the right-click opens the flyout and the same click doesn't close it");
let menu = ctx.memory(|m| m.area_rect(egui::Id::new("tool-flyout"))).expect("the flyout is shown");
assert_eq!(app.session.tool_id(), "selection", "a right-click only opens the flyout");
// Its second row is Rounded Rectangle (30 pt rows).
let row = egui::pos2(menu.left() + 60.0, menu.top() + 45.0);
let p = |pressed| Event::PointerButton { pos: row, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, &ctx, 2.0, vec![Event::PointerMoved(row), p(true)]);
frame(&mut app, &ctx, 2.05, vec![p(false)]);
assert_eq!((app.session.tool_id(), app.ui.flyout), ("roundedRectangle", None));
// A long press opens it too, and releasing the press (a click, being short of 0.8 s) leaves
// it open without choosing the button's tool.
let p = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, &ctx, 3.0, vec![Event::PointerMoved(at), p(true)]);
frame(&mut app, &ctx, 3.5, vec![]);
assert!(app.ui.flyout.is_some(), "a long press opens the flyout");
frame(&mut app, &ctx, 3.6, vec![p(false)]);
frame(&mut app, &ctx, 3.7, vec![]);
assert!(app.ui.flyout.is_some(), "releasing the long press leaves the flyout open");
// A click anywhere else closes it.
let away = egui::pos2(300.0, 1000.0);
let c = |pressed| Event::PointerButton { pos: away, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, &ctx, 4.0, vec![Event::PointerMoved(away), c(true)]);
frame(&mut app, &ctx, 4.05, vec![c(false)]);
assert_eq!((app.session.tool_id(), app.ui.flyout), ("roundedRectangle", None));
}
}