kas_widgets/
radio_box.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Toggle widgets
7
8use super::AccessLabel;
9use kas::prelude::*;
10use kas::theme::Feature;
11use std::fmt::Debug;
12use std::time::Instant;
13
14#[impl_self]
15mod RadioBox {
16    /// A bare radio box (no label)
17    ///
18    /// See also [`RadioButton`] which includes a label.
19    ///
20    /// # Messages
21    ///
22    /// [`kas::messages::Activate`] may be used to select this radio button.
23    #[autoimpl(Debug ignore self.state_fn, self.on_select)]
24    #[widget]
25    pub struct RadioBox<A> {
26        core: widget_core!(),
27        state: bool,
28        last_change: Option<Instant>,
29        state_fn: Box<dyn Fn(&ConfigCx, &A) -> bool>,
30        on_select: Option<Box<dyn Fn(&mut EventCx, &A)>>,
31    }
32
33    impl Layout for Self {
34        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
35            sizer.feature(Feature::RadioBox, axis)
36        }
37
38        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
39            let align = hints.complete_center();
40            let rect = cx.align_feature(Feature::RadioBox, rect, align);
41            widget_set_rect!(rect);
42        }
43
44        fn draw(&self, mut draw: DrawCx) {
45            draw.radio_box(self.rect(), self.state, self.last_change);
46        }
47    }
48
49    impl Tile for Self {
50        fn navigable(&self) -> bool {
51            true
52        }
53
54        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
55            Role::RadioButton(self.state)
56        }
57    }
58
59    impl Events for Self {
60        const REDRAW_ON_MOUSE_OVER: bool = true;
61
62        type Data = A;
63
64        fn update(&mut self, cx: &mut ConfigCx, data: &A) {
65            let new_state = (self.state_fn)(cx, data);
66            if self.state != new_state {
67                self.state = new_state;
68                self.last_change = Some(Instant::now());
69                cx.redraw(self);
70            }
71        }
72
73        fn handle_event(&mut self, cx: &mut EventCx, data: &Self::Data, event: Event) -> IsUsed {
74            event.on_click(cx, self.id(), |cx| self.select(cx, data))
75        }
76
77        fn handle_messages(&mut self, cx: &mut EventCx, data: &Self::Data) {
78            if let Some(kas::messages::Activate(code)) = cx.try_pop() {
79                self.select(cx, data);
80                cx.depress_with_key(&self, code);
81            }
82        }
83    }
84
85    impl Self {
86        /// Construct a radio box
87        ///
88        /// - `state_fn` extracts the current state from input data
89        #[inline]
90        pub fn new(state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static) -> Self {
91            RadioBox {
92                core: Default::default(),
93                state: false,
94                last_change: None,
95                state_fn: Box::new(state_fn),
96                on_select: None,
97            }
98        }
99
100        /// Call the handler `f` on selection
101        ///
102        /// No handler is called on deselection.
103        #[inline]
104        #[must_use]
105        pub fn with(mut self, f: impl Fn(&mut EventCx, &A) + 'static) -> Self {
106            debug_assert!(self.on_select.is_none());
107            self.on_select = Some(Box::new(f));
108            self
109        }
110
111        /// Construct a radio box
112        ///
113        /// - `state_fn` extracts the current state from input data
114        /// - A message generated by `msg_fn` is emitted when selected
115        #[inline]
116        pub fn new_msg<M: Debug + 'static>(
117            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
118            msg_fn: impl Fn() -> M + 'static,
119        ) -> Self {
120            RadioBox::new(state_fn).with(move |cx, _| cx.push(msg_fn()))
121        }
122
123        /// Construct a radio box
124        ///
125        /// This radio box expects data of type `A` and will appear set when
126        /// input `data == value`. Additionally, on selection, it will emit a
127        /// copy of `value` as a message.
128        #[inline]
129        pub fn new_value(value: A) -> Self
130        where
131            A: Clone + Debug + Eq + 'static,
132        {
133            let v2 = value.clone();
134            Self::new(move |_, data| *data == value).with(move |cx, _| cx.push(v2.clone()))
135        }
136
137        fn select(&mut self, cx: &mut EventCx, data: &A) {
138            self.state = true;
139            if let Some(ref f) = self.on_select {
140                f(cx, data);
141            }
142
143            self.last_change = Some(Instant::now());
144            cx.redraw(self);
145        }
146    }
147}
148
149#[impl_self]
150mod RadioButton {
151    /// A radio button with label
152    ///
153    /// See also [`RadioBox`] which excludes the label.
154    ///
155    /// # Messages
156    ///
157    /// [`kas::messages::Activate`] may be used to select this radio button.
158    #[widget]
159    #[layout(list![self.inner, self.label].with_direction(self.direction()))]
160    pub struct RadioButton<A> {
161        core: widget_core!(),
162        #[widget]
163        inner: RadioBox<A>,
164        #[widget(&())]
165        label: AccessLabel,
166    }
167
168    impl Layout for Self {
169        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
170            kas::MacroDefinedLayout::set_rect(self, cx, rect, hints);
171            let dir = self.direction();
172            crate::check_box::shrink_to_text(&mut self.rect(), dir, &self.label);
173        }
174    }
175
176    impl Tile for Self {
177        fn role_child_properties(&self, cx: &mut dyn RoleCx, index: usize) {
178            if index == widget_index!(self.inner) {
179                cx.set_label(self.label.id());
180            }
181        }
182
183        fn nav_next(&self, _: bool, from: Option<usize>) -> Option<usize> {
184            from.xor(Some(widget_index!(self.inner)))
185        }
186
187        fn probe(&self, _: Coord) -> Id {
188            self.inner.id()
189        }
190    }
191
192    impl Events for Self {
193        type Data = A;
194
195        fn configure_recurse(&mut self, cx: &mut ConfigCx, data: &Self::Data) {
196            let id = self.make_child_id(widget_index!(self.inner));
197            if id.is_valid() {
198                cx.configure(self.inner.as_node(data), id);
199            }
200
201            let id = self.make_child_id(widget_index!(self.label));
202            if id.is_valid() {
203                cx.configure(self.label.as_node(&()), id);
204                self.label.set_target(self.inner.id());
205            }
206        }
207
208        fn handle_messages(&mut self, cx: &mut EventCx, data: &Self::Data) {
209            if let Some(kas::messages::Activate(code)) = cx.try_pop() {
210                self.inner.select(cx, data);
211                cx.depress_with_key(self.inner.id(), code);
212            }
213        }
214    }
215
216    impl Self {
217        /// Construct a radio button with the given `label`
218        ///
219        /// - `label` is displayed to the left or right (according to text direction)
220        /// - `state_fn` extracts the current state from input data
221        #[inline]
222        pub fn new(
223            label: impl Into<AccessString>,
224            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
225        ) -> Self {
226            RadioButton {
227                core: Default::default(),
228                inner: RadioBox::new(state_fn),
229                label: AccessLabel::new(label.into()),
230            }
231        }
232
233        /// Call the handler `f` on selection
234        ///
235        /// No handler is called on deselection.
236        #[inline]
237        #[must_use]
238        pub fn with(self, f: impl Fn(&mut EventCx, &A) + 'static) -> Self {
239            RadioButton {
240                core: self.core,
241                inner: self.inner.with(f),
242                label: self.label,
243            }
244        }
245
246        /// Construct a radio button
247        ///
248        /// - `label` is displayed to the left or right (according to text direction)
249        /// - `state_fn` extracts the current state from input data
250        /// - A message generated by `msg_fn` is emitted when selected
251        #[inline]
252        pub fn new_msg<M: Debug + 'static>(
253            label: impl Into<AccessString>,
254            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
255            msg_fn: impl Fn() -> M + 'static,
256        ) -> Self {
257            RadioButton::new(label, state_fn).with(move |cx, _| cx.push(msg_fn()))
258        }
259
260        /// Construct a radio button
261        ///
262        /// This radio button expects data of type `A` and will appear set when
263        /// input `data == value`. Additionally, on selection, it will emit a
264        /// copy of `value` as a message.
265        #[inline]
266        pub fn new_value(label: impl Into<AccessString>, value: A) -> Self
267        where
268            A: Clone + Debug + Eq + 'static,
269        {
270            let v2 = value.clone();
271            Self::new(label, move |_, data| *data == value).with(move |cx, _| cx.push(v2.clone()))
272        }
273
274        fn direction(&self) -> Direction {
275            match self.label.text().text_is_rtl() {
276                false => Direction::Right,
277                true => Direction::Left,
278            }
279        }
280    }
281}