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