ratatui_interact/components/
checkbox.rs1use ratatui::{
21 buffer::Buffer,
22 layout::Rect,
23 style::{Color, Modifier, Style},
24 text::{Line, Span},
25 widgets::{Paragraph, Widget},
26};
27
28use crate::traits::{ClickRegion, FocusId};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum CheckBoxAction {
33 Toggle,
35}
36
37#[derive(Debug, Clone)]
39pub struct CheckBoxState {
40 pub checked: bool,
42 pub focused: bool,
44 pub enabled: bool,
46}
47
48impl Default for CheckBoxState {
49 fn default() -> Self {
50 Self {
51 checked: false,
52 focused: false,
53 enabled: true,
54 }
55 }
56}
57
58impl CheckBoxState {
59 pub fn new(checked: bool) -> Self {
65 Self {
66 checked,
67 ..Default::default()
68 }
69 }
70
71 pub fn toggle(&mut self) {
75 if self.enabled {
76 self.checked = !self.checked;
77 }
78 }
79
80 pub fn set_checked(&mut self, checked: bool) {
82 if self.enabled {
83 self.checked = checked;
84 }
85 }
86
87 pub fn set_focused(&mut self, focused: bool) {
89 self.focused = focused;
90 }
91
92 pub fn set_enabled(&mut self, enabled: bool) {
94 self.enabled = enabled;
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct CheckBoxStyle {
101 pub checked_symbol: &'static str,
103 pub unchecked_symbol: &'static str,
105 pub focused_fg: Color,
107 pub unfocused_fg: Color,
109 pub disabled_fg: Color,
111 pub checked_fg: Color,
113}
114
115impl Default for CheckBoxStyle {
116 fn default() -> Self {
117 Self {
118 checked_symbol: "[x]",
119 unchecked_symbol: "[ ]",
120 focused_fg: Color::Yellow,
121 unfocused_fg: Color::White,
122 disabled_fg: Color::DarkGray,
123 checked_fg: Color::Green,
124 }
125 }
126}
127
128impl CheckBoxStyle {
129 pub fn ascii() -> Self {
131 Self::default()
132 }
133
134 pub fn unicode() -> Self {
136 Self {
137 checked_symbol: "☑",
138 unchecked_symbol: "☐",
139 ..Default::default()
140 }
141 }
142
143 pub fn checkmark() -> Self {
145 Self {
146 checked_symbol: "✓",
147 unchecked_symbol: "○",
148 ..Default::default()
149 }
150 }
151
152 pub fn custom(checked: &'static str, unchecked: &'static str) -> Self {
154 Self {
155 checked_symbol: checked,
156 unchecked_symbol: unchecked,
157 ..Default::default()
158 }
159 }
160
161 pub fn focused_fg(mut self, color: Color) -> Self {
163 self.focused_fg = color;
164 self
165 }
166
167 pub fn unfocused_fg(mut self, color: Color) -> Self {
169 self.unfocused_fg = color;
170 self
171 }
172
173 pub fn disabled_fg(mut self, color: Color) -> Self {
175 self.disabled_fg = color;
176 self
177 }
178
179 pub fn checked_fg(mut self, color: Color) -> Self {
181 self.checked_fg = color;
182 self
183 }
184}
185
186pub struct CheckBox<'a> {
191 label: &'a str,
192 state: &'a CheckBoxState,
193 style: CheckBoxStyle,
194 focus_id: FocusId,
195}
196
197impl<'a> CheckBox<'a> {
198 pub fn new(label: &'a str, state: &'a CheckBoxState) -> Self {
205 Self {
206 label,
207 state,
208 style: CheckBoxStyle::default(),
209 focus_id: FocusId::default(),
210 }
211 }
212
213 pub fn style(mut self, style: CheckBoxStyle) -> Self {
215 self.style = style;
216 self
217 }
218
219 pub fn focus_id(mut self, id: FocusId) -> Self {
221 self.focus_id = id;
222 self
223 }
224
225 fn build_line(&self) -> Line<'a> {
227 let symbol = if self.state.checked {
228 self.style.checked_symbol
229 } else {
230 self.style.unchecked_symbol
231 };
232
233 let fg_color = if !self.state.enabled {
234 self.style.disabled_fg
235 } else if self.state.focused {
236 self.style.focused_fg
237 } else if self.state.checked {
238 self.style.checked_fg
239 } else {
240 self.style.unfocused_fg
241 };
242
243 let mut style = Style::default().fg(fg_color);
244 if self.state.focused && self.state.enabled {
245 style = style.add_modifier(Modifier::BOLD);
246 }
247
248 Line::from(vec![
249 Span::styled(symbol, style),
250 Span::styled(" ", style),
251 Span::styled(self.label, style),
252 ])
253 }
254
255 pub fn width(&self) -> u16 {
257 let symbol_len = if self.state.checked {
258 self.style.checked_symbol.chars().count()
259 } else {
260 self.style.unchecked_symbol.chars().count()
261 };
262 (symbol_len + 1 + self.label.chars().count()) as u16
263 }
264
265 pub fn render_stateful(self, area: Rect, buf: &mut Buffer) -> ClickRegion<CheckBoxAction> {
269 let width = self.width().min(area.width);
270 let click_area = Rect::new(area.x, area.y, width, 1);
271
272 let line = self.build_line();
273 let paragraph = Paragraph::new(line);
274 paragraph.render(area, buf);
275
276 ClickRegion::new(click_area, CheckBoxAction::Toggle)
277 }
278}
279
280impl Widget for CheckBox<'_> {
281 fn render(self, area: Rect, buf: &mut Buffer) {
282 let line = self.build_line();
283 let paragraph = Paragraph::new(line);
284 paragraph.render(area, buf);
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn test_state_default() {
294 let state = CheckBoxState::default();
295 assert!(!state.checked);
296 assert!(!state.focused);
297 assert!(state.enabled);
298 }
299
300 #[test]
301 fn test_state_new() {
302 let state = CheckBoxState::new(true);
303 assert!(state.checked);
304 assert!(!state.focused);
305 assert!(state.enabled);
306 }
307
308 #[test]
309 fn test_toggle() {
310 let mut state = CheckBoxState::new(false);
311 assert!(!state.checked);
312
313 state.toggle();
314 assert!(state.checked);
315
316 state.toggle();
317 assert!(!state.checked);
318 }
319
320 #[test]
321 fn test_toggle_disabled() {
322 let mut state = CheckBoxState::new(false);
323 state.enabled = false;
324
325 state.toggle();
326 assert!(!state.checked); }
328
329 #[test]
330 fn test_set_checked() {
331 let mut state = CheckBoxState::new(false);
332
333 state.set_checked(true);
334 assert!(state.checked);
335
336 state.set_checked(false);
337 assert!(!state.checked);
338 }
339
340 #[test]
341 fn test_set_checked_disabled() {
342 let mut state = CheckBoxState::new(false);
343 state.enabled = false;
344
345 state.set_checked(true);
346 assert!(!state.checked); }
348
349 #[test]
350 fn test_style_default() {
351 let style = CheckBoxStyle::default();
352 assert_eq!(style.checked_symbol, "[x]");
353 assert_eq!(style.unchecked_symbol, "[ ]");
354 }
355
356 #[test]
357 fn test_style_unicode() {
358 let style = CheckBoxStyle::unicode();
359 assert_eq!(style.checked_symbol, "☑");
360 assert_eq!(style.unchecked_symbol, "☐");
361 }
362
363 #[test]
364 fn test_style_checkmark() {
365 let style = CheckBoxStyle::checkmark();
366 assert_eq!(style.checked_symbol, "✓");
367 assert_eq!(style.unchecked_symbol, "○");
368 }
369
370 #[test]
371 fn test_style_custom() {
372 let style = CheckBoxStyle::custom("ON", "OFF");
373 assert_eq!(style.checked_symbol, "ON");
374 assert_eq!(style.unchecked_symbol, "OFF");
375 }
376
377 #[test]
378 fn test_checkbox_width() {
379 let state = CheckBoxState::new(false);
380 let checkbox = CheckBox::new("Test", &state);
381
382 assert_eq!(checkbox.width(), 8);
384 }
385
386 #[test]
387 fn test_checkbox_width_unicode() {
388 let state = CheckBoxState::new(true);
389 let checkbox = CheckBox::new("Test", &state).style(CheckBoxStyle::unicode());
390
391 assert_eq!(checkbox.width(), 6);
393 }
394
395 #[test]
396 fn test_render_basic() {
397 let state = CheckBoxState::new(true);
398 let checkbox = CheckBox::new("Test", &state);
399
400 let area = Rect::new(0, 0, 20, 1);
401 let mut buffer = Buffer::empty(area);
402
403 checkbox.render(area, &mut buffer);
404
405 let content: String = (0..8)
407 .map(|x| buffer[(x, 0)].symbol().to_string())
408 .collect();
409 assert!(content.contains("[x]"));
410 }
411
412 #[test]
413 fn test_render_stateful() {
414 let state = CheckBoxState::new(false);
415 let checkbox = CheckBox::new("Click me", &state);
416
417 let area = Rect::new(5, 3, 20, 1);
418 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 10));
419
420 let click_region = checkbox.render_stateful(area, &mut buffer);
421
422 assert_eq!(click_region.area.x, 5);
424 assert_eq!(click_region.area.y, 3);
425 assert_eq!(click_region.data, CheckBoxAction::Toggle);
426 }
427
428 #[test]
429 fn test_click_region_detection() {
430 let state = CheckBoxState::new(false);
431 let checkbox = CheckBox::new("Test", &state);
432
433 let area = Rect::new(10, 5, 20, 1);
434 let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 10));
435
436 let click_region = checkbox.render_stateful(area, &mut buffer);
437
438 assert!(click_region.contains(10, 5));
440 assert!(click_region.contains(15, 5));
441
442 assert!(!click_region.contains(9, 5));
444 assert!(!click_region.contains(10, 4));
445 assert!(!click_region.contains(10, 6));
446 }
447}