Skip to main content

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, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules {
35            cx.feature(Feature::RadioBox, axis)
36        }
37
38        fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) {
39            let align = hints.complete_center();
40            let rect = cx.align_feature(Feature::RadioBox, rect, align);
41            self.core.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();
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();
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 SizeCx, rect: Rect, hints: AlignHints) {
170            let _ = &self.core; // silence proc-macro warning
171            kas::MacroDefinedLayout::set_rect(self, cx, rect, hints);
172            let dir = self.direction();
173            crate::check_box::shrink_to_text(&mut self.rect(), dir, &self.label);
174        }
175    }
176
177    impl Tile for Self {
178        fn role_child_properties(&self, cx: &mut dyn RoleCx, index: usize) {
179            if index == widget_index!(self.inner) {
180                cx.set_label(self.label.id());
181            }
182        }
183
184        fn nav_next(&self, _: bool, from: Option<usize>) -> Option<usize> {
185            from.xor(Some(widget_index!(self.inner)))
186        }
187    }
188
189    impl Events for Self {
190        type Data = A;
191
192        fn probe(&self, _: Coord) -> Id {
193            self.inner.id()
194        }
195
196        fn post_configure(&mut self, _: &mut ConfigCx) {
197            self.label.set_target(self.inner.id());
198        }
199
200        fn handle_messages(&mut self, cx: &mut EventCx, data: &Self::Data) {
201            if let Some(kas::messages::Activate(code)) = cx.try_pop() {
202                self.inner.select(cx, data);
203                cx.depress_with_key(self.inner.id(), code);
204            }
205        }
206    }
207
208    impl Self {
209        /// Construct a radio button with the given `label`
210        ///
211        /// - `label` is displayed to the left or right (according to text direction)
212        /// - `state_fn` extracts the current state from input data
213        #[inline]
214        pub fn new(
215            label: impl Into<AccessString>,
216            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
217        ) -> Self {
218            RadioButton {
219                core: Default::default(),
220                inner: RadioBox::new(state_fn),
221                label: AccessLabel::new(label.into()),
222            }
223        }
224
225        /// Call the handler `f` on selection
226        ///
227        /// No handler is called on deselection.
228        #[inline]
229        #[must_use]
230        pub fn with(self, f: impl Fn(&mut EventCx, &A) + 'static) -> Self {
231            RadioButton {
232                core: self.core,
233                inner: self.inner.with(f),
234                label: self.label,
235            }
236        }
237
238        /// Construct a radio button
239        ///
240        /// - `label` is displayed to the left or right (according to text direction)
241        /// - `state_fn` extracts the current state from input data
242        /// - A message generated by `msg_fn` is emitted when selected
243        #[inline]
244        pub fn new_msg<M: Debug + 'static>(
245            label: impl Into<AccessString>,
246            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
247            msg_fn: impl Fn() -> M + 'static,
248        ) -> Self {
249            RadioButton::new(label, state_fn).with(move |cx, _| cx.push(msg_fn()))
250        }
251
252        /// Construct a radio button
253        ///
254        /// This radio button expects data of type `A` and will appear set when
255        /// input `data == value`. Additionally, on selection, it will emit a
256        /// copy of `value` as a message.
257        #[inline]
258        pub fn new_value(label: impl Into<AccessString>, value: A) -> Self
259        where
260            A: Clone + Debug + Eq + 'static,
261        {
262            let v2 = value.clone();
263            Self::new(label, move |_, data| *data == value).with(move |cx, _| cx.push(v2.clone()))
264        }
265
266        fn direction(&self) -> Direction {
267            match self.label.text().text_is_rtl() {
268                false => Direction::Right,
269                true => Direction::Left,
270            }
271        }
272    }
273}