Skip to main content

ratatui_interact/traits/
focusable.rs

1//! Focusable trait for keyboard navigation
2//!
3//! Components implementing this trait can receive keyboard focus
4//! and participate in Tab navigation.
5//!
6//! # Example
7//!
8//! ```rust
9//! use ratatui_interact::traits::{FocusId, Focusable};
10//! use ratatui::style::{Color, Modifier, Style};
11//!
12//! struct MyWidget {
13//!     focus_id: FocusId,
14//!     focused: bool,
15//! }
16//!
17//! impl Focusable for MyWidget {
18//!     fn focus_id(&self) -> FocusId {
19//!         self.focus_id
20//!     }
21//!
22//!     fn is_focused(&self) -> bool {
23//!         self.focused
24//!     }
25//!
26//!     fn set_focused(&mut self, focused: bool) {
27//!         self.focused = focused;
28//!     }
29//!
30//!     fn focused_style(&self) -> Style {
31//!         Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
32//!     }
33//!
34//!     fn unfocused_style(&self) -> Style {
35//!         Style::default().fg(Color::Gray)
36//!     }
37//! }
38//! ```
39
40use ratatui::style::{Color, Modifier, Style};
41
42/// A unique identifier for focusable elements.
43///
44/// Used to track which element has focus and for Tab navigation ordering.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
46pub struct FocusId(pub u32);
47
48impl FocusId {
49    /// Create a new focus ID with the given value.
50    pub fn new(id: u32) -> Self {
51        Self(id)
52    }
53
54    /// Get the inner ID value.
55    pub fn id(&self) -> u32 {
56        self.0
57    }
58}
59
60impl From<u32> for FocusId {
61    fn from(id: u32) -> Self {
62        Self(id)
63    }
64}
65
66impl From<usize> for FocusId {
67    fn from(id: usize) -> Self {
68        Self(id as u32)
69    }
70}
71
72/// Trait for components that can receive keyboard focus.
73///
74/// Components implementing this trait can:
75/// - Receive and lose focus
76/// - Provide different styles for focused/unfocused states
77/// - Participate in Tab navigation with tab ordering
78/// - Be conditionally focusable (enabled/disabled state)
79pub trait Focusable {
80    /// Returns the unique focus ID for this component.
81    fn focus_id(&self) -> FocusId;
82
83    /// Returns true if this component currently has focus.
84    fn is_focused(&self) -> bool;
85
86    /// Set the focus state of this component.
87    fn set_focused(&mut self, focused: bool);
88
89    /// Returns the style to use when this component has focus.
90    ///
91    /// Default implementation returns yellow foreground with bold modifier.
92    fn focused_style(&self) -> Style {
93        Style::default()
94            .fg(Color::Yellow)
95            .add_modifier(Modifier::BOLD)
96    }
97
98    /// Returns the style to use when this component does not have focus.
99    ///
100    /// Default implementation returns white foreground.
101    fn unfocused_style(&self) -> Style {
102        Style::default().fg(Color::White)
103    }
104
105    /// Returns the current style based on focus state.
106    ///
107    /// This is a convenience method that returns `focused_style()` if focused,
108    /// otherwise `unfocused_style()`.
109    fn current_style(&self) -> Style {
110        if self.is_focused() {
111            self.focused_style()
112        } else {
113            self.unfocused_style()
114        }
115    }
116
117    /// Whether this component can currently receive focus.
118    ///
119    /// Return `false` for disabled components that should be skipped
120    /// during Tab navigation.
121    ///
122    /// Default implementation returns `true`.
123    fn can_focus(&self) -> bool {
124        true
125    }
126
127    /// Tab order index for this component.
128    ///
129    /// Lower values come earlier in Tab navigation order.
130    /// Components with the same tab order are navigated in registration order.
131    ///
132    /// Default implementation returns `0`.
133    fn tab_order(&self) -> u32 {
134        0
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    struct TestWidget {
143        focus_id: FocusId,
144        focused: bool,
145        enabled: bool,
146        tab_order: u32,
147    }
148
149    impl TestWidget {
150        fn new(id: u32) -> Self {
151            Self {
152                focus_id: FocusId::new(id),
153                focused: false,
154                enabled: true,
155                tab_order: 0,
156            }
157        }
158    }
159
160    impl Focusable for TestWidget {
161        fn focus_id(&self) -> FocusId {
162            self.focus_id
163        }
164
165        fn is_focused(&self) -> bool {
166            self.focused
167        }
168
169        fn set_focused(&mut self, focused: bool) {
170            self.focused = focused;
171        }
172
173        fn can_focus(&self) -> bool {
174            self.enabled
175        }
176
177        fn tab_order(&self) -> u32 {
178            self.tab_order
179        }
180    }
181
182    #[test]
183    fn test_focus_id_creation() {
184        let id = FocusId::new(42);
185        assert_eq!(id.id(), 42);
186
187        let id_from_u32: FocusId = 100u32.into();
188        assert_eq!(id_from_u32.id(), 100);
189
190        let id_from_usize: FocusId = 200usize.into();
191        assert_eq!(id_from_usize.id(), 200);
192    }
193
194    #[test]
195    fn test_focus_state() {
196        let mut widget = TestWidget::new(1);
197        assert!(!widget.is_focused());
198
199        widget.set_focused(true);
200        assert!(widget.is_focused());
201
202        widget.set_focused(false);
203        assert!(!widget.is_focused());
204    }
205
206    #[test]
207    fn test_current_style() {
208        let mut widget = TestWidget::new(1);
209
210        // Unfocused style
211        let style = widget.current_style();
212        assert_eq!(style, widget.unfocused_style());
213
214        // Focused style
215        widget.set_focused(true);
216        let style = widget.current_style();
217        assert_eq!(style, widget.focused_style());
218    }
219
220    #[test]
221    fn test_can_focus() {
222        let mut widget = TestWidget::new(1);
223        assert!(widget.can_focus());
224
225        widget.enabled = false;
226        assert!(!widget.can_focus());
227    }
228
229    #[test]
230    fn test_tab_order() {
231        let mut widget = TestWidget::new(1);
232        assert_eq!(widget.tab_order(), 0);
233
234        widget.tab_order = 5;
235        assert_eq!(widget.tab_order(), 5);
236    }
237}