| author | |
| committer | |
| log | 568f5f7053dedb5040949c5833f1d2b0023d1f57 |
| tree | 720526176a1f997ad13838bb5964ac2463e68a31 |
| parent | 056d1cf622d6bb20abd1376d48307d0506f94f39 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
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-sol3 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 = page | 1125 | let selected = page |
| 1126 | && !field | 1126 | && !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.focus | 869 | self.focus |
| 870 | } | 870 | } |
| 871 | 871 | ||
| 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 | } |
| 1640 | 1640 | ||
| 1641 | #[test] | ||
| 1642 | fn 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] |
| 1642 | fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() { | 1695 | fn 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); |