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