From 568f5f7053dedb5040949c5833f1d2b0023d1f57 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 10 Oct 2026 20:43:15 -0700 Subject: [PATCH] fix: keep editor command availability in the palette Determine command availability from the focus the palette will restore, while its query retains typing focus. Keep genuine text-field origins distinct from the page and cover both focus paths in a UI regression. Fixes #115 Assisted-by: gpt-6.1-sol --- crates/snowbound/src/commands.rs | 6 ++-- crates/ui/src/lib.rs | 9 ++++-- crates/ui/src/tests.rs | 53 ++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/crates/snowbound/src/commands.rs b/crates/snowbound/src/commands.rs index 32a72afc0295d66a7ed8e9c31c8035180c70e9b5..1f8cf8366bbb30c456f084a984cd9e13e371e023 100644 --- a/crates/snowbound/src/commands.rs +++ b/crates/snowbound/src/commands.rs @@ -1119,8 +1119,8 @@ impl State { | canvas::interaction::ObjectFocus::File(_) ) ); - // Edit commands act on a focused field instead of the page. - let field = self.ui.focused_field().is_some(); + // Palette commands act after its search field returns focus to the editor. + let field = self.ui.focused_field(Some(crate::palette::id())).is_some(); let [anchor, focus] = self.view.editor.selection().positions; let selected = page && !field @@ -1465,7 +1465,7 @@ impl State { return Ok(()); } }; - let field = self.ui.focused_field(); + let field = self.ui.focused_field(None); let response = match id { Id::Settings => { self.open_options(); diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 372d2809750f25547bbe1d7165af95b33c7da83e..e843981af91881d07938d2987494a23f194d4920 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -869,9 +869,12 @@ impl Ui { self.focus } - /// The focus where it is a text field, which the keyboard's edits act on. - pub fn focused_field(&self) -> Option { - self.focus.filter(|focus| { + /// The focused text field, or the field `popup` will return focus to when it closes. + pub fn focused_field(&self, popup: Option) -> Option { + let focus = popup + .and_then(|id| self.popups.iter().find(|popup| popup.id == id)) + .map_or(self.focus, |popup| popup.focus); + focus.filter(|focus| { let flags = self.hit_flags(*focus); flags.contains(Flags::FOCUSABLE) && !flags.contains(Flags::CUSTOM) }) diff --git a/crates/ui/src/tests.rs b/crates/ui/src/tests.rs index 26eda90514f5fe8c1cc71bcf26246dc6154b5885..81c95c7f6bb8d7ec9ec9f93f94f0eba8965233c4 100644 --- a/crates/ui/src/tests.rs +++ b/crates/ui/src/tests.rs @@ -1638,6 +1638,59 @@ fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() { assert!(!ui.popup_open(menu_id())); } +#[test] +fn palette_commands_keep_the_origin_field_while_the_query_keeps_typing_focus() { + let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); + let palette = Id::ROOT.child("palette"); + let page = Id::ROOT.child("page"); + let commands = items(); + let mut text = "kept".to_owned(); + let mut build = |ui: &mut Ui| { + frame(ui, |ui| { + text_field( + ui, + field(), + &mut text, + "Filter", + Spec { + size: [px(200.0), px(26.0)], + ..Spec::default() + }, + ); + ui.leaf( + "page", + Spec { + size: [px(200.0), px(100.0)], + flags: Flags::FOCUSABLE | Flags::CUSTOM, + ..Spec::default() + }, + ); + let _ = popup::palette(ui, palette, &[("", &commands)], "Search commands"); + }); + }; + build(&mut ui); + for origin in [page, field()] { + ui.set_focus(Some(origin)); + let expected = (origin == field()).then_some(origin); + assert_eq!(ui.focused_field(Some(palette)), expected); + ui.open_popup(palette); + build(&mut ui); + let query = palette.child("filter"); + assert_eq!(ui.focused_field(None), Some(query)); + assert_eq!(ui.focused_field(Some(palette)), expected); + assert_eq!(ui.focused(), Some(query)); + ui.event(typed("Undo")); + build(&mut ui); + assert_eq!(popup::query(&ui, palette), Some("Undo")); + assert_eq!(ui.focused_field(Some(palette)), expected); + assert_eq!(ui.focused(), Some(query)); + ui.close_popup(palette); + assert_eq!(ui.focused(), Some(origin)); + assert_eq!(ui.focused_field(None), expected); + } + assert_eq!(text, "kept"); +} + #[test] fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() { let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); -- 2.54.0