rat_widget/
radio.rs

1///
2/// Radiobutton widget.
3///
4use crate::_private::NonExhaustive;
5use crate::choice::core::ChoiceCore;
6use crate::event::RadioOutcome;
7use crate::util::{block_size, fill_buf_area, revert_style, union_non_empty};
8use rat_event::util::{item_at, MouseFlags};
9use rat_event::{ct_event, HandleEvent, MouseOnly, Regular};
10use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
11use rat_reloc::{relocate_area, relocate_areas, RelocatableState};
12use ratatui::buffer::Buffer;
13use ratatui::layout::{Direction, Rect, Size};
14use ratatui::prelude::{BlockExt, StatefulWidget};
15use ratatui::style::{Style, Stylize};
16use ratatui::text::{Span, Text};
17use ratatui::widgets::{Block, Widget};
18use std::cmp::max;
19use unicode_segmentation::UnicodeSegmentation;
20
21/// Radio style.
22///
23/// This is used, if you don't provide your own layout constraints.
24#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
25pub enum RadioLayout {
26    /// Stacked one item after the other.
27    #[default]
28    Stacked,
29    /// Equally spaced items.
30    Spaced,
31}
32
33/// Horizontally aligned radio buttons.
34#[derive(Debug, Clone)]
35pub struct Radio<'a, T>
36where
37    T: PartialEq + Clone + Default,
38{
39    values: Vec<T>,
40    default_value: Option<T>,
41    items: Vec<Text<'a>>,
42    direction: Direction,
43    layout: RadioLayout,
44
45    true_str: Span<'a>,
46    false_str: Span<'a>,
47    continue_str: Span<'a>,
48
49    style: Style,
50    select_style: Option<Style>,
51    focus_style: Option<Style>,
52    block: Option<Block<'a>>,
53}
54
55/// Composite style.
56#[derive(Debug, Clone)]
57pub struct RadioStyle {
58    /// Radio layout
59    pub layout: Option<RadioLayout>,
60
61    /// Base style.
62    pub style: Style,
63    /// Selected style.
64    pub select: Option<Style>,
65    /// Focused style
66    pub focus: Option<Style>,
67    /// Border
68    pub block: Option<Block<'static>>,
69
70    /// Display text for 'true'
71    pub true_str: Option<Span<'static>>,
72    /// Display text for 'false'
73    pub false_str: Option<Span<'static>>,
74    /// Continue text.
75    pub continue_str: Option<Span<'static>>,
76
77    pub non_exhaustive: NonExhaustive,
78}
79
80/// State
81#[derive(Debug)]
82pub struct RadioState<T = usize>
83where
84    T: PartialEq + Clone + Default,
85{
86    /// Complete area
87    /// __read only__. renewed for each render.
88    pub area: Rect,
89    /// Area inside the block.
90    /// __read only__. renewed for each render.
91    pub inner: Rect,
92
93    /// Area for the focus marker.
94    /// __read only__. renewed for each render.
95    pub marker_area: Rect,
96    /// Area for a continue marker.
97    /// This is displayed if not all items can be displayed.
98    pub continue_area: Rect,
99    /// __read only__. renewed for each render.
100    /// Area of the check marks.
101    /// __read only__. renewed for each render.
102    pub check_areas: Vec<Rect>,
103    /// Area for the texts.
104    /// __read only__. renewed for each render.
105    pub text_areas: Vec<Rect>,
106
107    /// Core
108    pub core: ChoiceCore<T>,
109
110    /// Current focus state.
111    /// __read+write__
112    pub focus: FocusFlag,
113
114    /// Mouse helper
115    /// __read+write__
116    pub mouse: MouseFlags,
117
118    pub non_exhaustive: NonExhaustive,
119}
120
121pub(crate) mod event {
122    use rat_event::{ConsumedEvent, Outcome};
123
124    /// Result value for event-handling.
125    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
126    pub enum RadioOutcome {
127        /// The given event was not handled at all.
128        Continue,
129        /// The event was handled, no repaint necessary.
130        Unchanged,
131        /// The event was handled, repaint necessary.
132        Changed,
133        /// An item has been selected.
134        Value,
135    }
136
137    impl ConsumedEvent for RadioOutcome {
138        fn is_consumed(&self) -> bool {
139            *self != RadioOutcome::Continue
140        }
141    }
142
143    impl From<RadioOutcome> for Outcome {
144        fn from(value: RadioOutcome) -> Self {
145            match value {
146                RadioOutcome::Continue => Outcome::Continue,
147                RadioOutcome::Unchanged => Outcome::Unchanged,
148                RadioOutcome::Changed => Outcome::Changed,
149                RadioOutcome::Value => Outcome::Changed,
150            }
151        }
152    }
153}
154
155impl Default for RadioStyle {
156    fn default() -> Self {
157        Self {
158            layout: None,
159            style: Default::default(),
160            select: None,
161            focus: None,
162            block: Default::default(),
163            true_str: None,
164            false_str: None,
165            continue_str: None,
166            non_exhaustive: NonExhaustive,
167        }
168    }
169}
170
171impl<T> Default for Radio<'_, T>
172where
173    T: PartialEq + Clone + Default,
174{
175    fn default() -> Self {
176        Self {
177            values: Default::default(),
178            items: Default::default(),
179            direction: Default::default(),
180            layout: Default::default(),
181            default_value: Default::default(),
182            true_str: Span::from("\u{2B24}"),
183            false_str: Span::from("\u{25EF}"),
184            continue_str: Span::from("...").on_yellow(),
185            style: Default::default(),
186            select_style: None,
187            focus_style: None,
188            block: None,
189        }
190    }
191}
192
193impl<'a> Radio<'a, usize> {
194    /// Add items with auto-generated values.
195    #[inline]
196    pub fn auto_items<V: Into<Text<'a>>>(mut self, items: impl IntoIterator<Item = V>) -> Self {
197        {
198            self.values.clear();
199            self.items.clear();
200
201            for (k, v) in items.into_iter().enumerate() {
202                self.values.push(k);
203                self.items.push(v.into());
204            }
205        }
206
207        self
208    }
209
210    /// Add an item with an auto generated value.
211    pub fn auto_item(mut self, item: impl Into<Text<'a>>) -> Self {
212        let idx = self.values.len();
213        self.values.push(idx);
214        self.items.push(item.into());
215        self
216    }
217}
218
219impl<'a, T> Radio<'a, T>
220where
221    T: PartialEq + Clone + Default,
222{
223    /// New.
224    pub fn new() -> Self {
225        Self::default()
226    }
227
228    /// Set all styles.
229    pub fn styles(mut self, styles: RadioStyle) -> Self {
230        self.style = styles.style;
231        if let Some(layout) = styles.layout {
232            self.layout = layout;
233        }
234        if styles.focus.is_some() {
235            self.focus_style = styles.focus;
236        }
237        if styles.select.is_some() {
238            self.select_style = styles.focus;
239        }
240        if let Some(block) = styles.block {
241            self.block = Some(block);
242        }
243        if let Some(true_str) = styles.true_str {
244            self.true_str = true_str;
245        }
246        if let Some(false_str) = styles.false_str {
247            self.false_str = false_str;
248        }
249        self.block = self.block.map(|v| v.style(self.style));
250        self
251    }
252
253    /// Set the base-style.
254    #[inline]
255    pub fn style(mut self, style: impl Into<Style>) -> Self {
256        self.style = style.into();
257        self
258    }
259
260    /// Style when selected.
261    #[inline]
262    pub fn select_style(mut self, style: impl Into<Style>) -> Self {
263        self.select_style = Some(style.into());
264        self
265    }
266
267    /// Style when focused.
268    #[inline]
269    pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
270        self.focus_style = Some(style.into());
271        self
272    }
273
274    /// Radio direction
275    #[inline]
276    pub fn direction(mut self, direction: Direction) -> Self {
277        self.direction = direction;
278        self
279    }
280
281    /// Layout type, stacked or evenly spaced.
282    #[inline]
283    pub fn layout(mut self, layout: RadioLayout) -> Self {
284        self.layout = layout;
285        self
286    }
287
288    /// Button text.
289    #[inline]
290    pub fn items<V: Into<Text<'a>>>(mut self, items: impl IntoIterator<Item = (T, V)>) -> Self {
291        {
292            self.values.clear();
293            self.items.clear();
294
295            for (k, v) in items.into_iter() {
296                self.values.push(k);
297                self.items.push(v.into());
298            }
299        }
300
301        self
302    }
303
304    /// Add an item.
305    pub fn item(mut self, value: T, item: impl Into<Text<'a>>) -> Self {
306        self.values.push(value);
307        self.items.push(item.into());
308        self
309    }
310
311    /// Can return to default with user interaction.
312    pub fn default_value(mut self, default: T) -> Self {
313        self.default_value = Some(default);
314        self
315    }
316
317    /// Block.
318    #[inline]
319    pub fn block(mut self, block: Block<'a>) -> Self {
320        self.block = Some(block);
321        self.block = self.block.map(|v| v.style(self.style));
322        self
323    }
324
325    /// Text for true
326    pub fn true_str(mut self, str: Span<'a>) -> Self {
327        self.true_str = str;
328        self
329    }
330
331    /// Text for false
332    pub fn false_str(mut self, str: Span<'a>) -> Self {
333        self.false_str = str;
334        self
335    }
336
337    /// Inherent size
338    pub fn size(&self) -> Size {
339        if self.direction == Direction::Horizontal {
340            self.horizontal_size()
341        } else {
342            self.vertical_size()
343        }
344    }
345
346    /// Inherent width.
347    pub fn width(&self) -> u16 {
348        self.size().width
349    }
350
351    /// Inherent height.
352    pub fn height(&self) -> u16 {
353        self.size().height
354    }
355}
356
357impl<T> Radio<'_, T>
358where
359    T: PartialEq + Clone + Default,
360{
361    /// Length of the check
362    fn check_len(&self) -> u16 {
363        max(
364            self.true_str.content.graphemes(true).count(),
365            self.false_str.content.graphemes(true).count(),
366        ) as u16
367    }
368
369    fn horizontal_size(&self) -> Size {
370        let block_size = block_size(&self.block);
371        let check_len = self.check_len();
372        let marker_len = 2;
373
374        if self.layout == RadioLayout::Spaced {
375            let (max_width, max_height) = self
376                .items
377                .iter()
378                .map(|v| (v.width() as u16, v.height() as u16))
379                .max()
380                .unwrap_or_default();
381            let n = self.items.len() as u16;
382            let spacing = n.saturating_sub(1);
383
384            Size::new(
385                marker_len + n * (check_len + 1 + max_width) + spacing + block_size.width,
386                max_height + block_size.height,
387            )
388        } else {
389            let sum_width = self
390                .items //
391                .iter()
392                .map(|v| v.width() as u16)
393                .sum::<u16>();
394            let max_height = self
395                .items
396                .iter()
397                .map(|v| v.height() as u16)
398                .max()
399                .unwrap_or_default();
400
401            let n = self.items.len() as u16;
402            let spacing = n.saturating_sub(1);
403
404            Size::new(
405                marker_len + n * (check_len + 1) + sum_width + spacing + block_size.width,
406                max_height + block_size.height,
407            )
408        }
409    }
410
411    fn vertical_size(&self) -> Size {
412        let block_size = block_size(&self.block);
413        let check_len = self.check_len();
414        let marker_len = 2;
415
416        if self.layout == RadioLayout::Spaced {
417            let (max_width, max_height) = self
418                .items
419                .iter()
420                .map(|v| (v.width() as u16, v.height() as u16))
421                .max()
422                .unwrap_or_default();
423
424            let n = self.items.len() as u16;
425
426            Size::new(
427                marker_len + check_len + 1 + max_width + block_size.width,
428                n * max_height + block_size.width,
429            )
430        } else {
431            let max_width = self
432                .items
433                .iter()
434                .map(|v| v.width() as u16)
435                .max()
436                .unwrap_or_default();
437
438            let sum_height = self
439                .items //
440                .iter()
441                .map(|v| v.height() as u16)
442                .sum::<u16>();
443
444            Size::new(
445                marker_len + check_len + 1 + max_width + block_size.width,
446                sum_height + block_size.height,
447            )
448        }
449    }
450
451    fn horizontal_spaced_layout(&self, area: Rect, state: &mut RadioState<T>) {
452        state.inner = self.block.inner_if_some(area);
453
454        let check_len = self.check_len();
455        let continue_len = self.continue_str.width() as u16;
456        let n = self.items.len() as u16;
457
458        let text_width = max(
459            7,
460            (state.inner.width.saturating_sub(n * check_len) / n).saturating_sub(1),
461        );
462        let item_width = text_width + check_len + 1;
463
464        state.continue_area = Rect::new(
465            state.inner.right().saturating_sub(continue_len), //
466            state.inner.y,
467            continue_len,
468            1,
469        )
470        .intersection(state.inner);
471
472        state.marker_area = Rect::new(
473            state.inner.x, //
474            state.inner.y,
475            1,
476            state.inner.height,
477        )
478        .intersection(state.inner);
479
480        state.check_areas.clear();
481        state.text_areas.clear();
482
483        let mut need_continue = false;
484        for (i, item) in self.items.iter().enumerate() {
485            let i = i as u16;
486
487            state.check_areas.push(
488                Rect::new(
489                    state.inner.x + 2 + (i * item_width),
490                    state.inner.y,
491                    check_len,
492                    item.height() as u16,
493                )
494                .intersection(state.inner),
495            );
496
497            state.text_areas.push(
498                Rect::new(
499                    state.inner.x + 2 + (i * item_width) + check_len + 1,
500                    state.inner.y,
501                    item.width() as u16,
502                    item.height() as u16,
503                )
504                .intersection(state.inner),
505            );
506
507            need_continue = state.text_areas.last().expect("area").is_empty()
508        }
509
510        if !need_continue {
511            state.continue_area = Rect::new(state.inner.x, state.inner.y, 0, 0);
512        }
513    }
514
515    fn horizontal_stack_layout(&self, area: Rect, state: &mut RadioState<T>) {
516        state.inner = self.block.inner_if_some(area);
517
518        let check_len = self.check_len();
519        let continue_len = self.continue_str.width() as u16;
520
521        state.check_areas.clear();
522        state.text_areas.clear();
523
524        let mut x = state.inner.x;
525
526        state.continue_area = Rect::new(
527            state.inner.right().saturating_sub(continue_len), //
528            state.inner.y,
529            continue_len,
530            1,
531        )
532        .intersection(state.inner);
533
534        state.marker_area = Rect::new(
535            x, //
536            state.inner.y,
537            1,
538            state.inner.height,
539        )
540        .intersection(state.inner);
541        x += 2;
542
543        let mut need_continue = false;
544        for item in self.items.iter() {
545            state.check_areas.push(
546                Rect::new(
547                    x, //
548                    state.inner.y,
549                    check_len,
550                    item.height() as u16,
551                )
552                .intersection(state.inner),
553            );
554
555            x += check_len + 1;
556
557            state.text_areas.push(
558                Rect::new(
559                    x, //
560                    state.inner.y,
561                    item.width() as u16,
562                    item.height() as u16,
563                )
564                .intersection(state.inner),
565            );
566
567            x += item.width() as u16 + 1;
568
569            need_continue = state.text_areas.last().expect("area").is_empty()
570        }
571
572        if !need_continue {
573            state.continue_area = Rect::new(state.inner.x, state.inner.y, 0, 0);
574        }
575    }
576
577    fn vertical_spaced_layout(&self, area: Rect, state: &mut RadioState<T>) {
578        state.inner = self.block.inner_if_some(area);
579
580        let check_len = self.check_len();
581        let n = self.items.len() as u16;
582
583        let text_height = max(1, state.inner.height / n);
584
585        state.continue_area = Rect::new(
586            state.inner.x + 2,
587            state.inner.bottom().saturating_sub(1),
588            state.inner.width.saturating_sub(2),
589            1,
590        )
591        .intersection(state.inner);
592
593        state.marker_area = Rect::new(
594            state.inner.x, //
595            state.inner.y,
596            1,
597            state.inner.height,
598        )
599        .intersection(state.inner);
600
601        state.check_areas.clear();
602        state.text_areas.clear();
603
604        let mut need_continue = false;
605        for (i, item) in self.items.iter().enumerate() {
606            let i = i as u16;
607
608            state.check_areas.push(
609                Rect::new(
610                    state.inner.x + 2,
611                    state.inner.y + (i * text_height),
612                    check_len,
613                    item.height() as u16,
614                )
615                .intersection(state.inner),
616            );
617
618            state.text_areas.push(
619                Rect::new(
620                    state.inner.x + 2 + check_len + 1,
621                    state.inner.y + (i * text_height),
622                    item.width() as u16,
623                    item.height() as u16,
624                )
625                .intersection(state.inner),
626            );
627
628            need_continue = state.text_areas.last().expect("area").is_empty()
629        }
630
631        if !need_continue {
632            state.continue_area = Rect::new(state.inner.x, state.inner.y, 0, 0);
633        }
634    }
635
636    fn vertical_stack_layout(&self, area: Rect, state: &mut RadioState<T>) {
637        state.inner = self.block.inner_if_some(area);
638
639        let check_len = self.check_len();
640
641        state.continue_area = Rect::new(
642            state.inner.x + 2,
643            state.inner.bottom().saturating_sub(1),
644            state.inner.width.saturating_sub(2),
645            1,
646        )
647        .intersection(state.inner);
648
649        state.marker_area = Rect::new(
650            state.inner.x, //
651            state.inner.y,
652            1,
653            state.inner.height,
654        )
655        .intersection(state.inner);
656
657        state.check_areas.clear();
658        state.text_areas.clear();
659
660        let mut need_continue = false;
661        let mut y = state.inner.y;
662        for item in self.items.iter() {
663            state.check_areas.push(
664                Rect::new(
665                    state.inner.x + 2, //
666                    y,
667                    check_len,
668                    item.height() as u16,
669                )
670                .intersection(state.inner),
671            );
672
673            state.text_areas.push(
674                Rect::new(
675                    state.inner.x + 2 + check_len + 1,
676                    y,
677                    item.width() as u16,
678                    item.height() as u16,
679                )
680                .intersection(state.inner),
681            );
682
683            y += item.height() as u16;
684
685            need_continue = state.text_areas.last().expect("area").is_empty()
686        }
687
688        if !need_continue {
689            state.continue_area = Rect::new(state.inner.x, state.inner.y, 0, 0);
690        }
691    }
692}
693
694impl<T> StatefulWidget for Radio<'_, T>
695where
696    T: PartialEq + Clone + Default,
697{
698    type State = RadioState<T>;
699
700    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
701        state.area = area;
702
703        match (self.direction, self.layout) {
704            (Direction::Horizontal, RadioLayout::Stacked) => {
705                self.horizontal_stack_layout(area, state);
706            }
707            (Direction::Horizontal, RadioLayout::Spaced) => {
708                self.horizontal_spaced_layout(area, state);
709            }
710            (Direction::Vertical, RadioLayout::Stacked) => {
711                self.vertical_stack_layout(area, state);
712            }
713            (Direction::Vertical, RadioLayout::Spaced) => {
714                self.vertical_spaced_layout(area, state);
715            }
716        }
717
718        state.core.set_values(self.values);
719        if let Some(default_value) = self.default_value {
720            state.core.set_default_value(Some(default_value));
721        }
722
723        let style = self.style;
724        let focus_style = if let Some(focus_style) = self.focus_style {
725            style.patch(focus_style)
726        } else {
727            revert_style(self.style)
728        };
729        let select_style = if let Some(select_style) = self.select_style {
730            style.patch(select_style)
731        } else {
732            style
733        };
734
735        if self.block.is_some() {
736            self.block.render(area, buf);
737        } else {
738            buf.set_style(state.area, style);
739        }
740
741        if state.is_focused() {
742            buf.set_style(state.marker_area, focus_style);
743        }
744
745        for (i, item) in self.items.iter().enumerate() {
746            if Some(i) == state.core.selected() {
747                buf.set_style(
748                    union_non_empty(state.check_areas[i], state.text_areas[i]),
749                    if state.is_focused() {
750                        focus_style
751                    } else {
752                        select_style
753                    },
754                );
755                (&self.true_str).render(state.check_areas[i], buf);
756            } else {
757                (&self.false_str).render(state.check_areas[i], buf);
758            }
759            item.render(state.text_areas[i], buf);
760        }
761
762        if !state.continue_area.is_empty() {
763            fill_buf_area(buf, state.continue_area, " ", self.style);
764            self.continue_str.render(state.continue_area, buf);
765        }
766    }
767}
768
769impl<T> Clone for RadioState<T>
770where
771    T: PartialEq + Clone + Default,
772{
773    fn clone(&self) -> Self {
774        Self {
775            area: self.area,
776            inner: self.inner,
777            marker_area: self.marker_area,
778            continue_area: self.continue_area,
779            check_areas: self.check_areas.clone(),
780            text_areas: self.text_areas.clone(),
781            core: self.core.clone(),
782            focus: FocusFlag::named(self.focus.name()),
783            mouse: Default::default(),
784            non_exhaustive: NonExhaustive,
785        }
786    }
787}
788
789impl<T> Default for RadioState<T>
790where
791    T: PartialEq + Clone + Default,
792{
793    fn default() -> Self {
794        Self {
795            area: Default::default(),
796            inner: Default::default(),
797            marker_area: Default::default(),
798            continue_area: Default::default(),
799            check_areas: Default::default(),
800            text_areas: Default::default(),
801            core: Default::default(),
802            focus: Default::default(),
803            mouse: Default::default(),
804            non_exhaustive: NonExhaustive,
805        }
806    }
807}
808
809impl<T> HasFocus for RadioState<T>
810where
811    T: PartialEq + Clone + Default,
812{
813    fn build(&self, builder: &mut FocusBuilder) {
814        builder.leaf_widget(self);
815    }
816
817    fn focus(&self) -> FocusFlag {
818        self.focus.clone()
819    }
820
821    fn area(&self) -> Rect {
822        self.area
823    }
824}
825
826impl<T> RelocatableState for RadioState<T>
827where
828    T: PartialEq + Clone + Default,
829{
830    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
831        self.area = relocate_area(self.area, shift, clip);
832        self.inner = relocate_area(self.inner, shift, clip);
833        relocate_areas(self.check_areas.as_mut_slice(), shift, clip);
834        relocate_areas(self.text_areas.as_mut_slice(), shift, clip);
835    }
836}
837
838impl<T> RadioState<T>
839where
840    T: PartialEq + Clone + Default,
841{
842    pub fn new() -> Self {
843        Self::default()
844    }
845
846    pub fn named(name: &str) -> Self {
847        Self {
848            focus: FocusFlag::named(name),
849            ..Default::default()
850        }
851    }
852
853    pub fn is_empty(&self) -> bool {
854        self.text_areas.is_empty()
855    }
856
857    pub fn len(&self) -> usize {
858        self.text_areas.len()
859    }
860
861    /// Set a default-value other than T::default()
862    ///
863    /// The starting value will still be T::default()
864    /// after this. You must call clear() to use this
865    /// default.
866    ///
867    /// This default will be overridden by a default set
868    /// on the widget.
869    pub fn set_default_value(&mut self, default_value: Option<T>) {
870        self.core.set_default_value(default_value);
871    }
872
873    /// A default value.
874    pub fn default_value(&self) -> &Option<T> {
875        self.core.default_value()
876    }
877
878    /// Set the given value.
879    ///
880    /// If the value doesn't exist in the list or the list is
881    /// empty the value will still be set, but selected will be
882    /// None. The list will be empty before the first render, but
883    /// the first thing render will do is set the list of values.
884    /// This will adjust the selected index if possible.
885    /// It's still ok to set a value here that can not be represented.
886    /// As long as there is no user interaction, the same value
887    /// will be returned by value().
888    pub fn set_value(&mut self, value: T) -> bool {
889        self.core.set_value(value)
890    }
891
892    /// Get the selected value or None if no value
893    /// is selected or there are no options.
894    pub fn value(&self) -> T {
895        self.core.value()
896    }
897
898    /// Returns the selected index or None if the
899    /// value is not in the list or the list is empty.
900    ///
901    /// You can still get the value set with set_value() though.
902    pub fn selected(&self) -> Option<usize> {
903        self.core.selected()
904    }
905
906    /// Select the value at index. This will set the value
907    /// to the given index in the value-list. If the index is
908    /// out of bounds or the value-list is empty it will
909    /// set selected to None and leave the value as is.
910    /// The list is empty before the first render so this
911    /// may not work as expected.
912    ///
913    /// The selected index is a best effort artefact, the main
914    /// thing is the value itself.
915    ///
916    /// Use of set_value() is preferred.
917    pub fn select(&mut self, select: usize) -> bool {
918        self.core.set_selected(select)
919    }
920
921    /// Set the default value or T::default()
922    pub fn clear(&mut self) -> bool {
923        self.core.clear()
924    }
925
926    /// Select the next item.
927    #[allow(clippy::should_implement_trait)]
928    pub fn next(&mut self) -> bool {
929        if self.core.values().is_empty() {
930            false // noop
931        } else {
932            if let Some(selected) = self.core.selected() {
933                if selected + 1 >= self.core.values().len() {
934                    self.core.set_selected(0)
935                } else {
936                    self.core.set_selected(selected + 1)
937                }
938            } else {
939                self.core.set_selected(0)
940            }
941        }
942    }
943
944    /// Select the previous item.
945    pub fn prev(&mut self) -> bool {
946        if self.core.values().is_empty() {
947            false // noop
948        } else {
949            if let Some(selected) = self.core.selected() {
950                if selected == 0 {
951                    self.core.set_selected(self.core.values().len() - 1)
952                } else {
953                    self.core.set_selected(selected - 1)
954                }
955            } else {
956                self.core.set_selected(self.core.values().len() - 1)
957            }
958        }
959    }
960}
961
962impl<T> HandleEvent<crossterm::event::Event, Regular, RadioOutcome> for RadioState<T>
963where
964    T: PartialEq + Clone + Default,
965{
966    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> RadioOutcome {
967        let r = if self.is_focused() {
968            match event {
969                ct_event!(keycode press Left) => {
970                    if self.prev() {
971                        RadioOutcome::Value
972                    } else {
973                        RadioOutcome::Unchanged
974                    }
975                }
976                ct_event!(keycode press Right) => {
977                    if self.next() {
978                        RadioOutcome::Value
979                    } else {
980                        RadioOutcome::Unchanged
981                    }
982                }
983                ct_event!(keycode press Up) => {
984                    if self.prev() {
985                        RadioOutcome::Value
986                    } else {
987                        RadioOutcome::Unchanged
988                    }
989                }
990                ct_event!(keycode press Down) => {
991                    if self.next() {
992                        RadioOutcome::Value
993                    } else {
994                        RadioOutcome::Unchanged
995                    }
996                }
997                ct_event!(keycode press Home) => {
998                    if self.select(0) {
999                        RadioOutcome::Value
1000                    } else {
1001                        RadioOutcome::Unchanged
1002                    }
1003                }
1004                ct_event!(keycode press End) => {
1005                    if !self.is_empty() {
1006                        if self.select(self.len() - 1) {
1007                            RadioOutcome::Value
1008                        } else {
1009                            RadioOutcome::Unchanged
1010                        }
1011                    } else {
1012                        RadioOutcome::Unchanged
1013                    }
1014                }
1015                ct_event!(keycode press Delete) | ct_event!(keycode press Backspace) => {
1016                    if self.clear() {
1017                        RadioOutcome::Value
1018                    } else {
1019                        RadioOutcome::Unchanged
1020                    }
1021                }
1022                _ => RadioOutcome::Continue,
1023            }
1024        } else {
1025            RadioOutcome::Continue
1026        };
1027
1028        if r == RadioOutcome::Continue {
1029            HandleEvent::handle(self, event, MouseOnly)
1030        } else {
1031            r
1032        }
1033    }
1034}
1035
1036impl<T> HandleEvent<crossterm::event::Event, MouseOnly, RadioOutcome> for RadioState<T>
1037where
1038    T: PartialEq + Clone + Default,
1039{
1040    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> RadioOutcome {
1041        match event {
1042            ct_event!(mouse any for m) if self.mouse.drag(self.area, m) => {
1043                if let Some(sel) = item_at(self.text_areas.as_slice(), m.column, m.row)
1044                    .or_else(|| item_at(self.check_areas.as_slice(), m.column, m.row))
1045                {
1046                    if self.select(sel) {
1047                        RadioOutcome::Value
1048                    } else {
1049                        RadioOutcome::Unchanged
1050                    }
1051                } else {
1052                    RadioOutcome::Unchanged
1053                }
1054            }
1055            ct_event!(mouse down Left for x,y) if self.area.contains((*x, *y).into()) => {
1056                if let Some(sel) = item_at(self.text_areas.as_slice(), *x, *y)
1057                    .or_else(|| item_at(self.check_areas.as_slice(), *x, *y))
1058                {
1059                    if self.select(sel) {
1060                        RadioOutcome::Value
1061                    } else {
1062                        RadioOutcome::Unchanged
1063                    }
1064                } else {
1065                    RadioOutcome::Unchanged
1066                }
1067            }
1068            _ => RadioOutcome::Continue,
1069        }
1070    }
1071}
1072
1073/// Handle all events.
1074/// Text events are only processed if focus is true.
1075/// Mouse events are processed if they are in range.
1076pub fn handle_events<T: PartialEq + Clone + Default>(
1077    state: &mut RadioState<T>,
1078    focus: bool,
1079    event: &crossterm::event::Event,
1080) -> RadioOutcome {
1081    state.focus.set(focus);
1082    HandleEvent::handle(state, event, Regular)
1083}
1084
1085/// Handle only mouse-events.
1086pub fn handle_mouse_events<T: PartialEq + Clone + Default>(
1087    state: &mut RadioState<T>,
1088    event: &crossterm::event::Event,
1089) -> RadioOutcome {
1090    HandleEvent::handle(state, event, MouseOnly)
1091}