1#![warn(missing_docs)]
47#![warn(clippy::pedantic)]
48
49use std::borrow::Cow;
50
51use ratatui::buffer::Buffer;
52use ratatui::layout::Rect;
53use ratatui::style::{Style, Styled};
54use ratatui::text::{Line, Span};
55use ratatui::widgets::{Block, Widget};
56
57pub mod symbols;
58
59#[expect(clippy::struct_field_names)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
91pub struct Checkbox<'a> {
92 label: Line<'a>,
94 checked: bool,
96 block: Option<Block<'a>>,
98 style: Style,
100 checkbox_style: Style,
102 label_style: Style,
104 checked_symbol: Cow<'a, str>,
106 unchecked_symbol: Cow<'a, str>,
108}
109
110impl Default for Checkbox<'_> {
111 fn default() -> Self {
128 Self {
129 label: Line::default(),
130 checked: false,
131 block: None,
132 style: Style::default(),
133 checkbox_style: Style::default(),
134 label_style: Style::default(),
135 checked_symbol: Cow::Borrowed(symbols::CHECKED),
136 unchecked_symbol: Cow::Borrowed(symbols::UNCHECKED),
137 }
138 }
139}
140
141impl<'a> Checkbox<'a> {
142 pub fn new<T>(label: T, checked: bool) -> Self
160 where
161 T: Into<Line<'a>>,
162 {
163 Self {
164 label: label.into(),
165 checked,
166 ..Default::default()
167 }
168 }
169
170 #[must_use = "method moves the value of self and returns the modified value"]
182 pub fn label<T>(mut self, label: T) -> Self
183 where
184 T: Into<Line<'a>>,
185 {
186 self.label = label.into();
187 self
188 }
189
190 #[must_use = "method moves the value of self and returns the modified value"]
200 pub const fn checked(mut self, checked: bool) -> Self {
201 self.checked = checked;
202 self
203 }
204
205 #[must_use = "method moves the value of self and returns the modified value"]
216 pub fn block(mut self, block: Block<'a>) -> Self {
217 self.block = Some(block);
218 self
219 }
220
221 #[must_use = "method moves the value of self and returns the modified value"]
240 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
241 self.style = style.into();
242 self
243 }
244
245 #[must_use = "method moves the value of self and returns the modified value"]
263 pub fn checkbox_style<S: Into<Style>>(mut self, style: S) -> Self {
264 self.checkbox_style = style.into();
265 self
266 }
267
268 #[must_use = "method moves the value of self and returns the modified value"]
286 pub fn label_style<S: Into<Style>>(mut self, style: S) -> Self {
287 self.label_style = style.into();
288 self
289 }
290
291 #[must_use = "method moves the value of self and returns the modified value"]
303 pub fn checked_symbol<T>(mut self, symbol: T) -> Self
304 where
305 T: Into<Cow<'a, str>>,
306 {
307 self.checked_symbol = symbol.into();
308 self
309 }
310
311 #[must_use = "method moves the value of self and returns the modified value"]
323 pub fn unchecked_symbol<T>(mut self, symbol: T) -> Self
324 where
325 T: Into<Cow<'a, str>>,
326 {
327 self.unchecked_symbol = symbol.into();
328 self
329 }
330}
331
332impl Styled for Checkbox<'_> {
333 type Item = Self;
334
335 fn style(&self) -> Style {
336 self.style
337 }
338
339 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
340 self.style = style.into();
341 self
342 }
343}
344
345impl Widget for Checkbox<'_> {
346 fn render(self, area: Rect, buf: &mut Buffer) {
347 Widget::render(&self, area, buf);
348 }
349}
350
351impl Widget for &Checkbox<'_> {
352 fn render(self, area: Rect, buf: &mut Buffer) {
353 buf.set_style(area, self.style);
354 let inner = if let Some(ref block) = self.block {
355 let inner_area = block.inner(area);
356 block.render(area, buf);
357 inner_area
358 } else {
359 area
360 };
361 self.render_checkbox(inner, buf);
362 }
363}
364
365impl Checkbox<'_> {
366 fn render_checkbox(&self, area: Rect, buf: &mut Buffer) {
367 if area.is_empty() {
368 return;
369 }
370
371 let symbol = if self.checked {
373 &self.checked_symbol
374 } else {
375 &self.unchecked_symbol
376 };
377
378 let checkbox_style = self.style.patch(self.checkbox_style);
380 let label_style = self.style.patch(self.label_style);
381
382 let checkbox_span = Span::styled(symbol.as_ref(), checkbox_style);
384
385 let styled_label = self.label.clone().patch_style(label_style);
387
388 let mut spans = vec![checkbox_span, Span::raw(" ")];
390 spans.extend(styled_label.spans);
391
392 let line = Line::from(spans);
393
394 line.render(area, buf);
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use ratatui::style::{Color, Modifier, Stylize};
402
403 use super::*;
404
405 #[test]
406 fn checkbox_new() {
407 let checkbox = Checkbox::new("Test", true);
408 assert_eq!(checkbox.label, Line::from("Test"));
409 assert!(checkbox.checked);
410 }
411
412 #[test]
413 fn checkbox_default() {
414 let checkbox = Checkbox::default();
415 assert_eq!(checkbox.label, Line::default());
416 assert!(!checkbox.checked);
417 }
418
419 #[test]
420 fn checkbox_label() {
421 let checkbox = Checkbox::default().label("New label");
422 assert_eq!(checkbox.label, Line::from("New label"));
423 }
424
425 #[test]
426 fn checkbox_checked() {
427 let checkbox = Checkbox::default().checked(true);
428 assert!(checkbox.checked);
429 }
430
431 #[test]
432 fn checkbox_style() {
433 let style = Style::default().fg(Color::Red);
434 let checkbox = Checkbox::default().style(style);
435 assert_eq!(checkbox.style, style);
436 }
437
438 #[test]
439 fn checkbox_checkbox_style() {
440 let style = Style::default().fg(Color::Green);
441 let checkbox = Checkbox::default().checkbox_style(style);
442 assert_eq!(checkbox.checkbox_style, style);
443 }
444
445 #[test]
446 fn checkbox_label_style() {
447 let style = Style::default().fg(Color::Blue);
448 let checkbox = Checkbox::default().label_style(style);
449 assert_eq!(checkbox.label_style, style);
450 }
451
452 #[test]
453 fn checkbox_checked_symbol() {
454 let checkbox = Checkbox::default().checked_symbol("[X]");
455 assert_eq!(checkbox.checked_symbol, "[X]");
456 }
457
458 #[test]
459 fn checkbox_unchecked_symbol() {
460 let checkbox = Checkbox::default().unchecked_symbol("[ ]");
461 assert_eq!(checkbox.unchecked_symbol, "[ ]");
462 }
463
464 #[test]
465 fn checkbox_styled_trait() {
466 let checkbox = Checkbox::default().red();
467 assert_eq!(checkbox.style, Style::default().fg(Color::Red));
468 }
469
470 #[test]
471 fn checkbox_render_unchecked() {
472 let checkbox = Checkbox::new("Test", false);
473 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
474 checkbox.render(buffer.area, &mut buffer);
475
476 assert!(buffer
478 .cell(buffer.area.as_position())
479 .unwrap()
480 .symbol()
481 .starts_with('☐'));
482 }
483
484 #[test]
485 fn checkbox_render_checked() {
486 let checkbox = Checkbox::new("Test", true);
487 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
488 checkbox.render(buffer.area, &mut buffer);
489
490 assert!(buffer
492 .cell(buffer.area.as_position())
493 .unwrap()
494 .symbol()
495 .starts_with('☑'));
496 }
497
498 #[test]
499 fn checkbox_render_empty_area() {
500 let checkbox = Checkbox::new("Test", true);
501 let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
502
503 checkbox.render(buffer.area, &mut buffer);
505 }
506
507 #[test]
508 fn checkbox_render_with_block() {
509 let checkbox = Checkbox::new("Test", true).block(Block::bordered());
510 let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 3));
511
512 checkbox.render(buffer.area, &mut buffer);
514 }
515
516 #[test]
517 fn checkbox_render_with_custom_symbols() {
518 let checkbox = Checkbox::new("Test", true)
519 .checked_symbol("[X]")
520 .unchecked_symbol("[ ]");
521
522 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
523 checkbox.render(buffer.area, &mut buffer);
524
525 assert!(buffer
526 .cell(buffer.area.as_position())
527 .unwrap()
528 .symbol()
529 .starts_with('['));
530 }
531
532 #[test]
533 fn checkbox_with_styled_label() {
534 let checkbox = Checkbox::new("Test".blue(), true);
535 assert_eq!(checkbox.label.spans[0].style.fg, Some(Color::Blue));
536 }
537
538 #[test]
539 fn checkbox_complex_styling() {
540 let checkbox = Checkbox::new("Feature", true)
541 .style(Style::default().fg(Color::White))
542 .checkbox_style(
543 Style::default()
544 .fg(Color::Green)
545 .add_modifier(Modifier::BOLD),
546 )
547 .label_style(Style::default().fg(Color::Gray));
548
549 assert_eq!(checkbox.style.fg, Some(Color::White));
550 assert_eq!(checkbox.checkbox_style.fg, Some(Color::Green));
551 assert_eq!(checkbox.label_style.fg, Some(Color::Gray));
552 }
553
554 #[test]
555 fn checkbox_emoji_symbols() {
556 let checkbox = Checkbox::new("Test", true)
557 .checked_symbol("✅ ")
558 .unchecked_symbol("⬜ ");
559
560 assert_eq!(checkbox.checked_symbol, "✅ ");
561 assert_eq!(checkbox.unchecked_symbol, "⬜ ");
562 }
563
564 #[test]
565 fn checkbox_unicode_symbols() {
566 let checkbox = Checkbox::new("Test", false)
567 .checked_symbol("● ")
568 .unchecked_symbol("○ ");
569
570 assert_eq!(checkbox.checked_symbol, "● ");
571 assert_eq!(checkbox.unchecked_symbol, "○ ");
572 }
573
574 #[test]
575 fn checkbox_arrow_symbols() {
576 let checkbox = Checkbox::new("Test", true)
577 .checked_symbol("▶ ")
578 .unchecked_symbol("▷ ");
579
580 assert_eq!(checkbox.checked_symbol, "▶ ");
581 assert_eq!(checkbox.unchecked_symbol, "▷ ");
582 }
583
584 #[test]
585 fn checkbox_parenthesis_symbols() {
586 let checkbox = Checkbox::new("Test", false)
587 .checked_symbol("(X)")
588 .unchecked_symbol("(O)");
589
590 assert_eq!(checkbox.checked_symbol, "(X)");
591 assert_eq!(checkbox.unchecked_symbol, "(O)");
592 }
593
594 #[test]
595 fn checkbox_minus_symbols() {
596 let checkbox = Checkbox::new("Test", false)
597 .checked_symbol("[+]")
598 .unchecked_symbol("[-]");
599
600 assert_eq!(checkbox.checked_symbol, "[+]");
601 assert_eq!(checkbox.unchecked_symbol, "[-]");
602 }
603
604 #[test]
605 fn checkbox_predefined_minus_symbol() {
606 use crate::symbols;
607 let checkbox = Checkbox::new("Test", false).unchecked_symbol(symbols::UNCHECKED_MINUS);
608
609 assert_eq!(checkbox.unchecked_symbol, "[-]");
610 }
611
612 #[test]
613 fn checkbox_predefined_parenthesis_symbols() {
614 use crate::symbols;
615 let checkbox = Checkbox::new("Test", true)
616 .checked_symbol(symbols::CHECKED_PARENTHESIS_X)
617 .unchecked_symbol(symbols::UNCHECKED_PARENTHESIS_O);
618
619 assert_eq!(checkbox.checked_symbol, "(X)");
620 assert_eq!(checkbox.unchecked_symbol, "(O)");
621 }
622
623 #[test]
624 fn checkbox_render_emoji() {
625 let checkbox = Checkbox::new("Emoji", true)
626 .checked_symbol("✅ ")
627 .unchecked_symbol("⬜ ");
628
629 let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
630 checkbox.render(buffer.area, &mut buffer);
631
632 assert!(buffer.area.area() > 0);
634 }
635
636 #[test]
637 fn checkbox_label_style_overrides() {
638 let checkbox = Checkbox::new("Test", true)
639 .style(Style::default().fg(Color::White))
640 .label_style(Style::default().fg(Color::Blue));
641
642 assert_eq!(checkbox.style.fg, Some(Color::White));
643 assert_eq!(checkbox.label_style.fg, Some(Color::Blue));
644 }
645}