diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs index ca1f3c28f..4e39c75b7 100644 --- a/crates/ui-egui/src/panels/appearance.rs +++ b/crates/ui-egui/src/panels/appearance.rs @@ -385,9 +385,23 @@ pub(crate) fn row_popup(app: &mut VectorcraftApp, ctx: &egui::Context, panel: Re let area_id = egui::Id::unique("ap-row-popup"); let shown = ctx.memory(|m| m.area_rect(area_id)); // A press outside it (not one that closes a dropdown the popup opened) or Escape puts it away. + // With a field in it that has the keyboard, a press draws it once more first, so the field lets + // go of the keyboard to that press and commits what was typed, as in a panel (#6429). Escape + // drops what was typed. let pressed_outside = ctx.input(|i| i.pointer.any_pressed() && i.pointer.interact_pos().is_some_and(|p| shown.is_some_and(|r| !r.contains(p)))); - if pressed_outside && !egui::Popup::is_any_open(ctx) || ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) { + let mut last = false; + if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) { return close(app); + } else if pressed_outside && !egui::Popup::is_any_open(ctx) { + if !widgets::field_focused_in(ctx, egui::LayerId::new(egui::Order::Foreground, area_id)) { + return close(app); + } + last = true; + } + // Fields let go of the keyboard on a click, which this press isn't yet. + let surrender = ctx.options(|o| o.input_options.surrender_focus_on); + if last { + ctx.options_mut(|o| o.input_options.surrender_focus_on = egui::SurrenderFocusOn::Presses); } let screen = ctx.content_rect(); let left = panel.left() - POPUP_GAP - POPUP_W; @@ -415,6 +429,10 @@ pub(crate) fn row_popup(app: &mut VectorcraftApp, ctx: &egui::Context, panel: Re } }); }); + if last { + ctx.options_mut(|o| o.input_options.surrender_focus_on = surrender); + close(app); + } } /// A row's name that edits the row when clicked ([`link`]), with the tooltip "Click to edit". diff --git a/crates/ui-egui/src/panels/tests_appearance.rs b/crates/ui-egui/src/panels/tests_appearance.rs index 0431d6dec..2cf78847a 100644 --- a/crates/ui-egui/src/panels/tests_appearance.rs +++ b/crates/ui-egui/src/panels/tests_appearance.rs @@ -1994,3 +1994,31 @@ fn the_drop_buttons_light_up_while_a_row_is_dragged() { assert_eq!(drop_buttons_on(&ctx, &mut app, vec![]), [false, false], "dark again once it is released"); assert_eq!(stack_items(&app).len(), 2, "released elsewhere: nothing removed"); } + +/// #6429: a dash length typed into the Stroke panel that the Appearance panel's "Stroke:" row or +/// the Properties panel's Stroke link opens sticks when a click outside closes the popup, as it +/// does when Enter ends the edit. The popup used to close without drawing again, so the field +/// never saw itself lose the keyboard and the value went back. +#[test] +fn a_dash_typed_in_a_stroke_popup_sticks_when_a_click_outside_closes_it() { + type Panel = fn(&mut VectorcraftApp, &mut egui::Ui); + let hosts: [(&str, Panel); 2] = [("Stroke:", appearance::show), ("Stroke", super::properties::show)]; + for (link, show) in hosts { + let mut app = app_with_rect(); + run(&mut app, "stroke.set", json!({"dash": [10.0]})); + let ctx = egui::Context::default(); + let texts = frame_events(&ctx, &mut app, vec![], show); + click(&ctx, &mut app, text_rect(&texts, link).center(), show); + // The popup's first dash field: a click gives it the keyboard with its text selected. + let texts = frame_events(&ctx, &mut app, vec![], show); + click(&ctx, &mut app, text_rect(&texts, "10").center(), show); + frame_events(&ctx, &mut app, vec![egui::Event::Text("4".into())], show); + let dash = |app: &VectorcraftApp| current_stroke(app).and_then(|s| s.dash).map(|d| d.pattern); + assert_eq!(dash(&app), Some(vec![10.0]), "{link}: nothing set while typing"); + click(&ctx, &mut app, egui::pos2(5.0, 3000.0), show); + frame_events(&ctx, &mut app, vec![], show); + assert_eq!(dash(&app).and_then(|p| p.first().copied()), Some(4.0), "{link}: the typed dash stuck: {:?}", dash(&app)); + let texts = frame_events(&ctx, &mut app, vec![], show); + assert!(!texts.iter().any(|(t, _)| t == "Dashed Line"), "{link}: the popup closed"); + } +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 56bac9808..be2965ef3 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -47,7 +47,7 @@ pub use drag::{ use dropdowns::combo_button_id; #[cfg(test)] pub(crate) use dropdowns::menu_floor; -pub(crate) use dropdowns::{combo, combo_menu, combo_menu_field, combo_named, popover_is_open}; +pub(crate) use dropdowns::{combo, combo_menu, combo_menu_field, combo_named, field_focused_in, popover_is_open}; pub use dropdowns::{ dropdown, dropdown_named, dropdown_names, dropdown_names_named, dropdown_with, dropdown_with_named, list_scroll_style, menu_room, menu_scroll, panel_scroll, popover, set_menu_floor, strip_scroll, diff --git a/crates/ui-egui/src/widgets/dropdowns.rs b/crates/ui-egui/src/widgets/dropdowns.rs index 1046e56a7..d5d914b7b 100644 --- a/crates/ui-egui/src/widgets/dropdowns.rs +++ b/crates/ui-egui/src/widgets/dropdowns.rs @@ -234,8 +234,13 @@ pub fn popover(resp: &Response, toggle: bool, content: impl FnOnce(&mut Ui) - && click_pos.is_some_and(|pos| ctx.layer_id_at(pos).is_none_or(|l| l.order != egui::Order::Foreground)); was_open && !clicked_elsewhere }; + // Closing with a field in it that has the keyboard: it draws once more, so the field lets go of + // the keyboard to the click and commits what was typed, as in a panel (#6429). + let last = was_open && !open && field_focused_in(ctx, egui::LayerId::new(egui::Order::Foreground, id)); + let mut shown = open || last; let inner = - egui::Popup::menu(resp).id(id).open_bool(&mut open).close_behavior(egui::PopupCloseBehavior::IgnoreClicks).show(content).map(|r| r.inner); + egui::Popup::menu(resp).id(id).open_bool(&mut shown).close_behavior(egui::PopupCloseBehavior::IgnoreClicks).show(content).map(|r| r.inner); + open = shown && !last; ctx.data_mut(|d| { if open { d.insert_temp(id, frame); @@ -246,6 +251,13 @@ pub fn popover(resp: &Response, toggle: bool, content: impl FnOnce(&mut Ui) - inner } +/// Whether a field in the popup on `layer` has the keyboard. A popup closing with one draws once +/// more before it goes: the field lets go of the keyboard to the click away and commits what was +/// typed, as it does in a panel. A popup that closed without drawing again dropped it (#6429). +pub(crate) fn field_focused_in(ctx: &egui::Context, layer: egui::LayerId) -> bool { + ctx.memory(|m| m.focused()).and_then(|id| ctx.read_response(id)).is_some_and(|r| r.layer_id == layer) +} + /// Whether the popover for this anchor was open in the latest completed frame. pub(crate) fn popover_is_open(ctx: &egui::Context, anchor: egui::Id) -> bool { let frame = ctx.cumulative_frame_nr();