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 {...@@ -1119,8 +1119,8 @@ impl State {
1119 | canvas::interaction::ObjectFocus::File(_)1119 | canvas::interaction::ObjectFocus::File(_)
1120 )1120 )
1121 );1121 );
1122 // Edit commands act on a focused field instead of the page.1122 // Palette commands act after its search field returns focus to the editor.
1123 let field = self.ui.focused_field().is_some();1123 let field = self.ui.focused_field(Some(crate::palette::id())).is_some();
1124 let [anchor, focus] = self.view.editor.selection().positions;1124 let [anchor, focus] = self.view.editor.selection().positions;
1125 let selected = page1125 let selected = page
1126 && !field1126 && !field
...@@ -1465,7 +1465,7 @@ impl State {...@@ -1465,7 +1465,7 @@ impl State {
1465 return Ok(());1465 return Ok(());
1466 }1466 }
1467 };1467 };
1468 let field = self.ui.focused_field();1468 let field = self.ui.focused_field(None);
1469 let response = match id {1469 let response = match id {
1470 Id::Settings => {1470 Id::Settings => {
1471 self.open_options();1471 self.open_options();
crates/ui/src/lib.rs+6-3
...@@ -869,9 +869,12 @@ impl Ui {...@@ -869,9 +869,12 @@ impl Ui {
869 self.focus869 self.focus
870 }870 }
871871
872 /// The focus where it is a text field, which the keyboard's edits act on.872 /// The focused text field, or the field `popup` will return focus to when it closes.
873 pub fn focused_field(&self) -> Option<Id> {873 pub fn focused_field(&self, popup: Option<Id>) -> Option<Id> {
874 self.focus.filter(|focus| {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| {
875 let flags = self.hit_flags(*focus);878 let flags = self.hit_flags(*focus);
876 flags.contains(Flags::FOCUSABLE) && !flags.contains(Flags::CUSTOM)879 flags.contains(Flags::FOCUSABLE) && !flags.contains(Flags::CUSTOM)
877 })880 })
crates/ui/src/tests.rs+53
...@@ -1638,6 +1638,59 @@ fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {...@@ -1638,6 +1638,59 @@ fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {
1638 assert!(!ui.popup_open(menu_id()));1638 assert!(!ui.popup_open(menu_id()));
1639}1639}
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
1641#[test]1694#[test]
1642fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {1695fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {
1643 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);1696 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);