Skip to main content

tui/
focus.rs

1use crossterm::event::{KeyCode, KeyEvent};
2
3#[doc = include_str!("docs/focus_ring.md")]
4pub struct FocusRing {
5    focused: usize,
6    len: usize,
7    wrap: bool,
8}
9
10/// The result of [`FocusRing::handle_key`].
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FocusOutcome {
13    /// Focus moved to a different index.
14    FocusChanged,
15    /// The key was recognized (Tab/BackTab) but focus didn't move (e.g. at boundary without wrap).
16    Unchanged,
17    /// The key was not a focus-navigation key and was ignored.
18    Ignored,
19}
20
21impl FocusRing {
22    /// Create a new `FocusRing` with wrapping enabled.
23    ///
24    /// Focus starts at index 0. If `len` is 0, all navigation is a no-op.
25    pub fn new(len: usize) -> Self {
26        Self { focused: 0, len, wrap: true }
27    }
28
29    /// Disable wrap-around: `focus_next` at the last item and `focus_prev` at
30    /// the first item will not cycle.
31    pub fn without_wrap(mut self) -> Self {
32        self.wrap = false;
33        self
34    }
35
36    /// The currently focused index.
37    pub fn focused(&self) -> usize {
38        self.focused
39    }
40
41    /// Returns `true` if `index` is the currently focused index.
42    pub fn is_focused(&self, index: usize) -> bool {
43        self.focused == index
44    }
45
46    /// The number of focusable items.
47    pub fn len(&self) -> usize {
48        self.len
49    }
50
51    /// Returns `true` if there are no focusable items.
52    pub fn is_empty(&self) -> bool {
53        self.len == 0
54    }
55
56    /// Update the number of focusable items. Clamps `focused` if it would be
57    /// out of bounds.
58    pub fn set_len(&mut self, len: usize) {
59        self.len = len;
60        if len == 0 {
61            self.focused = 0;
62        } else if self.focused >= len {
63            self.focused = len - 1;
64        }
65    }
66
67    /// Programmatically set focus to `index`. Returns `false` if `index` is
68    /// out of bounds (focus unchanged).
69    pub fn focus(&mut self, index: usize) -> bool {
70        if index < self.len {
71            self.focused = index;
72            true
73        } else {
74            false
75        }
76    }
77
78    /// Move focus to the next item. Returns `true` if focus changed.
79    pub fn focus_next(&mut self) -> bool {
80        if self.len == 0 {
81            return false;
82        }
83        if self.focused + 1 < self.len {
84            self.focused += 1;
85            true
86        } else if self.wrap {
87            self.focused = 0;
88            true
89        } else {
90            false
91        }
92    }
93
94    /// Move focus to the previous item. Returns `true` if focus changed.
95    pub fn focus_prev(&mut self) -> bool {
96        if self.len == 0 {
97            return false;
98        }
99        if self.focused > 0 {
100            self.focused -= 1;
101            true
102        } else if self.wrap {
103            self.focused = self.len - 1;
104            true
105        } else {
106            false
107        }
108    }
109
110    /// Handle Tab (next) and `BackTab` (previous) key events.
111    ///
112    /// Returns [`FocusOutcome::FocusChanged`] if focus moved,
113    /// [`FocusOutcome::Unchanged`] if a focus key was pressed but focus didn't
114    /// move, or [`FocusOutcome::Ignored`] for all other keys.
115    pub fn handle_key(&mut self, key_event: KeyEvent) -> FocusOutcome {
116        match key_event.code {
117            KeyCode::Tab => {
118                if self.focus_next() {
119                    FocusOutcome::FocusChanged
120                } else {
121                    FocusOutcome::Unchanged
122                }
123            }
124            KeyCode::BackTab => {
125                if self.focus_prev() {
126                    FocusOutcome::FocusChanged
127                } else {
128                    FocusOutcome::Unchanged
129                }
130            }
131            _ => FocusOutcome::Ignored,
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
140
141    fn key(code: KeyCode) -> KeyEvent {
142        KeyEvent { code, modifiers: KeyModifiers::empty(), kind: KeyEventKind::Press, state: KeyEventState::empty() }
143    }
144
145    #[test]
146    fn new_starts_at_zero() {
147        let ring = FocusRing::new(3);
148        assert_eq!(ring.focused(), 0);
149        assert!(ring.is_focused(0));
150        assert!(!ring.is_focused(1));
151        assert_eq!(ring.len(), 3);
152        assert!(!ring.is_empty());
153    }
154
155    #[test]
156    fn cycle_forward_wraps() {
157        let mut ring = FocusRing::new(3);
158        assert!(ring.focus_next());
159        assert_eq!(ring.focused(), 1);
160        assert!(ring.focus_next());
161        assert_eq!(ring.focused(), 2);
162        assert!(ring.focus_next());
163        assert_eq!(ring.focused(), 0); // wrapped
164    }
165
166    #[test]
167    fn cycle_backward_wraps() {
168        let mut ring = FocusRing::new(3);
169        assert!(ring.focus_prev());
170        assert_eq!(ring.focused(), 2); // wrapped to end
171        assert!(ring.focus_prev());
172        assert_eq!(ring.focused(), 1);
173        assert!(ring.focus_prev());
174        assert_eq!(ring.focused(), 0);
175    }
176
177    #[test]
178    fn no_wrap_stops_at_boundaries() {
179        let mut ring = FocusRing::new(3).without_wrap();
180
181        // At start, can't go prev
182        assert!(!ring.focus_prev());
183        assert_eq!(ring.focused(), 0);
184
185        // Go to end
186        assert!(ring.focus_next());
187        assert!(ring.focus_next());
188        assert_eq!(ring.focused(), 2);
189
190        // At end, can't go next
191        assert!(!ring.focus_next());
192        assert_eq!(ring.focused(), 2);
193    }
194
195    #[test]
196    fn empty_ring_is_noop() {
197        let mut ring = FocusRing::new(0);
198        assert!(ring.is_empty());
199        assert_eq!(ring.focused(), 0);
200        assert!(!ring.focus_next());
201        assert!(!ring.focus_prev());
202        assert!(!ring.focus(0));
203    }
204
205    #[test]
206    fn programmatic_focus() {
207        let mut ring = FocusRing::new(5);
208        assert!(ring.focus(3));
209        assert_eq!(ring.focused(), 3);
210        assert!(ring.is_focused(3));
211
212        // Out of bounds
213        assert!(!ring.focus(5));
214        assert_eq!(ring.focused(), 3); // unchanged
215    }
216
217    #[test]
218    fn set_len_clamps_focused() {
219        let mut ring = FocusRing::new(5);
220        ring.focus(4);
221        assert_eq!(ring.focused(), 4);
222
223        ring.set_len(3);
224        assert_eq!(ring.len(), 3);
225        assert_eq!(ring.focused(), 2); // clamped
226
227        ring.set_len(0);
228        assert_eq!(ring.focused(), 0);
229        assert!(ring.is_empty());
230    }
231
232    #[test]
233    fn set_len_preserves_focused_when_in_range() {
234        let mut ring = FocusRing::new(5);
235        ring.focus(2);
236        ring.set_len(4);
237        assert_eq!(ring.focused(), 2); // still valid
238    }
239
240    #[test]
241    fn handle_key_tab_cycles_forward() {
242        let mut ring = FocusRing::new(3);
243        assert_eq!(ring.handle_key(key(KeyCode::Tab)), FocusOutcome::FocusChanged);
244        assert_eq!(ring.focused(), 1);
245    }
246
247    #[test]
248    fn handle_key_backtab_cycles_backward() {
249        let mut ring = FocusRing::new(3);
250        ring.focus(1);
251        assert_eq!(ring.handle_key(key(KeyCode::BackTab)), FocusOutcome::FocusChanged);
252        assert_eq!(ring.focused(), 0);
253    }
254
255    #[test]
256    fn handle_key_other_keys_ignored() {
257        let mut ring = FocusRing::new(3);
258        assert_eq!(ring.handle_key(key(KeyCode::Enter)), FocusOutcome::Ignored);
259        assert_eq!(ring.handle_key(key(KeyCode::Char('a'))), FocusOutcome::Ignored);
260        assert_eq!(ring.focused(), 0); // unchanged
261    }
262
263    #[test]
264    fn handle_key_no_wrap_returns_unchanged() {
265        let mut ring = FocusRing::new(2).without_wrap();
266        // At index 0, BackTab can't go further
267        assert_eq!(ring.handle_key(key(KeyCode::BackTab)), FocusOutcome::Unchanged);
268        assert_eq!(ring.focused(), 0);
269
270        // Go to end
271        ring.focus(1);
272        assert_eq!(ring.handle_key(key(KeyCode::Tab)), FocusOutcome::Unchanged);
273        assert_eq!(ring.focused(), 1);
274    }
275
276    #[test]
277    fn single_item_wrap_returns_true() {
278        // With wrap enabled and len=1, focus_next wraps to 0 (same index).
279        // This still "changed" in the sense that the cycle completed.
280        let mut ring = FocusRing::new(1);
281        assert!(ring.focus_next());
282        assert_eq!(ring.focused(), 0);
283        assert!(ring.focus_prev());
284        assert_eq!(ring.focused(), 0);
285    }
286
287    #[test]
288    fn single_item_no_wrap() {
289        let mut ring = FocusRing::new(1).without_wrap();
290        assert!(!ring.focus_next());
291        assert!(!ring.focus_prev());
292        assert_eq!(ring.focused(), 0);
293    }
294}