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#![allow(clippy::cast_possible_truncation)] // Terminal dimensions are always small
49
50use std::borrow::Cow;
51
52use ratatui::buffer::Buffer;
53use ratatui::layout::Rect;
54use ratatui::style::{Style, Styled};
55use ratatui::text::{Line, Span};
56use ratatui::widgets::{Block, Widget};
57
58pub mod symbols;
59
60/// Position of the label relative to the checkbox symbol.
61#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
62pub enum LabelPosition {
63    /// Label appears to the right of the checkbox (default)
64    #[default]
65    Right,
66    /// Label appears to the left of the checkbox
67    Left,
68    /// Label appears above the checkbox
69    Top,
70    /// Label appears below the checkbox
71    Bottom,
72}
73
74/// Horizontal alignment of content within its area.
75#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
76pub enum HorizontalAlignment {
77    /// Align to the left (default)
78    #[default]
79    Left,
80    /// Align to the center
81    Center,
82    /// Align to the right
83    Right,
84}
85
86/// Vertical alignment of content within its area.
87#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
88pub enum VerticalAlignment {
89    /// Align to the top (default)
90    #[default]
91    Top,
92    /// Align to the center
93    Center,
94    /// Align to the bottom
95    Bottom,
96}
97
98/// A widget that displays a checkbox with a label.
99///
100/// A `Checkbox` can be in a checked or unchecked state. The checkbox is rendered with a symbol
101/// (default `☐` for unchecked and `☑` for checked) followed by a label.
102///
103/// The widget can be styled using [`Checkbox::style`] which affects both the checkbox symbol and
104/// the label. You can also style just the checkbox symbol using [`Checkbox::checkbox_style`] or
105/// the label using [`Checkbox::label_style`].
106///
107/// You can create a `Checkbox` using [`Checkbox::new`] or [`Checkbox::default`].
108///
109/// # Examples
110///
111/// ```
112/// use ratatui::style::{Color, Style, Stylize};
113/// use tui_checkbox::Checkbox;
114///
115/// Checkbox::new("Enable feature", true)
116///     .style(Style::default().fg(Color::White))
117///     .checkbox_style(Style::default().fg(Color::Green))
118///     .label_style(Style::default().fg(Color::Gray));
119/// ```
120///
121/// With a block:
122/// ```
123/// use ratatui::widgets::Block;
124/// use tui_checkbox::Checkbox;
125///
126/// Checkbox::new("Accept terms", false).block(Block::bordered().title("Settings"));
127/// ```
128#[expect(clippy::struct_field_names)] // checkbox_style needs to be differentiated from style
129#[derive(Debug, Clone, Eq, PartialEq, Hash)]
130pub struct Checkbox<'a> {
131    /// The label text displayed next to the checkbox
132    label: Line<'a>,
133    /// Whether the checkbox is checked
134    checked: bool,
135    /// Optional block to wrap the checkbox
136    block: Option<Block<'a>>,
137    /// Base style for the entire widget
138    style: Style,
139    /// Style specifically for the checkbox symbol
140    checkbox_style: Style,
141    /// Style specifically for the label text
142    label_style: Style,
143    /// Symbol to use when checked
144    checked_symbol: Cow<'a, str>,
145    /// Symbol to use when unchecked
146    unchecked_symbol: Cow<'a, str>,
147    /// Position of the label relative to the checkbox
148    label_position: LabelPosition,
149    /// Horizontal alignment of the checkbox symbol
150    horizontal_alignment: HorizontalAlignment,
151    /// Vertical alignment of the checkbox symbol
152    vertical_alignment: VerticalAlignment,
153    /// Minimum width constraint
154    min_width: Option<u16>,
155    /// Maximum width constraint
156    max_width: Option<u16>,
157    /// Whether to wrap label text to multiple lines
158    wrap_label: bool,
159}
160
161impl Default for Checkbox<'_> {
162    /// Returns a default `Checkbox` widget.
163    ///
164    /// The default widget has:
165    /// - Empty label
166    /// - Unchecked state
167    /// - No block
168    /// - Default style for all elements
169    /// - Unicode checkbox symbols (☐ and ☑)
170    /// - Label position on the right
171    /// - Left and top alignment
172    /// - No width constraints
173    /// - No label wrapping
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use tui_checkbox::Checkbox;
179    ///
180    /// let checkbox = Checkbox::default();
181    /// ```
182    fn default() -> Self {
183        Self {
184            label: Line::default(),
185            checked: false,
186            block: None,
187            style: Style::default(),
188            checkbox_style: Style::default(),
189            label_style: Style::default(),
190            checked_symbol: Cow::Borrowed(symbols::CHECKED),
191            unchecked_symbol: Cow::Borrowed(symbols::UNCHECKED),
192            label_position: LabelPosition::default(),
193            horizontal_alignment: HorizontalAlignment::default(),
194            vertical_alignment: VerticalAlignment::default(),
195            min_width: None,
196            max_width: None,
197            wrap_label: false,
198        }
199    }
200}
201
202impl<'a> Checkbox<'a> {
203    /// Creates a new `Checkbox` with the given label and checked state.
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use tui_checkbox::Checkbox;
209    ///
210    /// let checkbox = Checkbox::new("Enable feature", true);
211    /// ```
212    ///
213    /// With styled label:
214    /// ```
215    /// use ratatui::style::Stylize;
216    /// use tui_checkbox::Checkbox;
217    ///
218    /// let checkbox = Checkbox::new("Enable feature".blue(), false);
219    /// ```
220    pub fn new<T>(label: T, checked: bool) -> Self
221    where
222        T: Into<Line<'a>>,
223    {
224        Self {
225            label: label.into(),
226            checked,
227            ..Default::default()
228        }
229    }
230
231    /// Sets the label of the checkbox.
232    ///
233    /// The label can be any type that converts into a [`Line`], such as a string or a styled span.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use tui_checkbox::Checkbox;
239    ///
240    /// let checkbox = Checkbox::default().label("My checkbox");
241    /// ```
242    #[must_use = "method moves the value of self and returns the modified value"]
243    pub fn label<T>(mut self, label: T) -> Self
244    where
245        T: Into<Line<'a>>,
246    {
247        self.label = label.into();
248        self
249    }
250
251    /// Sets the checked state of the checkbox.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use tui_checkbox::Checkbox;
257    ///
258    /// let checkbox = Checkbox::default().checked(true);
259    /// ```
260    #[must_use = "method moves the value of self and returns the modified value"]
261    pub const fn checked(mut self, checked: bool) -> Self {
262        self.checked = checked;
263        self
264    }
265
266    /// Wraps the checkbox with the given block.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// use ratatui::widgets::Block;
272    /// use tui_checkbox::Checkbox;
273    ///
274    /// let checkbox = Checkbox::new("Option", false).block(Block::bordered().title("Settings"));
275    /// ```
276    #[must_use = "method moves the value of self and returns the modified value"]
277    pub fn block(mut self, block: Block<'a>) -> Self {
278        self.block = Some(block);
279        self
280    }
281
282    /// Sets the base style of the widget.
283    ///
284    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
285    /// your own type that implements [`Into<Style>`]).
286    ///
287    /// This style will be applied to both the checkbox symbol and the label unless overridden by
288    /// more specific styles.
289    ///
290    /// # Examples
291    ///
292    /// ```
293    /// use ratatui::style::{Color, Style};
294    /// use tui_checkbox::Checkbox;
295    ///
296    /// let checkbox = Checkbox::new("Option", false).style(Style::default().fg(Color::White));
297    /// ```
298    ///
299    /// [`Color`]: ratatui::style::Color
300    #[must_use = "method moves the value of self and returns the modified value"]
301    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
302        self.style = style.into();
303        self
304    }
305
306    /// Sets the style of the checkbox symbol.
307    ///
308    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
309    /// your own type that implements [`Into<Style>`]).
310    ///
311    /// This style will be combined with the base style set by [`Checkbox::style`].
312    ///
313    /// # Examples
314    ///
315    /// ```
316    /// use ratatui::style::{Color, Style};
317    /// use tui_checkbox::Checkbox;
318    ///
319    /// let checkbox = Checkbox::new("Option", true).checkbox_style(Style::default().fg(Color::Green));
320    /// ```
321    ///
322    /// [`Color`]: ratatui::style::Color
323    #[must_use = "method moves the value of self and returns the modified value"]
324    pub fn checkbox_style<S: Into<Style>>(mut self, style: S) -> Self {
325        self.checkbox_style = style.into();
326        self
327    }
328
329    /// Sets the style of the label text.
330    ///
331    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
332    /// your own type that implements [`Into<Style>`]).
333    ///
334    /// This style will be combined with the base style set by [`Checkbox::style`].
335    ///
336    /// # Examples
337    ///
338    /// ```
339    /// use ratatui::style::{Color, Style};
340    /// use tui_checkbox::Checkbox;
341    ///
342    /// let checkbox = Checkbox::new("Option", false).label_style(Style::default().fg(Color::Gray));
343    /// ```
344    ///
345    /// [`Color`]: ratatui::style::Color
346    #[must_use = "method moves the value of self and returns the modified value"]
347    pub fn label_style<S: Into<Style>>(mut self, style: S) -> Self {
348        self.label_style = style.into();
349        self
350    }
351
352    /// Sets the symbol to use when the checkbox is checked.
353    ///
354    /// The default is `☑` (U+2611).
355    ///
356    /// # Examples
357    ///
358    /// ```
359    /// use tui_checkbox::Checkbox;
360    ///
361    /// let checkbox = Checkbox::new("Option", true).checked_symbol("[X]");
362    /// ```
363    #[must_use = "method moves the value of self and returns the modified value"]
364    pub fn checked_symbol<T>(mut self, symbol: T) -> Self
365    where
366        T: Into<Cow<'a, str>>,
367    {
368        self.checked_symbol = symbol.into();
369        self
370    }
371
372    /// Sets the symbol to use when the checkbox is unchecked.
373    ///
374    /// The default is `☐` (U+2610).
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use tui_checkbox::Checkbox;
380    ///
381    /// let checkbox = Checkbox::new("Option", false).unchecked_symbol("[ ]");
382    /// ```
383    #[must_use = "method moves the value of self and returns the modified value"]
384    pub fn unchecked_symbol<T>(mut self, symbol: T) -> Self
385    where
386        T: Into<Cow<'a, str>>,
387    {
388        self.unchecked_symbol = symbol.into();
389        self
390    }
391
392    /// Sets the position of the label relative to the checkbox symbol.
393    ///
394    /// The default is [`LabelPosition::Right`].
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use tui_checkbox::{Checkbox, LabelPosition};
400    ///
401    /// let checkbox = Checkbox::new("Option", false).label_position(LabelPosition::Left);
402    /// ```
403    #[must_use = "method moves the value of self and returns the modified value"]
404    pub const fn label_position(mut self, position: LabelPosition) -> Self {
405        self.label_position = position;
406        self
407    }
408
409    /// Sets the horizontal alignment of the checkbox content within its area.
410    ///
411    /// The default is [`HorizontalAlignment::Left`].
412    ///
413    /// # Examples
414    ///
415    /// ```
416    /// use tui_checkbox::{Checkbox, HorizontalAlignment};
417    ///
418    /// let checkbox = Checkbox::new("Option", false)
419    ///     .horizontal_alignment(HorizontalAlignment::Center);
420    /// ```
421    #[must_use = "method moves the value of self and returns the modified value"]
422    pub const fn horizontal_alignment(mut self, alignment: HorizontalAlignment) -> Self {
423        self.horizontal_alignment = alignment;
424        self
425    }
426
427    /// Sets the vertical alignment of the checkbox content within its area.
428    ///
429    /// The default is [`VerticalAlignment::Top`].
430    ///
431    /// # Examples
432    ///
433    /// ```
434    /// use tui_checkbox::{Checkbox, VerticalAlignment};
435    ///
436    /// let checkbox = Checkbox::new("Option", false)
437    ///     .vertical_alignment(VerticalAlignment::Center);
438    /// ```
439    #[must_use = "method moves the value of self and returns the modified value"]
440    pub const fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
441        self.vertical_alignment = alignment;
442        self
443    }
444
445    /// Sets the minimum width constraint for the checkbox widget.
446    ///
447    /// The default is no minimum width.
448    ///
449    /// # Examples
450    ///
451    /// ```
452    /// use tui_checkbox::Checkbox;
453    ///
454    /// let checkbox = Checkbox::new("Option", false).min_width(20);
455    /// ```
456    #[must_use = "method moves the value of self and returns the modified value"]
457    pub const fn min_width(mut self, width: u16) -> Self {
458        self.min_width = Some(width);
459        self
460    }
461
462    /// Sets the maximum width constraint for the checkbox widget.
463    ///
464    /// The default is no maximum width.
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// use tui_checkbox::Checkbox;
470    ///
471    /// let checkbox = Checkbox::new("Option", false).max_width(40);
472    /// ```
473    #[must_use = "method moves the value of self and returns the modified value"]
474    pub const fn max_width(mut self, width: u16) -> Self {
475        self.max_width = Some(width);
476        self
477    }
478
479    /// Enables or disables label text wrapping.
480    ///
481    /// When enabled, the label will wrap to multiple lines if it exceeds the available width.
482    /// The default is `false` (no wrapping).
483    ///
484    /// # Examples
485    ///
486    /// ```
487    /// use tui_checkbox::Checkbox;
488    ///
489    /// let checkbox = Checkbox::new("This is a very long label that should wrap", false)
490    ///     .wrap_label(true)
491    ///     .max_width(30);
492    /// ```
493    #[must_use = "method moves the value of self and returns the modified value"]
494    pub const fn wrap_label(mut self, wrap: bool) -> Self {
495        self.wrap_label = wrap;
496        self
497    }
498}
499
500impl Styled for Checkbox<'_> {
501    type Item = Self;
502
503    fn style(&self) -> Style {
504        self.style
505    }
506
507    fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
508        self.style = style.into();
509        self
510    }
511}
512
513impl Widget for Checkbox<'_> {
514    fn render(self, area: Rect, buf: &mut Buffer) {
515        Widget::render(&self, area, buf);
516    }
517}
518
519impl Widget for &Checkbox<'_> {
520    fn render(self, area: Rect, buf: &mut Buffer) {
521        buf.set_style(area, self.style);
522        let inner = if let Some(ref block) = self.block {
523            let inner_area = block.inner(area);
524            block.render(area, buf);
525            inner_area
526        } else {
527            area
528        };
529        self.render_checkbox(inner, buf);
530    }
531}
532
533impl Checkbox<'_> {
534    fn render_checkbox(&self, area: Rect, buf: &mut Buffer) {
535        if area.is_empty() {
536            return;
537        }
538
539        // Determine which symbol to use based on checked state
540        let symbol = if self.checked {
541            &self.checked_symbol
542        } else {
543            &self.unchecked_symbol
544        };
545
546        // Calculate the combined styles
547        let checkbox_style = self.style.patch(self.checkbox_style);
548        let label_style = self.style.patch(self.label_style);
549
550        // Apply width constraints
551        let mut render_area = area;
552        if let Some(min_width) = self.min_width {
553            render_area.width = render_area.width.max(min_width);
554        }
555        if let Some(max_width) = self.max_width {
556            render_area.width = render_area.width.min(max_width);
557        }
558
559        // Ensure render_area doesn't exceed original area
560        render_area.width = render_area.width.min(area.width);
561
562        // Create checkbox and label spans
563        let checkbox_span = Span::styled(symbol.as_ref(), checkbox_style);
564        let styled_label = self.label.clone().patch_style(label_style);
565        let owned_label = Line::from(
566            styled_label
567                .spans
568                .iter()
569                .map(|s| Span::styled(s.content.to_string(), s.style))
570                .collect::<Vec<_>>(),
571        );
572
573        // Calculate dimensions based on label position
574        match self.label_position {
575            LabelPosition::Right | LabelPosition::Left => {
576                self.render_horizontal(render_area, buf, checkbox_span, owned_label);
577            }
578            LabelPosition::Top | LabelPosition::Bottom => {
579                self.render_vertical(render_area, buf, checkbox_span, owned_label);
580            }
581        }
582    }
583
584    fn render_horizontal(
585        &self,
586        area: Rect,
587        buf: &mut Buffer,
588        checkbox_span: Span<'_>,
589        label: Line<'static>,
590    ) {
591        if area.height == 0 || area.width == 0 {
592            return;
593        }
594
595        let checkbox_width = checkbox_span.width() as u16;
596        let space_width = 1u16;
597
598        // Handle wrapping if enabled
599        let label_lines = if self.wrap_label {
600            let available_width = area.width.saturating_sub(checkbox_width + space_width);
601            Self::wrap_text(&label, available_width)
602        } else {
603            vec![label]
604        };
605
606        let total_width = if label_lines.is_empty() {
607            checkbox_width
608        } else {
609            checkbox_width
610                + space_width
611                + label_lines
612                    .iter()
613                    .map(|l| l.width() as u16)
614                    .max()
615                    .unwrap_or(0)
616        };
617
618        // Calculate horizontal offset based on alignment
619        let x_offset = match self.horizontal_alignment {
620            HorizontalAlignment::Left => 0,
621            HorizontalAlignment::Center => area.width.saturating_sub(total_width) / 2,
622            HorizontalAlignment::Right => area.width.saturating_sub(total_width),
623        };
624
625        // Calculate vertical offset based on alignment
626        let content_height = label_lines.len() as u16;
627        let y_offset = match self.vertical_alignment {
628            VerticalAlignment::Top => 0,
629            VerticalAlignment::Center => area.height.saturating_sub(content_height) / 2,
630            VerticalAlignment::Bottom => area.height.saturating_sub(content_height),
631        };
632
633        // Render based on label position
634        match self.label_position {
635            LabelPosition::Right => {
636                // Render checkbox first, then label
637                if x_offset < area.width && y_offset < area.height {
638                    let checkbox_area = Rect {
639                        x: area.x + x_offset,
640                        y: area.y + y_offset,
641                        width: checkbox_width.min(area.width.saturating_sub(x_offset)),
642                        height: 1,
643                    };
644                    Line::from(vec![checkbox_span]).render(checkbox_area, buf);
645
646                    // Render label lines
647                    for (i, label_line) in label_lines.iter().enumerate() {
648                        let label_x = area.x + x_offset + checkbox_width + space_width;
649                        let label_y = area.y + y_offset + i as u16;
650                        if label_y < area.y + area.height && label_x < area.x + area.width {
651                            let label_area = Rect {
652                                x: label_x,
653                                y: label_y,
654                                width: area
655                                    .width
656                                    .saturating_sub(x_offset + checkbox_width + space_width),
657                                height: 1,
658                            };
659                            label_line.clone().render(label_area, buf);
660                        }
661                    }
662                }
663            }
664            LabelPosition::Left => {
665                // Render label first, then checkbox
666                let max_label_width = label_lines
667                    .iter()
668                    .map(|l| l.width() as u16)
669                    .max()
670                    .unwrap_or(0);
671
672                // Render label lines
673                for (i, label_line) in label_lines.iter().enumerate() {
674                    let label_y = area.y + y_offset + i as u16;
675                    if label_y < area.y + area.height && x_offset < area.width {
676                        let label_area = Rect {
677                            x: area.x + x_offset,
678                            y: label_y,
679                            width: max_label_width.min(area.width.saturating_sub(x_offset)),
680                            height: 1,
681                        };
682                        label_line.clone().render(label_area, buf);
683                    }
684                }
685
686                // Render checkbox
687                let checkbox_x = area.x + x_offset + max_label_width + space_width;
688                if checkbox_x < area.x + area.width && y_offset < area.height {
689                    let checkbox_area = Rect {
690                        x: checkbox_x,
691                        y: area.y + y_offset,
692                        width: checkbox_width.min(
693                            area.width
694                                .saturating_sub(x_offset + max_label_width + space_width),
695                        ),
696                        height: 1,
697                    };
698                    Line::from(vec![checkbox_span]).render(checkbox_area, buf);
699                }
700            }
701            _ => {}
702        }
703    }
704
705    fn render_vertical(
706        &self,
707        area: Rect,
708        buf: &mut Buffer,
709        checkbox_span: Span<'_>,
710        label: Line<'static>,
711    ) {
712        if area.height == 0 || area.width == 0 {
713            return;
714        }
715
716        // Handle wrapping if enabled
717        let label_lines = if self.wrap_label {
718            Self::wrap_text(&label, area.width)
719        } else {
720            vec![label]
721        };
722
723        let checkbox_width = checkbox_span.width() as u16;
724        let label_height = label_lines.len() as u16;
725        let total_height = 1 + label_height; // checkbox + label lines
726
727        // Calculate vertical offset
728        let y_offset = match self.vertical_alignment {
729            VerticalAlignment::Top => 0,
730            VerticalAlignment::Center => area.height.saturating_sub(total_height) / 2,
731            VerticalAlignment::Bottom => area.height.saturating_sub(total_height),
732        };
733
734        match self.label_position {
735            LabelPosition::Top => {
736                // Render label first
737                for (i, label_line) in label_lines.iter().enumerate() {
738                    let label_y = area.y + y_offset + i as u16;
739                    if label_y < area.y + area.height {
740                        let x_offset = match self.horizontal_alignment {
741                            HorizontalAlignment::Left => 0,
742                            HorizontalAlignment::Center => {
743                                area.width.saturating_sub(label_line.width() as u16) / 2
744                            }
745                            HorizontalAlignment::Right => {
746                                area.width.saturating_sub(label_line.width() as u16)
747                            }
748                        };
749                        let label_area = Rect {
750                            x: area.x + x_offset,
751                            y: label_y,
752                            width: area.width.saturating_sub(x_offset),
753                            height: 1,
754                        };
755                        label_line.clone().render(label_area, buf);
756                    }
757                }
758
759                // Render checkbox
760                let checkbox_y = area.y + y_offset + label_height;
761                if checkbox_y < area.y + area.height {
762                    let x_offset = match self.horizontal_alignment {
763                        HorizontalAlignment::Left => 0,
764                        HorizontalAlignment::Center => {
765                            area.width.saturating_sub(checkbox_width) / 2
766                        }
767                        HorizontalAlignment::Right => area.width.saturating_sub(checkbox_width),
768                    };
769                    let checkbox_area = Rect {
770                        x: area.x + x_offset,
771                        y: checkbox_y,
772                        width: checkbox_width.min(area.width.saturating_sub(x_offset)),
773                        height: 1,
774                    };
775                    Line::from(vec![checkbox_span]).render(checkbox_area, buf);
776                }
777            }
778            LabelPosition::Bottom => {
779                // Render checkbox first
780                let x_offset = match self.horizontal_alignment {
781                    HorizontalAlignment::Left => 0,
782                    HorizontalAlignment::Center => area.width.saturating_sub(checkbox_width) / 2,
783                    HorizontalAlignment::Right => area.width.saturating_sub(checkbox_width),
784                };
785                let checkbox_area = Rect {
786                    x: area.x + x_offset,
787                    y: area.y + y_offset,
788                    width: checkbox_width.min(area.width.saturating_sub(x_offset)),
789                    height: 1,
790                };
791                Line::from(vec![checkbox_span]).render(checkbox_area, buf);
792
793                // Render label
794                for (i, label_line) in label_lines.iter().enumerate() {
795                    let label_y = area.y + y_offset + 1 + i as u16;
796                    if label_y < area.y + area.height {
797                        let x_offset = match self.horizontal_alignment {
798                            HorizontalAlignment::Left => 0,
799                            HorizontalAlignment::Center => {
800                                area.width.saturating_sub(label_line.width() as u16) / 2
801                            }
802                            HorizontalAlignment::Right => {
803                                area.width.saturating_sub(label_line.width() as u16)
804                            }
805                        };
806                        let label_area = Rect {
807                            x: area.x + x_offset,
808                            y: label_y,
809                            width: area.width.saturating_sub(x_offset),
810                            height: 1,
811                        };
812                        label_line.clone().render(label_area, buf);
813                    }
814                }
815            }
816            _ => {}
817        }
818    }
819
820    fn wrap_text(line: &Line<'_>, max_width: u16) -> Vec<Line<'static>> {
821        if max_width == 0 {
822            let owned = Line::from(
823                line.spans
824                    .iter()
825                    .map(|s| Span::styled(s.content.to_string(), s.style))
826                    .collect::<Vec<_>>(),
827            );
828            return vec![owned];
829        }
830
831        let mut result = Vec::new();
832        let mut current_line = Vec::new();
833        let mut current_width = 0u16;
834
835        for span in &line.spans {
836            let text = span.content.as_ref();
837            let words: Vec<&str> = text.split(' ').collect();
838
839            for (i, word) in words.iter().enumerate() {
840                let word_width = word.chars().count() as u16;
841                let space_width = u16::from(i > 0 || !current_line.is_empty());
842
843                if current_width + space_width + word_width > max_width && !current_line.is_empty()
844                {
845                    result.push(Line::from(current_line.clone()));
846                    current_line.clear();
847                    current_width = 0;
848                }
849
850                if i > 0 {
851                    current_line.push(Span::styled(String::from(" "), span.style));
852                    current_width += 1;
853                }
854
855                current_line.push(Span::styled(String::from(*word), span.style));
856                current_width += word_width;
857            }
858        }
859
860        if !current_line.is_empty() {
861            result.push(Line::from(current_line));
862        }
863
864        if result.is_empty() {
865            let owned = Line::from(
866                line.spans
867                    .iter()
868                    .map(|s| Span::styled(s.content.to_string(), s.style))
869                    .collect::<Vec<_>>(),
870            );
871            result.push(owned);
872        }
873
874        result
875    }
876}
877
878#[cfg(test)]
879mod tests {
880    use ratatui::style::{Color, Modifier, Stylize};
881
882    use super::*;
883
884    #[test]
885    fn checkbox_new() {
886        let checkbox = Checkbox::new("Test", true);
887        assert_eq!(checkbox.label, Line::from("Test"));
888        assert!(checkbox.checked);
889    }
890
891    #[test]
892    fn checkbox_default() {
893        let checkbox = Checkbox::default();
894        assert_eq!(checkbox.label, Line::default());
895        assert!(!checkbox.checked);
896    }
897
898    #[test]
899    fn checkbox_label() {
900        let checkbox = Checkbox::default().label("New label");
901        assert_eq!(checkbox.label, Line::from("New label"));
902    }
903
904    #[test]
905    fn checkbox_checked() {
906        let checkbox = Checkbox::default().checked(true);
907        assert!(checkbox.checked);
908    }
909
910    #[test]
911    fn checkbox_style() {
912        let style = Style::default().fg(Color::Red);
913        let checkbox = Checkbox::default().style(style);
914        assert_eq!(checkbox.style, style);
915    }
916
917    #[test]
918    fn checkbox_checkbox_style() {
919        let style = Style::default().fg(Color::Green);
920        let checkbox = Checkbox::default().checkbox_style(style);
921        assert_eq!(checkbox.checkbox_style, style);
922    }
923
924    #[test]
925    fn checkbox_label_style() {
926        let style = Style::default().fg(Color::Blue);
927        let checkbox = Checkbox::default().label_style(style);
928        assert_eq!(checkbox.label_style, style);
929    }
930
931    #[test]
932    fn checkbox_checked_symbol() {
933        let checkbox = Checkbox::default().checked_symbol("[X]");
934        assert_eq!(checkbox.checked_symbol, "[X]");
935    }
936
937    #[test]
938    fn checkbox_unchecked_symbol() {
939        let checkbox = Checkbox::default().unchecked_symbol("[ ]");
940        assert_eq!(checkbox.unchecked_symbol, "[ ]");
941    }
942
943    #[test]
944    fn checkbox_styled_trait() {
945        let checkbox = Checkbox::default().red();
946        assert_eq!(checkbox.style, Style::default().fg(Color::Red));
947    }
948
949    #[test]
950    fn checkbox_render_unchecked() {
951        let checkbox = Checkbox::new("Test", false);
952        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
953        checkbox.render(buffer.area, &mut buffer);
954
955        // The buffer should contain the unchecked symbol followed by space and label
956        assert!(buffer
957            .cell(buffer.area.as_position())
958            .unwrap()
959            .symbol()
960            .starts_with('☐'));
961    }
962
963    #[test]
964    fn checkbox_render_checked() {
965        let checkbox = Checkbox::new("Test", true);
966        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
967        checkbox.render(buffer.area, &mut buffer);
968
969        // The buffer should contain the checked symbol followed by space and label
970        assert!(buffer
971            .cell(buffer.area.as_position())
972            .unwrap()
973            .symbol()
974            .starts_with('☑'));
975    }
976
977    #[test]
978    fn checkbox_render_empty_area() {
979        let checkbox = Checkbox::new("Test", true);
980        let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
981
982        // Should not panic
983        checkbox.render(buffer.area, &mut buffer);
984    }
985
986    #[test]
987    fn checkbox_render_with_block() {
988        let checkbox = Checkbox::new("Test", true).block(Block::bordered());
989        let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 3));
990
991        // Should not panic
992        checkbox.render(buffer.area, &mut buffer);
993    }
994
995    #[test]
996    fn checkbox_render_with_custom_symbols() {
997        let checkbox = Checkbox::new("Test", true)
998            .checked_symbol("[X]")
999            .unchecked_symbol("[ ]");
1000
1001        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
1002        checkbox.render(buffer.area, &mut buffer);
1003
1004        assert!(buffer
1005            .cell(buffer.area.as_position())
1006            .unwrap()
1007            .symbol()
1008            .starts_with('['));
1009    }
1010
1011    #[test]
1012    fn checkbox_with_styled_label() {
1013        let checkbox = Checkbox::new("Test".blue(), true);
1014        assert_eq!(checkbox.label.spans[0].style.fg, Some(Color::Blue));
1015    }
1016
1017    #[test]
1018    fn checkbox_complex_styling() {
1019        let checkbox = Checkbox::new("Feature", true)
1020            .style(Style::default().fg(Color::White))
1021            .checkbox_style(
1022                Style::default()
1023                    .fg(Color::Green)
1024                    .add_modifier(Modifier::BOLD),
1025            )
1026            .label_style(Style::default().fg(Color::Gray));
1027
1028        assert_eq!(checkbox.style.fg, Some(Color::White));
1029        assert_eq!(checkbox.checkbox_style.fg, Some(Color::Green));
1030        assert_eq!(checkbox.label_style.fg, Some(Color::Gray));
1031    }
1032
1033    #[test]
1034    fn checkbox_emoji_symbols() {
1035        let checkbox = Checkbox::new("Test", true)
1036            .checked_symbol("✅ ")
1037            .unchecked_symbol("⬜ ");
1038
1039        assert_eq!(checkbox.checked_symbol, "✅ ");
1040        assert_eq!(checkbox.unchecked_symbol, "⬜ ");
1041    }
1042
1043    #[test]
1044    fn checkbox_unicode_symbols() {
1045        let checkbox = Checkbox::new("Test", false)
1046            .checked_symbol("● ")
1047            .unchecked_symbol("○ ");
1048
1049        assert_eq!(checkbox.checked_symbol, "● ");
1050        assert_eq!(checkbox.unchecked_symbol, "○ ");
1051    }
1052
1053    #[test]
1054    fn checkbox_arrow_symbols() {
1055        let checkbox = Checkbox::new("Test", true)
1056            .checked_symbol("▶ ")
1057            .unchecked_symbol("▷ ");
1058
1059        assert_eq!(checkbox.checked_symbol, "▶ ");
1060        assert_eq!(checkbox.unchecked_symbol, "▷ ");
1061    }
1062
1063    #[test]
1064    fn checkbox_parenthesis_symbols() {
1065        let checkbox = Checkbox::new("Test", false)
1066            .checked_symbol("(X)")
1067            .unchecked_symbol("(O)");
1068
1069        assert_eq!(checkbox.checked_symbol, "(X)");
1070        assert_eq!(checkbox.unchecked_symbol, "(O)");
1071    }
1072
1073    #[test]
1074    fn checkbox_minus_symbols() {
1075        let checkbox = Checkbox::new("Test", false)
1076            .checked_symbol("[+]")
1077            .unchecked_symbol("[-]");
1078
1079        assert_eq!(checkbox.checked_symbol, "[+]");
1080        assert_eq!(checkbox.unchecked_symbol, "[-]");
1081    }
1082
1083    #[test]
1084    fn checkbox_predefined_minus_symbol() {
1085        use crate::symbols;
1086        let checkbox = Checkbox::new("Test", false).unchecked_symbol(symbols::UNCHECKED_MINUS);
1087
1088        assert_eq!(checkbox.unchecked_symbol, "[-]");
1089    }
1090
1091    #[test]
1092    fn checkbox_predefined_parenthesis_symbols() {
1093        use crate::symbols;
1094        let checkbox = Checkbox::new("Test", true)
1095            .checked_symbol(symbols::CHECKED_PARENTHESIS_X)
1096            .unchecked_symbol(symbols::UNCHECKED_PARENTHESIS_O);
1097
1098        assert_eq!(checkbox.checked_symbol, "(X)");
1099        assert_eq!(checkbox.unchecked_symbol, "(O)");
1100    }
1101
1102    #[test]
1103    fn checkbox_render_emoji() {
1104        let checkbox = Checkbox::new("Emoji", true)
1105            .checked_symbol("✅ ")
1106            .unchecked_symbol("⬜ ");
1107
1108        let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
1109        checkbox.render(buffer.area, &mut buffer);
1110
1111        // Should render without panic
1112        assert!(buffer.area.area() > 0);
1113    }
1114
1115    #[test]
1116    fn checkbox_label_style_overrides() {
1117        let checkbox = Checkbox::new("Test", true)
1118            .style(Style::default().fg(Color::White))
1119            .label_style(Style::default().fg(Color::Blue));
1120
1121        assert_eq!(checkbox.style.fg, Some(Color::White));
1122        assert_eq!(checkbox.label_style.fg, Some(Color::Blue));
1123    }
1124}