Skip to main content

tui_lipan/widgets/checkbox/
mod.rs

1//! Checkbox widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_checkbox;
8pub use node::CheckboxNode;
9pub use reconcile::reconcile_checkbox;
10
11use std::sync::Arc;
12
13use crate::callback::{Callback, KeyHandler};
14use crate::core::element::{Element, ElementKind};
15use crate::core::event::MouseEvent;
16use crate::style::{Length, Padding, Style, StyleSlot};
17
18/// Visual variant for a [`Checkbox`].
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
20pub enum CheckboxVariant {
21    /// Rendered as `[x]` / `[ ]`.
22    #[default]
23    Bracket,
24    /// Rendered as `◉` / `○`.
25    Circle,
26    /// Rendered as `☐` with `✓` when checked - modern box style.
27    Box,
28    /// Rendered as `●` / `○` - switch-style glyph pair.
29    Switch,
30    /// Custom variant with user-defined strings.
31    Custom {
32        /// String for checked state.
33        checked: &'static str,
34        /// String for unchecked state.
35        unchecked: &'static str,
36        /// String for indeterminate state.
37        indeterminate: &'static str,
38    },
39}
40
41impl CheckboxVariant {
42    /// Get the display string for checked state.
43    pub fn checked_str(self) -> &'static str {
44        match self {
45            Self::Bracket => "[x]",
46            Self::Circle => "◉",
47            Self::Box => "✓",
48            Self::Switch => "●",
49            Self::Custom { checked, .. } => checked,
50        }
51    }
52
53    /// Get the display string for unchecked state.
54    pub fn unchecked_str(self) -> &'static str {
55        match self {
56            Self::Bracket => "[ ]",
57            Self::Circle => "○",
58            Self::Box => "☐",
59            Self::Switch => "○",
60            Self::Custom { unchecked, .. } => unchecked,
61        }
62    }
63
64    /// Get the display string for indeterminate state.
65    pub fn indeterminate_str(self) -> &'static str {
66        match self {
67            Self::Bracket => "[-]",
68            Self::Circle => "◍",
69            Self::Box => "▣",
70            Self::Switch => "◐",
71            Self::Custom { indeterminate, .. } => indeterminate,
72        }
73    }
74
75    /// Get the width of the checkbox symbol (in cells).
76    pub fn width(self) -> u16 {
77        use unicode_width::UnicodeWidthStr;
78        match self {
79            Self::Bracket => 3,
80            Self::Circle | Self::Box | Self::Switch => 1,
81            Self::Custom {
82                checked,
83                unchecked,
84                indeterminate,
85            } => UnicodeWidthStr::width(checked)
86                .max(UnicodeWidthStr::width(unchecked))
87                .max(UnicodeWidthStr::width(indeterminate)) as u16,
88        }
89    }
90}
91
92/// Checkbox state.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94pub enum CheckboxState {
95    /// Unchecked state.
96    Unchecked,
97    /// Checked state.
98    Checked,
99    /// Indeterminate state.
100    Indeterminate,
101}
102
103impl CheckboxState {
104    /// Return true if checked.
105    pub fn is_checked(self) -> bool {
106        matches!(self, Self::Checked)
107    }
108
109    /// Return true if indeterminate.
110    pub fn is_indeterminate(self) -> bool {
111        matches!(self, Self::Indeterminate)
112    }
113
114    /// Toggle the state (indeterminate -> checked).
115    pub fn toggle(self) -> Self {
116        match self {
117            Self::Checked => Self::Unchecked,
118            Self::Unchecked | Self::Indeterminate => Self::Checked,
119        }
120    }
121}
122
123/// A checkbox toggle event.
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
125pub struct CheckboxEvent {
126    /// New checkbox state after toggle.
127    pub state: CheckboxState,
128}
129
130/// A checkbox widget for boolean values.
131#[derive(Clone)]
132pub struct Checkbox {
133    /// Checkbox state.
134    pub state: CheckboxState,
135    /// Optional label displayed next to the checkbox.
136    pub label: Option<Arc<str>>,
137    /// Visual variant.
138    pub variant: CheckboxVariant,
139    /// Gap between checkbox symbol and label.
140    pub gap: u16,
141    /// Base style.
142    pub style: Style,
143    /// Style applied when hovered.
144    pub hover_style: StyleSlot,
145    /// Style applied when focused.
146    pub focus_style: StyleSlot,
147    /// Style for the checked state symbol.
148    pub checked_style: Style,
149    /// Style for the unchecked state symbol.
150    pub unchecked_style: Style,
151    /// Style for the indeterminate state symbol.
152    pub indeterminate_style: Style,
153    /// Label style.
154    pub label_style: Style,
155    /// Padding.
156    /// Default: `Padding::default()`.
157    pub padding: Padding,
158    /// Whether the checkbox is disabled.
159    pub disabled: bool,
160    /// Style applied when disabled.
161    pub disabled_style: Style,
162    /// Requested width.
163    /// Default: `Length::Auto`.
164    pub width: Length,
165    /// Requested height.
166    /// Default: `Length::Auto`.
167    pub height: Length,
168    /// Toggle handler.
169    pub on_toggle: Option<Callback<CheckboxEvent>>,
170    /// Mouse click handler.
171    pub on_click: Option<Callback<MouseEvent>>,
172    /// Keyboard handler.
173    pub on_key: Option<KeyHandler>,
174    /// Whether the checkbox participates in focus traversal.
175    pub focusable: bool,
176    /// Whether the checkbox participates in tab traversal when focusable.
177    pub tab_stop: bool,
178    /// Callback fired when the checkbox gains focus.
179    pub on_focus: Option<Callback<()>>,
180    /// Callback fired when the checkbox loses focus.
181    pub on_blur: Option<Callback<()>>,
182}
183
184impl Checkbox {
185    /// Create a new checkbox.
186    pub fn new(checked: bool) -> Self {
187        Self {
188            state: if checked {
189                CheckboxState::Checked
190            } else {
191                CheckboxState::Unchecked
192            },
193            label: None,
194            variant: CheckboxVariant::Bracket,
195            gap: 1,
196            style: Style::default(),
197            hover_style: StyleSlot::Inherit,
198            focus_style: StyleSlot::Inherit,
199            checked_style: Style::default(),
200            unchecked_style: Style::default(),
201            indeterminate_style: Style::default(),
202            label_style: Style::default(),
203            padding: Padding::default(),
204            disabled: false,
205            disabled_style: Style::default(),
206            width: Length::Auto,
207            height: Length::Auto,
208            on_toggle: None,
209            on_click: None,
210            on_key: None,
211            focusable: true,
212            tab_stop: true,
213            on_focus: None,
214            on_blur: None,
215        }
216    }
217
218    /// Set the checkbox state.
219    pub fn state(mut self, state: CheckboxState) -> Self {
220        self.state = state;
221        self
222    }
223
224    /// Set the checked state.
225    pub fn checked(mut self, checked: bool) -> Self {
226        self.state = if checked {
227            CheckboxState::Checked
228        } else {
229            CheckboxState::Unchecked
230        };
231        self
232    }
233
234    /// Set indeterminate state.
235    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
236        if indeterminate {
237            self.state = CheckboxState::Indeterminate;
238        } else if self.state.is_indeterminate() {
239            self.state = CheckboxState::Unchecked;
240        }
241        self
242    }
243
244    /// Set the label.
245    pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
246        self.label = Some(label.into());
247        self
248    }
249
250    /// Set the visual variant.
251    pub fn variant(mut self, variant: CheckboxVariant) -> Self {
252        self.variant = variant;
253        self
254    }
255
256    /// Set the gap between checkbox and label.
257    pub fn gap(mut self, gap: u16) -> Self {
258        self.gap = gap;
259        self
260    }
261
262    /// Set base style.
263    pub fn style(mut self, style: Style) -> Self {
264        self.style = style;
265        self
266    }
267
268    /// Set hover style.
269    pub fn hover_style(mut self, style: Style) -> Self {
270        self.hover_style = StyleSlot::Replace(style);
271        self
272    }
273
274    /// Extend the themed hover style with the given style.
275    pub fn extend_hover_style(mut self, style: Style) -> Self {
276        self.hover_style = StyleSlot::Extend(style);
277        self
278    }
279
280    /// Inherit hover style from the active theme.
281    pub fn inherit_hover_style(mut self) -> Self {
282        self.hover_style = StyleSlot::Inherit;
283        self
284    }
285
286    /// Set the hover style slot directly.
287    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
288        self.hover_style = slot;
289        self
290    }
291
292    /// Set focus style.
293    pub fn focus_style(mut self, style: Style) -> Self {
294        self.focus_style = StyleSlot::Replace(style);
295        self
296    }
297
298    /// Extend the themed focus style with the given style.
299    pub fn extend_focus_style(mut self, style: Style) -> Self {
300        self.focus_style = StyleSlot::Extend(style);
301        self
302    }
303
304    /// Inherit focus style from the active theme.
305    pub fn inherit_focus_style(mut self) -> Self {
306        self.focus_style = StyleSlot::Inherit;
307        self
308    }
309
310    /// Set the focus style slot directly.
311    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
312        self.focus_style = slot;
313        self
314    }
315
316    /// Set checked state symbol style.
317    pub fn checked_style(mut self, style: Style) -> Self {
318        self.checked_style = style;
319        self
320    }
321
322    /// Set unchecked state symbol style.
323    pub fn unchecked_style(mut self, style: Style) -> Self {
324        self.unchecked_style = style;
325        self
326    }
327
328    /// Set indeterminate state symbol style.
329    pub fn indeterminate_style(mut self, style: Style) -> Self {
330        self.indeterminate_style = style;
331        self
332    }
333
334    /// Set label style.
335    pub fn label_style(mut self, style: Style) -> Self {
336        self.label_style = style;
337        self
338    }
339
340    /// Set padding.
341    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
342        self.padding = padding.into();
343        self
344    }
345
346    /// Set disabled state.
347    pub fn disabled(mut self, disabled: bool) -> Self {
348        self.disabled = disabled;
349        self
350    }
351
352    /// Set disabled style.
353    pub fn disabled_style(mut self, style: Style) -> Self {
354        self.disabled_style = style;
355        self
356    }
357
358    /// Set requested width.
359    pub fn width(mut self, width: Length) -> Self {
360        self.width = width;
361        self
362    }
363
364    /// Set requested height.
365    pub fn height(mut self, height: Length) -> Self {
366        self.height = height;
367        self
368    }
369
370    /// Set toggle handler.
371    pub fn on_toggle(mut self, cb: Callback<CheckboxEvent>) -> Self {
372        self.on_toggle = Some(cb);
373        self
374    }
375
376    /// Set mouse click handler.
377    pub fn on_click(mut self, cb: Callback<MouseEvent>) -> Self {
378        self.on_click = Some(cb);
379        self
380    }
381
382    /// Set keyboard handler.
383    pub fn on_key(mut self, handler: KeyHandler) -> Self {
384        self.on_key = Some(handler);
385        self
386    }
387
388    /// Control whether the node is focusable.
389    pub fn focusable(mut self, focusable: bool) -> Self {
390        self.focusable = focusable;
391        self
392    }
393
394    /// Control whether the checkbox participates in tab traversal.
395    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
396        self.tab_stop = tab_stop;
397        self
398    }
399
400    /// Set the callback fired when the checkbox gains focus.
401    pub fn on_focus(mut self, cb: Callback<()>) -> Self {
402        self.on_focus = Some(cb);
403        self
404    }
405
406    /// Set the callback fired when the checkbox loses focus.
407    pub fn on_blur(mut self, cb: Callback<()>) -> Self {
408        self.on_blur = Some(cb);
409        self
410    }
411}
412
413impl From<Checkbox> for Element {
414    fn from(value: Checkbox) -> Self {
415        Element::new(ElementKind::Checkbox(value))
416    }
417}
418
419impl crate::layout::hash::LayoutHash for Checkbox {
420    fn layout_hash(
421        &self,
422        hasher: &mut impl std::hash::Hasher,
423        _recurse: &dyn Fn(&Element) -> Option<u64>,
424    ) -> Option<()> {
425        use std::hash::Hash;
426        self.width.hash(hasher);
427        self.height.hash(hasher);
428        self.variant.hash(hasher);
429        self.gap.hash(hasher);
430        self.padding.hash(hasher);
431        self.label.hash(hasher);
432        Some(())
433    }
434}