tui_checkbox/
lib.rs

1//! # tui-checkbox
2//!
3//! A customizable checkbox widget for [Ratatui](https://github.com/ratatui/ratatui) TUI applications.
4//!
5//! ## Features
6//!
7//! - ☑️ Simple checkbox with label
8//! - 🎨 Customizable styling for checkbox and label separately
9//! - 🔤 Custom symbols (unicode, emoji, ASCII)
10//! - 📦 Optional block wrapper
11//! - ⚡ Zero-cost abstractions
12//!
13//! ## Examples
14//!
15//! Basic usage:
16//!
17//! ```no_run
18//! use ratatui::style::{Color, Style};
19//! use tui_checkbox::Checkbox;
20//!
21//! let checkbox = Checkbox::new("Enable feature", true);
22//! ```
23//!
24//! With custom styling:
25//!
26//! ```no_run
27//! use ratatui::style::{Color, Style, Modifier};
28//! use tui_checkbox::Checkbox;
29//!
30//! let checkbox = Checkbox::new("Enable feature", true)
31//!     .style(Style::default().fg(Color::White))
32//!     .checkbox_style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD))
33//!     .label_style(Style::default().fg(Color::Gray));
34//! ```
35//!
36//! With custom symbols:
37//!
38//! ```no_run
39//! use tui_checkbox::Checkbox;
40//!
41//! let checkbox = Checkbox::new("Task", false)
42//!     .checked_symbol("✅ ")
43//!     .unchecked_symbol("⬜ ");
44//! ```
45
46#![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/// A widget that displays a checkbox with a label.
60///
61/// A `Checkbox` can be in a checked or unchecked state. The checkbox is rendered with a symbol
62/// (default `☐` for unchecked and `☑` for checked) followed by a label.
63///
64/// The widget can be styled using [`Checkbox::style`] which affects both the checkbox symbol and
65/// the label. You can also style just the checkbox symbol using [`Checkbox::checkbox_style`] or
66/// the label using [`Checkbox::label_style`].
67///
68/// You can create a `Checkbox` using [`Checkbox::new`] or [`Checkbox::default`].
69///
70/// # Examples
71///
72/// ```
73/// use ratatui::style::{Color, Style, Stylize};
74/// use tui_checkbox::Checkbox;
75///
76/// Checkbox::new("Enable feature", true)
77///     .style(Style::default().fg(Color::White))
78///     .checkbox_style(Style::default().fg(Color::Green))
79///     .label_style(Style::default().fg(Color::Gray));
80/// ```
81///
82/// With a block:
83/// ```
84/// use ratatui::widgets::Block;
85/// use tui_checkbox::Checkbox;
86///
87/// Checkbox::new("Accept terms", false).block(Block::bordered().title("Settings"));
88/// ```
89#[expect(clippy::struct_field_names)] // checkbox_style needs to be differentiated from style
90#[derive(Debug, Clone, Eq, PartialEq, Hash)]
91pub struct Checkbox<'a> {
92    /// The label text displayed next to the checkbox
93    label: Line<'a>,
94    /// Whether the checkbox is checked
95    checked: bool,
96    /// Optional block to wrap the checkbox
97    block: Option<Block<'a>>,
98    /// Base style for the entire widget
99    style: Style,
100    /// Style specifically for the checkbox symbol
101    checkbox_style: Style,
102    /// Style specifically for the label text
103    label_style: Style,
104    /// Symbol to use when checked
105    checked_symbol: Cow<'a, str>,
106    /// Symbol to use when unchecked
107    unchecked_symbol: Cow<'a, str>,
108}
109
110impl Default for Checkbox<'_> {
111    /// Returns a default `Checkbox` widget.
112    ///
113    /// The default widget has:
114    /// - Empty label
115    /// - Unchecked state
116    /// - No block
117    /// - Default style for all elements
118    /// - Unicode checkbox symbols (☐ and ☑)
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use tui_checkbox::Checkbox;
124    ///
125    /// let checkbox = Checkbox::default();
126    /// ```
127    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    /// Creates a new `Checkbox` with the given label and checked state.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use tui_checkbox::Checkbox;
148    ///
149    /// let checkbox = Checkbox::new("Enable feature", true);
150    /// ```
151    ///
152    /// With styled label:
153    /// ```
154    /// use ratatui::style::Stylize;
155    /// use tui_checkbox::Checkbox;
156    ///
157    /// let checkbox = Checkbox::new("Enable feature".blue(), false);
158    /// ```
159    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    /// Sets the label of the checkbox.
171    ///
172    /// The label can be any type that converts into a [`Line`], such as a string or a styled span.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use tui_checkbox::Checkbox;
178    ///
179    /// let checkbox = Checkbox::default().label("My checkbox");
180    /// ```
181    #[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    /// Sets the checked state of the checkbox.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use tui_checkbox::Checkbox;
196    ///
197    /// let checkbox = Checkbox::default().checked(true);
198    /// ```
199    #[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    /// Wraps the checkbox with the given block.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use ratatui::widgets::Block;
211    /// use tui_checkbox::Checkbox;
212    ///
213    /// let checkbox = Checkbox::new("Option", false).block(Block::bordered().title("Settings"));
214    /// ```
215    #[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    /// Sets the base style of the widget.
222    ///
223    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
224    /// your own type that implements [`Into<Style>`]).
225    ///
226    /// This style will be applied to both the checkbox symbol and the label unless overridden by
227    /// more specific styles.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use ratatui::style::{Color, Style};
233    /// use tui_checkbox::Checkbox;
234    ///
235    /// let checkbox = Checkbox::new("Option", false).style(Style::default().fg(Color::White));
236    /// ```
237    ///
238    /// [`Color`]: ratatui::style::Color
239    #[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    /// Sets the style of the checkbox symbol.
246    ///
247    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
248    /// your own type that implements [`Into<Style>`]).
249    ///
250    /// This style will be combined with the base style set by [`Checkbox::style`].
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use ratatui::style::{Color, Style};
256    /// use tui_checkbox::Checkbox;
257    ///
258    /// let checkbox = Checkbox::new("Option", true).checkbox_style(Style::default().fg(Color::Green));
259    /// ```
260    ///
261    /// [`Color`]: ratatui::style::Color
262    #[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    /// Sets the style of the label text.
269    ///
270    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
271    /// your own type that implements [`Into<Style>`]).
272    ///
273    /// This style will be combined with the base style set by [`Checkbox::style`].
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use ratatui::style::{Color, Style};
279    /// use tui_checkbox::Checkbox;
280    ///
281    /// let checkbox = Checkbox::new("Option", false).label_style(Style::default().fg(Color::Gray));
282    /// ```
283    ///
284    /// [`Color`]: ratatui::style::Color
285    #[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    /// Sets the symbol to use when the checkbox is checked.
292    ///
293    /// The default is `☑` (U+2611).
294    ///
295    /// # Examples
296    ///
297    /// ```
298    /// use tui_checkbox::Checkbox;
299    ///
300    /// let checkbox = Checkbox::new("Option", true).checked_symbol("[X]");
301    /// ```
302    #[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    /// Sets the symbol to use when the checkbox is unchecked.
312    ///
313    /// The default is `☐` (U+2610).
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use tui_checkbox::Checkbox;
319    ///
320    /// let checkbox = Checkbox::new("Option", false).unchecked_symbol("[ ]");
321    /// ```
322    #[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        // Determine which symbol to use based on checked state
372        let symbol = if self.checked {
373            &self.checked_symbol
374        } else {
375            &self.unchecked_symbol
376        };
377
378        // Calculate the combined styles
379        let checkbox_style = self.style.patch(self.checkbox_style);
380        let label_style = self.style.patch(self.label_style);
381
382        // Render the checkbox symbol
383        let checkbox_span = Span::styled(symbol.as_ref(), checkbox_style);
384
385        // Render label with appropriate styling
386        let styled_label = self.label.clone().patch_style(label_style);
387
388        // Combine checkbox symbol and label with a space
389        let mut spans = vec![checkbox_span, Span::raw(" ")];
390        spans.extend(styled_label.spans);
391
392        let line = Line::from(spans);
393
394        // Render the line
395        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        // The buffer should contain the unchecked symbol followed by space and label
477        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        // The buffer should contain the checked symbol followed by space and label
491        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        // Should not panic
504        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        // Should not panic
513        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        // Should render without panic
633        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}