photon_ui/components/
tabs.rs1use crossterm::event::KeyCode;
2
3use crate::{
4 Component,
5 Event,
6 Focusable,
7 InputResult,
8 RenderError,
9 Rendered,
10 theme::{
11 Palette,
12 Style,
13 Theme,
14 stylize,
15 },
16};
17
18pub struct Tabs {
24 items: Vec<String>,
25 active: usize,
26 focused: bool,
27}
28
29impl Tabs {
30 pub fn new(items: Vec<impl Into<String>>) -> Self {
32 Self {
33 items: items.into_iter().map(Into::into).collect(),
34 active: 0,
35 focused: false,
36 }
37 }
38
39 pub fn active(&self) -> usize {
41 self.active
42 }
43
44 pub fn set_active(&mut self, index: usize) {
46 self.active = index.min(self.items.len().saturating_sub(1));
47 }
48}
49
50impl Focusable for Tabs {
51 fn focused(&self) -> bool {
52 self.focused
53 }
54
55 fn set_focused(&mut self, focused: bool) {
56 self.focused = focused;
57 }
58}
59
60impl Component for Tabs {
61 fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
62 let theme = Theme::current();
63 let accent_style = Style::new().fg(theme.accent()).bold();
64 let inactive_style = Style::new().fg(theme.text_secondary());
65
66 let mut line = String::new();
67 if self.focused {
68 line.push('│');
69 line.push(' ');
70 }
71
72 for (i, item) in self.items.iter().enumerate() {
73 if i == self.active {
74 let text = format!(" [{}] ", item);
75 line.push_str(&stylize(&text, &accent_style));
76 } else {
77 let text = format!(" {} ", item);
78 line.push_str(&stylize(&text, &inactive_style));
79 }
80 }
81
82 Ok(Rendered {
83 lines: vec![line],
84 cursor: None,
85 images: Vec::new(),
86 })
87 }
88
89 fn handle_input(&mut self, event: &Event) -> InputResult {
90 use crossterm::event::KeyModifiers;
91 if let Event::Key(key) = event {
92 match key.code {
93 | KeyCode::Right => {
94 if self.active + 1 < self.items.len() {
95 self.active += 1;
96 }
97 InputResult::Handled
98 },
99 | KeyCode::Left => {
100 if self.active > 0 {
101 self.active -= 1;
102 }
103 InputResult::Handled
104 },
105 | KeyCode::Char('l') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
106 if self.active + 1 < self.items.len() {
107 self.active += 1;
108 }
109 InputResult::Handled
110 },
111 | KeyCode::Char('h') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
112 if self.active > 0 {
113 self.active -= 1;
114 }
115 InputResult::Handled
116 },
117 | _ => InputResult::Ignored,
118 }
119 } else {
120 InputResult::Ignored
121 }
122 }
123
124 fn as_focusable(&self) -> Option<&dyn Focusable> {
125 Some(self)
126 }
127
128 fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
129 Some(self)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use crossterm::event::KeyCode;
136
137 use super::*;
138
139 #[test]
140 fn tabs_new() {
141 let tabs = Tabs::new(vec!["a", "b", "c"]);
142 assert_eq!(tabs.active(), 0);
143 assert_eq!(tabs.items.len(), 3);
144 }
145
146 #[test]
147 fn tabs_set_active() {
148 let mut tabs = Tabs::new(vec!["a", "b", "c"]);
149 tabs.set_active(1);
150 assert_eq!(tabs.active(), 1);
151 tabs.set_active(10);
152 assert_eq!(tabs.active(), 2);
153 }
154
155 #[test]
156 fn tabs_focusable() {
157 let mut tabs = Tabs::new(vec!["a", "b"]);
158 assert!(!tabs.focused());
159 tabs.set_focused(true);
160 assert!(tabs.focused());
161 }
162
163 #[test]
164 fn tabs_render_unfocused() {
165 Theme::with(Theme::Light, || {
166 let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]);
167 let rendered = tabs.render(80).unwrap();
168 assert_eq!(rendered.lines.len(), 1);
169 assert!(!rendered.lines[0].starts_with('│'));
170 });
171 }
172
173 #[test]
174 fn tabs_render_focused() {
175 Theme::with(Theme::Light, || {
176 let mut tabs = Tabs::new(vec!["Tab 1", "Tab 2"]);
177 tabs.set_focused(true);
178 let rendered = tabs.render(80).unwrap();
179 assert!(rendered.lines[0].starts_with('│'));
180 });
181 }
182
183 #[test]
184 fn tabs_handle_input_right() {
185 let mut tabs = Tabs::new(vec!["a", "b", "c"]);
186 tabs.set_focused(true);
187 let result = tabs.handle_input(&Event::Key(KeyCode::Right.into()));
188 assert_eq!(result, InputResult::Handled);
189 assert_eq!(tabs.active(), 1);
190 }
191
192 #[test]
193 fn tabs_handle_input_left() {
194 let mut tabs = Tabs::new(vec!["a", "b", "c"]);
195 tabs.set_focused(true);
196 tabs.set_active(2);
197 let result = tabs.handle_input(&Event::Key(KeyCode::Left.into()));
198 assert_eq!(result, InputResult::Handled);
199 assert_eq!(tabs.active(), 1);
200 }
201
202 #[test]
203 fn tabs_handle_input_h_l() {
204 let mut tabs = Tabs::new(vec!["a", "b", "c"]);
205 tabs.set_focused(true);
206 tabs.set_active(1);
207 let result = tabs.handle_input(&Event::Key(KeyCode::Char('h').into()));
208 assert_eq!(result, InputResult::Handled);
209 assert_eq!(tabs.active(), 0);
210
211 let result = tabs.handle_input(&Event::Key(KeyCode::Char('l').into()));
212 assert_eq!(result, InputResult::Handled);
213 assert_eq!(tabs.active(), 1);
214 }
215
216 #[test]
217 fn tabs_handle_input_clamps() {
218 let mut tabs = Tabs::new(vec!["a", "b"]);
219 tabs.set_focused(true);
220 tabs.set_active(1);
221 let result = tabs.handle_input(&Event::Key(KeyCode::Right.into()));
222 assert_eq!(result, InputResult::Handled);
223 assert_eq!(tabs.active(), 1); tabs.set_active(0);
226 let result = tabs.handle_input(&Event::Key(KeyCode::Left.into()));
227 assert_eq!(result, InputResult::Handled);
228 assert_eq!(tabs.active(), 0); }
230}