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