authorclover caruso <git@paperclover.net> 2026-10-10 20:43:15-07:00
committerclover caruso <git@paperclover.net> 2026-10-10 22:56:06-07:00
log568f5f7053dedb5040949c5833f1d2b0023d1f57
tree720526176a1f997ad13838bb5964ac2463e68a31
parent056d1cf622d6bb20abd1376d48307d0506f94f39
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

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

3 files changed, 62 insertions(+), 6 deletions(-)

crates/snowbound/src/commands.rs+3-3
......@@ -1119,8 +1119,8 @@ impl State {
11191119 | canvas::interaction::ObjectFocus::File(_)
11201120 )
11211121 );
1122 // Edit commands act on a focused field instead of the page.
1123 let field = self.ui.focused_field().is_some();
1122 // Palette commands act after its search field returns focus to the editor.
1123 let field = self.ui.focused_field(Some(crate::palette::id())).is_some();
11241124 let [anchor, focus] = self.view.editor.selection().positions;
11251125 let selected = page
11261126 && !field
......@@ -1465,7 +1465,7 @@ impl State {
14651465 return Ok(());
14661466 }
14671467 };
1468 let field = self.ui.focused_field();
1468 let field = self.ui.focused_field(None);
14691469 let response = match id {
14701470 Id::Settings => {
14711471 self.open_options();
crates/ui/src/lib.rs+6-3
......@@ -869,9 +869,12 @@ impl Ui {
869869 self.focus
870870 }
871871
872 /// The focus where it is a text field, which the keyboard's edits act on.
873 pub fn focused_field(&self) -> Option<Id> {
874 self.focus.filter(|focus| {
872 /// The focused text field, or the field `popup` will return focus to when it closes.
873 pub fn focused_field(&self, popup: Option<Id>) -> Option<Id> {
874 let focus = popup
875 .and_then(|id| self.popups.iter().find(|popup| popup.id == id))
876 .map_or(self.focus, |popup| popup.focus);
877 focus.filter(|focus| {
875878 let flags = self.hit_flags(*focus);
876879 flags.contains(Flags::FOCUSABLE) && !flags.contains(Flags::CUSTOM)
877880 })
crates/ui/src/tests.rs+53
......@@ -1638,6 +1638,59 @@ fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {
16381638 assert!(!ui.popup_open(menu_id()));
16391639}
16401640
1641#[test]
1642fn palette_commands_keep_the_origin_field_while_the_query_keeps_typing_focus() {
1643 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1644 let palette = Id::ROOT.child("palette");
1645 let page = Id::ROOT.child("page");
1646 let commands = items();
1647 let mut text = "kept".to_owned();
1648 let mut build = |ui: &mut Ui| {
1649 frame(ui, |ui| {
1650 text_field(
1651 ui,
1652 field(),
1653 &mut text,
1654 "Filter",
1655 Spec {
1656 size: [px(200.0), px(26.0)],
1657 ..Spec::default()
1658 },
1659 );
1660 ui.leaf(
1661 "page",
1662 Spec {
1663 size: [px(200.0), px(100.0)],
1664 flags: Flags::FOCUSABLE | Flags::CUSTOM,
1665 ..Spec::default()
1666 },
1667 );
1668 let _ = popup::palette(ui, palette, &[("", &commands)], "Search commands");
1669 });
1670 };
1671 build(&mut ui);
1672 for origin in [page, field()] {
1673 ui.set_focus(Some(origin));
1674 let expected = (origin == field()).then_some(origin);
1675 assert_eq!(ui.focused_field(Some(palette)), expected);
1676 ui.open_popup(palette);
1677 build(&mut ui);
1678 let query = palette.child("filter");
1679 assert_eq!(ui.focused_field(None), Some(query));
1680 assert_eq!(ui.focused_field(Some(palette)), expected);
1681 assert_eq!(ui.focused(), Some(query));
1682 ui.event(typed("Undo"));
1683 build(&mut ui);
1684 assert_eq!(popup::query(&ui, palette), Some("Undo"));
1685 assert_eq!(ui.focused_field(Some(palette)), expected);
1686 assert_eq!(ui.focused(), Some(query));
1687 ui.close_popup(palette);
1688 assert_eq!(ui.focused(), Some(origin));
1689 assert_eq!(ui.focused_field(None), expected);
1690 }
1691 assert_eq!(text, "kept");
1692}
1693
16411694#[test]
16421695fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {
16431696 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);