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, StatefulWidget, Text, Widget};
33use ratatui::style::Style;
34use ratatui::text::Span;
35use ratatui::widgets::Block;
36#[cfg(feature = "unstable-widget-ref")]
37use ratatui::widgets::StatefulWidgetRef;
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
273#[cfg(feature = "unstable-widget-ref")]
274impl<'a> StatefulWidgetRef for Checkbox<'a> {
275    type State = CheckboxState;
276
277    fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
278        render_ref(self, area, buf, state);
279    }
280}
281
282impl StatefulWidget for Checkbox<'_> {
283    type State = CheckboxState;
284
285    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
286        render_ref(&self, area, buf, state);
287    }
288}
289
290fn render_ref(widget: &Checkbox<'_>, area: Rect, buf: &mut Buffer, state: &mut CheckboxState) {
291    state.area = area;
292    state.inner = widget.block.inner_if_some(area);
293
294    let chk_len = widget.check_len();
295    state.check_area = Rect::new(state.inner.x, state.inner.y, chk_len, 1);
296    state.text_area = Rect::new(
297        state.inner.x + chk_len + 1,
298        state.inner.y,
299        state.inner.width.saturating_sub(chk_len + 1),
300        state.inner.height,
301    );
302
303    if let Some(checked) = widget.checked {
304        state.checked = checked;
305    }
306    if let Some(default) = widget.default {
307        state.default = default;
308    }
309
310    let style = widget.style;
311    let focus_style = if let Some(focus_style) = widget.focus_style {
312        style.patch(focus_style)
313    } else {
314        revert_style(style)
315    };
316
317    if widget.block.is_some() {
318        widget.block.render(area, buf);
319        if state.focus.get() {
320            buf.set_style(state.inner, focus_style);
321        }
322    } else {
323        if state.focus.get() {
324            buf.set_style(state.inner, focus_style);
325        } else {
326            buf.set_style(state.inner, widget.style);
327        }
328    }
329
330    let cc = if state.checked {
331        &widget.true_str
332    } else {
333        &widget.false_str
334    };
335    cc.render(state.check_area, buf);
336    (&widget.text).render(state.text_area, buf);
337}
338
339impl Clone for CheckboxState {
340    fn clone(&self) -> Self {
341        Self {
342            area: self.area,
343            inner: self.inner,
344            check_area: self.check_area,
345            text_area: self.text_area,
346            checked: self.checked,
347            default: self.default,
348            focus: FocusFlag::named(self.focus.name()),
349            mouse: Default::default(),
350            non_exhaustive: NonExhaustive,
351        }
352    }
353}
354
355impl Default for CheckboxState {
356    fn default() -> Self {
357        Self {
358            area: Default::default(),
359            inner: Default::default(),
360            check_area: Default::default(),
361            text_area: Default::default(),
362            checked: false,
363            default: false,
364            focus: Default::default(),
365            mouse: Default::default(),
366            non_exhaustive: NonExhaustive,
367        }
368    }
369}
370
371impl HasFocus for CheckboxState {
372    fn build(&self, builder: &mut FocusBuilder) {
373        builder.leaf_widget(self);
374    }
375
376    fn focus(&self) -> FocusFlag {
377        self.focus.clone()
378    }
379
380    fn area(&self) -> Rect {
381        self.area
382    }
383}
384
385impl RelocatableState for CheckboxState {
386    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
387        self.area = relocate_area(self.area, shift, clip);
388        self.inner = relocate_area(self.inner, shift, clip);
389    }
390}
391
392impl CheckboxState {
393    pub fn new() -> Self {
394        Self::default()
395    }
396
397    pub fn named(name: &str) -> Self {
398        Self {
399            focus: FocusFlag::named(name),
400            ..Default::default()
401        }
402    }
403
404    /// Get the value.
405    pub fn checked(&self) -> bool {
406        self.checked
407    }
408
409    /// Set the value.
410    pub fn set_checked(&mut self, checked: bool) -> bool {
411        let old_value = self.checked;
412        self.checked = checked;
413        old_value != self.checked
414    }
415
416    /// Get the default value.
417    pub fn default_(&self) -> bool {
418        self.default
419    }
420
421    /// Set the default value.
422    pub fn set_default(&mut self, default: bool) -> bool {
423        let old_value = self.default;
424        self.default = default;
425        old_value != self.default
426    }
427
428    /// Get the checked value, disregarding of the default state.
429    pub fn value(&self) -> bool {
430        self.checked
431    }
432
433    /// Set checked value. Always sets default to false.
434    pub fn set_value(&mut self, checked: bool) -> bool {
435        let old_value = self.checked;
436        self.checked = checked;
437        old_value != self.checked
438    }
439
440    /// Flip the checkbox.
441    /// If it was in default state it just switches off
442    /// the default flag. Otherwise, it flips true/false.
443    pub fn flip_checked(&mut self) {
444        self.checked = !self.checked;
445    }
446}
447
448impl HandleEvent<crossterm::event::Event, Regular, CheckOutcome> for CheckboxState {
449    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CheckOutcome {
450        let r = if self.is_focused() {
451            match event {
452                ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
453                    self.flip_checked();
454                    CheckOutcome::Value
455                }
456                ct_event!(keycode press Backspace) | ct_event!(keycode press Delete) => {
457                    self.set_value(self.default);
458                    CheckOutcome::Value
459                }
460                _ => CheckOutcome::Continue,
461            }
462        } else {
463            CheckOutcome::Continue
464        };
465
466        if r == CheckOutcome::Continue {
467            HandleEvent::handle(self, event, MouseOnly)
468        } else {
469            r
470        }
471    }
472}
473
474impl HandleEvent<crossterm::event::Event, MouseOnly, CheckOutcome> for CheckboxState {
475    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> CheckOutcome {
476        match event {
477            ct_event!(mouse any for m) if self.mouse.doubleclick(self.area, m) => {
478                self.flip_checked();
479                CheckOutcome::Value
480            }
481            _ => CheckOutcome::Continue,
482        }
483    }
484}
485
486/// Handle all events.
487/// Text events are only processed if focus is true.
488/// Mouse events are processed if they are in range.
489pub fn handle_events(
490    state: &mut CheckboxState,
491    focus: bool,
492    event: &crossterm::event::Event,
493) -> CheckOutcome {
494    state.focus.set(focus);
495    HandleEvent::handle(state, event, Regular)
496}
497
498/// Handle only mouse-events.
499pub fn handle_mouse_events(
500    state: &mut CheckboxState,
501    event: &crossterm::event::Event,
502) -> CheckOutcome {
503    HandleEvent::handle(state, event, MouseOnly)
504}