rat_widget/
checkbox.rs

1//!
2//! Checkbox widget.
3//!
4//! Can use a third optional/defaulted state too.
5//!
6//! ```rust ignore
7//! use rat_widget::checkbox::{Checkbox, CheckboxState};
8//! use ratatui::widgets::StatefulWidget;
9//!
10//! Checkbox::new()
11//!     .text("Carrots 🥕")
12//!     .default_settable()
13//!     .styles(THEME.checkbox_style())
14//!     .render(layout[1][1], frame.buffer_mut(), &mut state.c1);
15//!
16//! Checkbox::new()
17//!     .text("Potatoes 🥔\nTomatoes 🍅")
18//!     .default_settable()
19//!     .styles(THEME.checkbox_style())
20//!     .render(layout[1][2], frame.buffer_mut(), &mut state.c2);
21//! ```
22//!
23use crate::_private::NonExhaustive;
24use crate::checkbox::event::CheckOutcome;
25use crate::text::HasScreenCursor;
26use crate::util::{block_size, revert_style};
27use rat_event::util::MouseFlags;
28use rat_event::{HandleEvent, MouseOnly, Regular, ct_event};
29use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
30use rat_reloc::RelocatableState;
31use ratatui::buffer::Buffer;
32use ratatui::layout::Rect;
33use ratatui::prelude::BlockExt;
34use ratatui::style::Style;
35use ratatui::text::Span;
36use ratatui::text::Text;
37use ratatui::widgets::Block;
38use ratatui::widgets::{StatefulWidget, Widget};
39use std::cmp::max;
40use unicode_segmentation::UnicodeSegmentation;
41
42/// Enum controling the behaviour of the Checkbox.
43#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
44pub enum CheckboxCheck {
45    SingleClick,
46    #[default]
47    DoubleClick,
48}
49
50/// Checkbox widget.
51#[derive(Debug, Clone)]
52pub struct Checkbox<'a> {
53    text: Text<'a>,
54
55    // Check state override.
56    checked: Option<bool>,
57    default: Option<bool>,
58
59    true_str: Span<'a>,
60    false_str: Span<'a>,
61
62    behave_check: CheckboxCheck,
63
64    style: Style,
65    focus_style: Option<Style>,
66    block: Option<Block<'a>>,
67}
68
69/// Composite style.
70#[derive(Debug, Clone)]
71pub struct CheckboxStyle {
72    /// Base style.
73    pub style: Style,
74    /// Focused style
75    pub focus: Option<Style>,
76    /// Border
77    pub block: Option<Block<'static>>,
78
79    /// Display text for 'true'
80    pub true_str: Option<Span<'static>>,
81    /// Display text for 'false'
82    pub false_str: Option<Span<'static>>,
83
84    pub behave_check: Option<CheckboxCheck>,
85
86    pub non_exhaustive: NonExhaustive,
87}
88
89/// State.
90#[derive(Debug)]
91pub struct CheckboxState {
92    /// Complete area
93    /// __read only__. renewed for each render.
94    pub area: Rect,
95    /// Area inside the block.
96    /// __read only__. renewed for each render.
97    pub inner: Rect,
98    /// Area of the check mark.
99    /// __read only__. renewed for each render.
100    pub check_area: Rect,
101    /// Area for the text.
102    /// __read only__. renewed for each render.
103    pub text_area: Rect,
104    /// Behaviour for check.
105    /// __read only__. renewed for each render.
106    pub behave_check: CheckboxCheck,
107
108    /// Checked state.
109    /// __read+write__
110    pub checked: bool,
111
112    /// Default state.
113    /// __read+write__ Maybe overriden by a default set for the widget.
114    pub default: bool,
115
116    /// Current focus state.
117    /// __read+write__
118    pub focus: FocusFlag,
119
120    /// Mouse helper
121    /// __read+write__
122    pub mouse: MouseFlags,
123
124    pub non_exhaustive: NonExhaustive,
125}
126
127pub(crate) mod event {
128    use rat_event::{ConsumedEvent, Outcome};
129
130    /// Result value for event-handling.
131    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
132    pub enum CheckOutcome {
133        /// The given event was not handled at all.
134        Continue,
135        /// The event was handled, no repaint necessary.
136        Unchanged,
137        /// The event was handled, repaint necessary.
138        Changed,
139        /// Checkbox has been checked or unchecked.
140        Value,
141    }
142
143    impl ConsumedEvent for CheckOutcome {
144        fn is_consumed(&self) -> bool {
145            *self != CheckOutcome::Continue
146        }
147    }
148
149    impl From<CheckOutcome> for Outcome {
150        fn from(value: CheckOutcome) -> Self {
151            match value {
152                CheckOutcome::Continue => Outcome::Continue,
153                CheckOutcome::Unchanged => Outcome::Unchanged,
154                CheckOutcome::Changed => Outcome::Changed,
155                CheckOutcome::Value => Outcome::Changed,
156            }
157        }
158    }
159}
160
161impl Default for CheckboxStyle {
162    fn default() -> Self {
163        Self {
164            style: Default::default(),
165            focus: Default::default(),
166            block: Default::default(),
167            true_str: Default::default(),
168            false_str: Default::default(),
169            behave_check: Default::default(),
170            non_exhaustive: NonExhaustive,
171        }
172    }
173}
174
175impl Default for Checkbox<'_> {
176    fn default() -> Self {
177        Self {
178            text: Default::default(),
179            checked: Default::default(),
180            default: Default::default(),
181            true_str: Span::from("[\u{2713}]"),
182            false_str: Span::from("[ ]"),
183            behave_check: Default::default(),
184            style: Default::default(),
185            focus_style: Default::default(),
186            block: Default::default(),
187        }
188    }
189}
190
191impl<'a> Checkbox<'a> {
192    /// New.
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    /// Set all styles.
198    pub fn styles(mut self, styles: CheckboxStyle) -> Self {
199        self.style = styles.style;
200        if styles.focus.is_some() {
201            self.focus_style = styles.focus;
202        }
203        if let Some(block) = styles.block {
204            self.block = Some(block);
205        }
206        if let Some(true_str) = styles.true_str {
207            self.true_str = true_str;
208        }
209        if let Some(false_str) = styles.false_str {
210            self.false_str = false_str;
211        }
212        if let Some(check) = styles.behave_check {
213            self.behave_check = check;
214        }
215        self.block = self.block.map(|v| v.style(self.style));
216        self
217    }
218
219    /// Set the base-style.
220    #[inline]
221    pub fn style(mut self, style: impl Into<Style>) -> Self {
222        self.style = style.into();
223        self
224    }
225
226    /// Style when focused.
227    #[inline]
228    pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
229        self.focus_style = Some(style.into());
230        self
231    }
232
233    /// Button text.
234    #[inline]
235    pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
236        self.text = text.into();
237        self
238    }
239
240    /// Checked state. If set overrides the value from the state.
241    pub fn checked(mut self, checked: bool) -> Self {
242        self.checked = Some(checked);
243        self
244    }
245
246    /// Default state. If set overrides the value from the state.
247    pub fn default_(mut self, default: bool) -> Self {
248        self.default = Some(default);
249        self
250    }
251
252    /// Block.
253    #[inline]
254    pub fn block(mut self, block: Block<'a>) -> Self {
255        self.block = Some(block);
256        self.block = self.block.map(|v| v.style(self.style));
257        self
258    }
259
260    /// Text for true
261    pub fn true_str(mut self, str: Span<'a>) -> Self {
262        self.true_str = str;
263        self
264    }
265
266    /// Text for false
267    pub fn false_str(mut self, str: Span<'a>) -> Self {
268        self.false_str = str;
269        self
270    }
271
272    /// Sets the behaviour for selecting from the list.
273    pub fn behave_check(mut self, check: CheckboxCheck) -> Self {
274        self.behave_check = check;
275        self
276    }
277
278    /// Length of the check
279    fn check_len(&self) -> u16 {
280        max(
281            self.true_str.content.graphemes(true).count(),
282            self.false_str.content.graphemes(true).count(),
283        ) as u16
284    }
285
286    /// Inherent width.
287    pub fn width(&self) -> u16 {
288        let chk_len = self.check_len();
289        let txt_len = self.text.width() as u16;
290
291        chk_len + 1 + txt_len + block_size(&self.block).width
292    }
293
294    /// Inherent height.
295    pub fn height(&self) -> u16 {
296        self.text.height() as u16 + block_size(&self.block).height
297    }
298}
299
300impl<'a> StatefulWidget for &Checkbox<'a> {
301    type State = CheckboxState;
302
303    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
304        render_ref(self, area, buf, state);
305    }
306}
307
308impl StatefulWidget for Checkbox<'_> {
309    type State = CheckboxState;
310
311    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
312        render_ref(&self, area, buf, state);
313    }
314}
315
316fn render_ref(widget: &Checkbox<'_>, area: Rect, buf: &mut Buffer, state: &mut CheckboxState) {
317    state.area = area;
318    state.inner = widget.block.inner_if_some(area);
319    state.behave_check = widget.behave_check;
320
321    let chk_len = widget.check_len();
322    state.check_area = Rect::new(state.inner.x, state.inner.y, chk_len, 1);
323    state.text_area = Rect::new(
324        state.inner.x + chk_len + 1,
325        state.inner.y,
326        state.inner.width.saturating_sub(chk_len + 1),
327        state.inner.height,
328    );
329
330    if let Some(checked) = widget.checked {
331        state.checked = checked;
332    }
333    if let Some(default) = widget.default {
334        state.default = default;
335    }
336
337    let style = widget.style;
338    let focus_style = if let Some(focus_style) = widget.focus_style {
339        style.patch(focus_style)
340    } else {
341        revert_style(style)
342    };
343
344    if let Some(block) = &widget.block {
345        block.render(area, buf);
346        if state.focus.get() {
347            buf.set_style(state.inner, focus_style);
348        }
349    } else {
350        if state.focus.get() {
351            buf.set_style(state.inner, focus_style);
352        } else {
353            buf.set_style(state.inner, widget.style);
354        }
355    }
356
357    let cc = if state.checked {
358        &widget.true_str
359    } else {
360        &widget.false_str
361    };
362    cc.render(state.check_area, buf);
363    (&widget.text).render(state.text_area, buf);
364}
365
366impl Clone for CheckboxState {
367    fn clone(&self) -> Self {
368        Self {
369            area: self.area,
370            inner: self.inner,
371            check_area: self.check_area,
372            text_area: self.text_area,
373            behave_check: self.behave_check,
374            checked: self.checked,
375            default: self.default,
376            focus: self.focus.new_instance(),
377            mouse: Default::default(),
378            non_exhaustive: NonExhaustive,
379        }
380    }
381}
382
383impl Default for CheckboxState {
384    fn default() -> Self {
385        Self {
386            area: Default::default(),
387            inner: Default::default(),
388            check_area: Default::default(),
389            text_area: Default::default(),
390            behave_check: Default::default(),
391            checked: false,
392            default: false,
393            focus: Default::default(),
394            mouse: Default::default(),
395            non_exhaustive: NonExhaustive,
396        }
397    }
398}
399
400impl HasFocus for CheckboxState {
401    fn build(&self, builder: &mut FocusBuilder) {
402        builder.leaf_widget(self);
403    }
404
405    fn focus(&self) -> FocusFlag {
406        self.focus.clone()
407    }
408
409    fn area(&self) -> Rect {
410        self.area
411    }
412}
413
414impl HasScreenCursor for CheckboxState {
415    fn screen_cursor(&self) -> Option<(u16, u16)> {
416        None
417    }
418}
419
420impl RelocatableState for CheckboxState {
421    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
422        self.area.relocate(shift, clip);
423        self.inner.relocate(shift, clip);
424        self.check_area.relocate(shift, clip);
425    }
426}
427
428impl CheckboxState {
429    pub fn new() -> Self {
430        Self::default()
431    }
432
433    pub fn named(name: &str) -> Self {
434        let mut z = Self::default();
435        z.focus = z.focus.with_name(name);
436        z
437    }
438
439    /// Get the value.
440    pub fn checked(&self) -> bool {
441        self.checked
442    }
443
444    /// Set the value.
445    pub fn set_checked(&mut self, checked: bool) -> bool {
446        let old_value = self.checked;
447        self.checked = checked;
448        old_value != self.checked
449    }
450
451    /// Get the default value.
452    pub fn default_(&self) -> bool {
453        self.default
454    }
455
456    /// Set the default value.
457    pub fn set_default(&mut self, default: bool) -> bool {
458        let old_value = self.default;
459        self.default = default;
460        old_value != self.default
461    }
462
463    /// Get the checked value, disregarding of the default state.
464    pub fn value(&self) -> bool {
465        self.checked
466    }
467
468    /// Set checked value. Always sets default to false.
469    pub fn set_value(&mut self, checked: bool) -> bool {
470        let old_value = self.checked;
471        self.checked = checked;
472        old_value != self.checked
473    }
474
475    /// Reset to default.
476    pub fn clear(&mut self) {
477        self.checked = self.default;
478    }
479
480    /// Flip the checkbox.
481    /// If it was in default state it just switches off
482    /// the default flag. Otherwise, it flips true/false.
483    pub fn flip_checked(&mut self) {
484        self.checked = !self.checked;
485    }
486}
487
488impl HandleEvent<crossterm::event::Event, Regular, CheckOutcome> for CheckboxState {
489    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CheckOutcome {
490        let r = if self.is_focused() {
491            match event {
492                ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
493                    self.flip_checked();
494                    CheckOutcome::Value
495                }
496                ct_event!(keycode press Backspace) | ct_event!(keycode press Delete) => {
497                    self.set_value(self.default);
498                    CheckOutcome::Value
499                }
500                _ => CheckOutcome::Continue,
501            }
502        } else {
503            CheckOutcome::Continue
504        };
505
506        if r == CheckOutcome::Continue {
507            HandleEvent::handle(self, event, MouseOnly)
508        } else {
509            r
510        }
511    }
512}
513
514impl HandleEvent<crossterm::event::Event, MouseOnly, CheckOutcome> for CheckboxState {
515    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> CheckOutcome {
516        match event {
517            ct_event!(mouse any for m)
518                if self.behave_check == CheckboxCheck::DoubleClick
519                    && self.mouse.doubleclick(self.area, m) =>
520            {
521                self.flip_checked();
522                CheckOutcome::Value
523            }
524            ct_event!(mouse down Left for x,y)
525                if self.behave_check == CheckboxCheck::SingleClick
526                    && self.area.contains((*x, *y).into()) =>
527            {
528                self.flip_checked();
529                CheckOutcome::Value
530            }
531            _ => CheckOutcome::Continue,
532        }
533    }
534}
535
536/// Handle all events.
537/// Text events are only processed if focus is true.
538/// Mouse events are processed if they are in range.
539pub fn handle_events(
540    state: &mut CheckboxState,
541    focus: bool,
542    event: &crossterm::event::Event,
543) -> CheckOutcome {
544    state.focus.set(focus);
545    HandleEvent::handle(state, event, Regular)
546}
547
548/// Handle only mouse-events.
549pub fn handle_mouse_events(
550    state: &mut CheckboxState,
551    event: &crossterm::event::Event,
552) -> CheckOutcome {
553    HandleEvent::handle(state, event, MouseOnly)
554}