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}