diff --git a/crates/ui-egui/src/fillui.rs b/crates/ui-egui/src/fillui.rs index a0e60fb..7f0f7d1 100644 --- a/crates/ui-egui/src/fillui.rs +++ b/crates/ui-egui/src/fillui.rs @@ -276,7 +276,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) -> let (sw, _) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover()); ui.painter().rect(sw, CornerRadius::same(3), c32(&s.color, sc), Stroke::new(1.0, t.border), egui::StrokeKind::Inside); let r = widgets::drop_button(ui, "", vec2(24.0, 20.0), true); - egui::Popup::menu(&r).show(|ui| { + widgets::color_popup(&r).show(|ui| { if let Some(Some(c)) = widgets::color_grid(ui, sc, None) { let mut ng = g.clone(); ng.stops[sel].color = with_alpha(c, alpha_of(&s.color)); diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index 6d37463..fff1da4 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -75,7 +75,7 @@ fn color_button( ui.label(label); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let r = widgets::drop_button(ui, "Color", vec2(70.0, 22.0), true); - egui::Popup::menu(&r).show(|ui| { + widgets::color_popup(&r).show(|ui| { if let Some(c) = widgets::color_grid(ui, &sc, none_label) { let (id, p) = on_pick(c); run(app, &id, p); @@ -391,7 +391,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.label("Color"); let r = widgets::drop_button(ui, "Color", vec2(70.0, 20.0), true); - egui::Popup::menu(&r).show(|ui| { + widgets::color_popup(&r).show(|ui| { if let Some(Some(c)) = widgets::color_grid(ui, &sc, None) { run(app, "design.background", json!({"color": cref_param(&c)})); } @@ -417,7 +417,7 @@ fn background(app: &mut SlideApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.label(label); let r = widgets::drop_button(ui, "Color", vec2(70.0, 20.0), true); - egui::Popup::menu(&r).show(|ui| { + widgets::color_popup(&r).show(|ui| { if let Some(Some(c)) = widgets::color_grid(ui, &sc, None) { let (f, b) = if fg { (c, pt.bg.clone()) } else { (pt.fg.clone(), c) }; run(app, "design.background", json!({"pattern": {"preset": pt.preset, "fg": cref_param(&f), "bg": cref_param(&b)}})); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 3f063ca..897fa75 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -473,7 +473,7 @@ fn font_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { if m.clicked() { run(app, "format.highlight", json!({"color": "#FFFF00"})); } - egui::Popup::menu(&a).show(|ui| { + widgets::color_popup(&a).show(|ui| { if let Some(c) = widgets::color_grid(ui, &sc, Some("No Color")) { run(app, "format.highlight", json!({"color": c.map(|c| serde_json::to_value(cref_param(&c)).unwrap_or_default())})); } @@ -482,7 +482,7 @@ fn font_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) { if m.clicked() { run(app, "format.color", json!({"color": "#C00000"})); } - egui::Popup::menu(&a).show(|ui| { + widgets::color_popup(&a).show(|ui| { if let Some(Some(c)) = widgets::color_grid(ui, &sc, None) { run(app, "format.color", json!({"color": cref_param(&c)})); } @@ -743,7 +743,7 @@ fn drawing_group(app: &mut SlideApp, ui: &mut Ui) { run(app, "shape.fill", json!({"color": "accent1"})); } if let Some(a) = a { - egui::Popup::menu(&a).show(|ui| { + widgets::color_popup(&a).show(|ui| { if let Some(c) = widgets::color_grid(ui, &sc, Some("No Fill")) { match c { Some(c) => run(app, "shape.fill", json!({"color": cref_param(&c)})), @@ -757,7 +757,7 @@ fn drawing_group(app: &mut SlideApp, ui: &mut Ui) { run(app, "shape.line", json!({"color": "tx1", "width": 1})); } if let Some(a) = a { - egui::Popup::menu(&a).show(|ui| outline_menu(app, ui, &sc)); + widgets::color_popup(&a).show(|ui| outline_menu(app, ui, &sc)); } let (_, a) = small_button_ex(ui, Icon::ShapeEffects, "Shape Effects", sel, false, true); if let Some(a) = a { @@ -778,6 +778,7 @@ pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::Color for w in [0.25, 0.5, 0.75, 1.0, 1.5, 2.25, 3.0, 4.5, 6.0] { if ui.button(format!("{w} pt")).clicked() { run(app, "shape.line", json!({"width": w})); + ui.close(); } } }); @@ -793,6 +794,7 @@ pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::Color ] { if ui.button(l).clicked() { run(app, "shape.line", json!({"dash": d})); + ui.close(); } } }); @@ -807,6 +809,7 @@ pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::Color ] { if ui.button(l).clicked() { run(app, "shape.line", json!({"head": h, "tail": tl})); + ui.close(); } } }); @@ -1881,7 +1884,7 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { run(app, "shape.fill", json!({"color": "accent1"})); } if let Some(a) = a { - egui::Popup::menu(&a).show(|ui| { + widgets::color_popup(&a).show(|ui| { if let Some(c) = widgets::color_grid(ui, &sc, Some("No Fill")) { match c { Some(c) => run(app, "shape.fill", json!({"color": cref_param(&c)})), @@ -1890,12 +1893,13 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { } if ui.button("Gradient…").clicked() { let _ = app.run("view.pane", json!({"pane": "format", "tab": "fill"})); + ui.close(); } }); } let (_, a) = small_button_ex(ui, Icon::ShapeOutline, "Shape Outline", sel, false, true); if let Some(a) = a { - egui::Popup::menu(&a).show(|ui| outline_menu(app, ui, &sc)); + widgets::color_popup(&a).show(|ui| outline_menu(app, ui, &sc)); } let (_, a) = small_button_ex(ui, Icon::ShapeEffects, "Shape Effects", sel, false, true); if let Some(a) = a { @@ -1907,7 +1911,7 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { rows(ui, |ui| { let (_, a) = small_button_ex(ui, Icon::TextFill, "Text Fill", sel, false, true); if let Some(a) = a { - egui::Popup::menu(&a).show(|ui| { + widgets::color_popup(&a).show(|ui| { if let Some(Some(c)) = widgets::color_grid(ui, &sc, None) { run(app, "format.color", json!({"color": cref_param(&c)})); } @@ -1915,7 +1919,7 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) { } let (_, a) = small_button_ex(ui, Icon::TextOutline, "Text Outline", sel, false, true); if let Some(a) = a { - egui::Popup::menu(&a).show(|ui| { + widgets::color_popup(&a).show(|ui| { if let Some(c) = widgets::color_grid(ui, &sc, Some("No Outline")) { run(app, "format.textOutline", json!({"color": c.map(|c| cref_param(&c))})); } @@ -2183,7 +2187,7 @@ fn table_design(app: &mut SlideApp, ui: &mut Ui) { if m.clicked() { run(app, "table.cellFill", json!({"color": "accent1"})); } - egui::Popup::menu(&a).show(|ui| { + widgets::color_popup(&a).show(|ui| { if let Some(c) = widgets::color_grid(ui, &sc, Some("No Fill")) { match c { Some(c) => run(app, "table.cellFill", json!({"color": cref_param(&c)})), diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 47282e2..5827b39 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -239,6 +239,13 @@ pub fn dropdown(ui: &mut Ui, id: &str, current: &str, width: f32, enabled: bool, resp } +/// The drop-down for a `color_grid`: a menu that stays open when its hex field is clicked +/// (egui menus close on any click inside). Picking a colour closes it (`color_grid` calls +/// `ui.close()`); other buttons in it must close it themselves. +pub fn color_popup(button: &Response) -> egui::Popup<'_> { + egui::Popup::menu(button).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) +} + /// The colour grid popup body: theme colours with tints/shades, standard colours, extra entries. /// Returns the picked colour (`Some(None)` = "No Fill"/"Automatic" when `none_label` is given). pub fn color_grid(ui: &mut Ui, scheme: &ColorScheme, none_label: Option<&str>) -> Option> { @@ -440,3 +447,42 @@ pub fn drop_button(ui: &mut Ui, label: &str, size: egui::Vec2, enabled: bool) -> chevron(ui.painter(), egui::pos2(r.rect.max.x - 8.0, r.rect.center().y), 3.0, col); r } + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used, clippy::expect_used)] + + use std::cell::RefCell; + + use super::*; + use egui::accesskit::Role; + use egui_kittest::kittest::Queryable; + + /// #32: clicking the "More Colors" hex field closed the colour menu before anything could be + /// typed. It stays open now; Enter picks the colour and closes it. + #[test] + fn the_hex_field_keeps_the_color_menu_open() { + let picked = RefCell::new(None); + let scheme = deckcraft_model::Theme::default().colors; + let mut harness = egui_kittest::Harness::builder().with_size(vec2(400.0, 500.0)).build_ui(|ui| { + theme::install_fonts(ui.ctx()); + let r = ui.button("Pick"); + color_popup(&r).show(|ui| { + if let Some(c) = color_grid(ui, &scheme, None) { + *picked.borrow_mut() = Some(c); + } + }); + }); + harness.get_by_label("Pick").click(); + harness.run(); + harness.get_by_role(Role::TextInput).click(); + harness.run(); + assert!(harness.query_by_role(Role::TextInput).is_some(), "the menu is still open after clicking the hex field"); + harness.get_by_role(Role::TextInput).type_text("00FF00"); + harness.run(); + harness.key_press(egui::Key::Enter); + harness.run(); + assert_eq!(*picked.borrow(), Some(Some(ColorRef::rgb(Rgba::from_hex("00FF00").unwrap())))); + assert!(harness.query_by_role(Role::TextInput).is_none(), "picking a colour closes the menu"); + } +}