From d9b5eea18d04843f6092a8418e160561144ad005 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 16:53:57 -0600 Subject: [PATCH] Fix #536: the Fill/Stroke popover's Swatch Libraries button opens its menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the Fill or Stroke chip's popover (Control bar, Properties › Appearance), the Swatch Libraries Menu button at the foot of the swatches closed the popover and showed nothing, as in the video on #536; Show Swatch Kinds did the same. egui remembers one open popup per window: the chip's popover was such a remembered popup, so the button's menu, opening as another one, closed the popover and itself with it. The popover now keeps its own open state with widgets::popover (made for popovers that open menus of their own), so the libraries menu opens over it and a library chosen there opens in the library panel. Escape, a click outside and a click on the chip still close it. Test: tests_paintchips::the_popover_swatch_libraries_button_opens_the_libraries_menu (fails before this change: the popover closes). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01K3vVft4VXvvy8SpbbUceTW --- crates/ui-egui/src/panels/mod.rs | 7 ++-- crates/ui-egui/src/tests_paintchips.rs | 52 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 6c7fa7712..2ce75db3c 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -346,10 +346,9 @@ pub(crate) fn paint_chip(app: &mut VectorcraftApp, ui: &mut Ui, stroke: bool, si app.run("paint.toggleActive", json!({ "fill": !stroke })).ok(); set_pstate(ui.ctx(), MIXER, ui.input(|i| i.modifiers.shift)); } - egui::Popup::from_toggle_button_response(&resp) - .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) - .width(POPOVER_WIDTH) - .show(|ui| paint_popover(app, ui)); + // A popover that keeps its own open state: the Swatches body opens menus of its own (Swatch + // Libraries, Show Swatch Kinds), which a remembered egui popup would close with them (#536). + crate::widgets::popover(&resp, resp.clicked(), |ui| paint_popover(app, ui)); } /// The chip popovers' width (a narrow Swatches or Color panel). diff --git a/crates/ui-egui/src/tests_paintchips.rs b/crates/ui-egui/src/tests_paintchips.rs index 4ce5cf5b9..b28ad83e3 100644 --- a/crates/ui-egui/src/tests_paintchips.rs +++ b/crates/ui-egui/src/tests_paintchips.rs @@ -270,3 +270,55 @@ fn the_transform_link_opens_the_transform_panel() { click(&mut app, &ctx, Pos2::new(1300.0, 900.0), Modifiers::NONE, control_bar); assert!(text_rect(&frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar), "Scale Corners").is_none()); } + +/// The 24 pt icon buttons of the popover's bottom bar (its Swatch Libraries Menu first), left to +/// right. +fn popover_bottom_buttons(ctx: &egui::Context) -> Vec { + let mut r: Vec = ctx.viewport(|vp| { + vp.prev_pass + .widgets + .layers() + .filter(|(l, _)| l.order != egui::Order::Background) + .flat_map(|(_, w)| w.iter()) + .filter(|w| w.rect.size() == vec2(24.0, 24.0)) + .map(|w| w.rect) + .collect() + }); + let bottom = r.iter().map(|r| r.top()).fold(f32::MIN, f32::max); + r.retain(|w| (w.top() - bottom).abs() < 1.0); + r.sort_by(|a, b| a.left().total_cmp(&b.left())); + r +} + +/// A menu row's text (menu labels leave room for a check mark before them). +fn menu_item_rect(shapes: &[Shape], s: &str) -> Option { + shapes.iter().find_map(|sh| match sh { + Shape::Text(t) if t.galley.text().trim() == s => Some(t.visual_bounding_rect()), + Shape::Vec(v) => menu_item_rect(v, s), + _ => None, + }) +} + +/// #536: the Swatch Libraries Menu button at the foot of a Fill chip's popover opens the +/// libraries menu (it used to close the popover and show nothing), and a library chosen there +/// opens in the library panel. +#[test] +fn the_popover_swatch_libraries_button_opens_the_libraries_menu() { + let mut app = app(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); + let ctx = context(); + frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar); + let (fill, _) = chips(&ctx); + click(&mut app, &ctx, fill.center(), Modifiers::NONE, control_bar); + let libraries = *popover_bottom_buttons(&ctx).first().expect("the popover has a bottom bar"); + click(&mut app, &ctx, libraries.center(), Modifiers::NONE, control_bar); + let shapes = frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar); + assert!(text_rect(&shapes, "Swatch Tiles").is_some(), "the popover stays open"); + let defaults = menu_item_rect(&shapes, "Default Swatches").expect("the libraries menu is open"); + assert!(menu_item_rect(&shapes, "Other Library…").is_some()); + let earth = menu_item_rect(&shapes, "Earth Tones").expect("the menu lists the built-in libraries"); + assert!(earth.top() > defaults.top()); + click(&mut app, &ctx, earth.center(), Modifiers::NONE, control_bar); + let open = app.ui.library_panel.as_ref().map(|o| (o.kind.clone(), o.id.clone())); + assert_eq!(open, Some(("swatches".into(), "earth-tones".into()))); +}