kas_widgets/
check_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 CheckBox {
16    /// A bare check box (no label)
17    ///
18    /// See also [`CheckButton`] which includes a label.
19    ///
20    /// # Messages
21    ///
22    /// [`kas::messages::Activate`] may be used to toggle the state.
23    #[autoimpl(Debug ignore self.state_fn, self.on_toggle)]
24    #[widget]
25    pub struct CheckBox<A> {
26        core: widget_core!(),
27        state: bool,
28        editable: bool,
29        last_change: Option<Instant>,
30        state_fn: Box<dyn Fn(&ConfigCx, &A) -> bool>,
31        on_toggle: Option<Box<dyn Fn(&mut EventCx, &A, bool)>>,
32    }
33
34    impl Layout for Self {
35        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
36            sizer.feature(Feature::CheckBox, axis)
37        }
38
39        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
40            let rect = cx.align_feature(Feature::CheckBox, rect, hints.complete_center());
41            widget_set_rect!(rect);
42        }
43
44        fn draw(&self, mut draw: DrawCx) {
45            draw.check_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::CheckBox(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: &A, event: Event) -> IsUsed {
74            event.on_click(cx, self.id(), |cx| self.toggle(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.toggle(cx, data);
80                cx.depress_with_key(&self, code);
81            }
82        }
83    }
84
85    impl Self {
86        /// Construct a check 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            CheckBox {
92                core: Default::default(),
93                state: false,
94                editable: true,
95                last_change: None,
96                state_fn: Box::new(state_fn),
97                on_toggle: None,
98            }
99        }
100
101        /// Call the handler `f` on toggle
102        #[inline]
103        #[must_use]
104        pub fn with(mut self, f: impl Fn(&mut EventCx, &A, bool) + 'static) -> Self {
105            debug_assert!(self.on_toggle.is_none());
106            self.on_toggle = Some(Box::new(f));
107            self
108        }
109
110        /// Send the message generated by `f` on toggle
111        #[inline]
112        #[must_use]
113        pub fn with_msg<M>(self, f: impl Fn(bool) -> M + 'static) -> Self
114        where
115            M: std::fmt::Debug + 'static,
116        {
117            self.with(move |cx, _, state| cx.push(f(state)))
118        }
119
120        /// Construct a check box
121        ///
122        /// - `state_fn` extracts the current state from input data
123        /// - A message generated by `msg_fn` is emitted when toggled
124        #[inline]
125        pub fn new_msg<M: Debug + 'static>(
126            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
127            msg_fn: impl Fn(bool) -> M + 'static,
128        ) -> Self {
129            CheckBox::new(state_fn).with_msg(msg_fn)
130        }
131
132        /// Set whether this widget is editable (inline)
133        #[inline]
134        #[must_use]
135        pub fn with_editable(mut self, editable: bool) -> Self {
136            self.editable = editable;
137            self
138        }
139
140        /// Get whether this widget is editable
141        #[inline]
142        pub fn is_editable(&self) -> bool {
143            self.editable
144        }
145
146        /// Set whether this widget is editable
147        #[inline]
148        pub fn set_editable(&mut self, editable: bool) {
149            self.editable = editable;
150        }
151
152        /// Toggle the check box
153        pub fn toggle(&mut self, cx: &mut EventCx, data: &A) {
154            // Note: do not update self.state; that is the responsibility of update.
155            self.state = !self.state;
156            if let Some(f) = self.on_toggle.as_ref() {
157                // Pass what should be the new value of state here:
158                f(cx, data, self.state);
159            }
160
161            // Do animate (even if state never changes):
162            self.last_change = Some(Instant::now());
163            cx.redraw(self);
164        }
165    }
166}
167
168// Shrink left/right edge to only make portion with text clickable.
169// This is a little hacky since neither Label widgets nor row
170// layouts shrink self due to unused space.
171// We don't shrink vertically since normally that isn't an issue.
172pub(crate) fn shrink_to_text(rect: &mut Rect, direction: Direction, label: &AccessLabel) {
173    if let Ok(bb) = label.text().bounding_box() {
174        match direction {
175            Direction::Right => {
176                let offset = label.rect().pos.0 - rect.pos.0;
177                let text_right: i32 = ((bb.1).0).cast_ceil();
178                rect.size.0 = offset + text_right;
179            }
180            Direction::Left => {
181                let text_left: i32 = ((bb.0).0).cast_floor();
182                rect.pos.0 += text_left;
183                rect.size.0 -= text_left
184            }
185            _ => (),
186        }
187    }
188}
189
190#[impl_self]
191mod CheckButton {
192    /// A check button with label
193    ///
194    /// This is a [`CheckBox`] with a label.
195    ///
196    /// # Messages
197    ///
198    /// [`kas::messages::Activate`] may be used to toggle the state.
199    #[widget]
200    #[layout(list![self.inner, self.label].with_direction(self.direction()))]
201    pub struct CheckButton<A> {
202        core: widget_core!(),
203        #[widget]
204        inner: CheckBox<A>,
205        #[widget(&())]
206        label: AccessLabel,
207    }
208
209    impl Layout for Self {
210        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
211            kas::MacroDefinedLayout::set_rect(self, cx, rect, hints);
212            let dir = self.direction();
213            shrink_to_text(&mut self.rect(), dir, &self.label);
214        }
215    }
216
217    impl Tile for Self {
218        fn role_child_properties(&self, cx: &mut dyn RoleCx, index: usize) {
219            if index == widget_index!(self.inner) {
220                cx.set_label(self.label.id());
221            }
222        }
223
224        fn nav_next(&self, _: bool, from: Option<usize>) -> Option<usize> {
225            from.xor(Some(widget_index!(self.inner)))
226        }
227
228        fn probe(&self, _: Coord) -> Id {
229            self.inner.id()
230        }
231    }
232
233    impl Events for Self {
234        type Data = A;
235
236        fn configure_recurse(&mut self, cx: &mut ConfigCx, data: &Self::Data) {
237            let id = self.make_child_id(widget_index!(self.inner));
238            if id.is_valid() {
239                cx.configure(self.inner.as_node(data), id);
240            }
241
242            let id = self.make_child_id(widget_index!(self.label));
243            if id.is_valid() {
244                cx.configure(self.label.as_node(&()), id);
245                self.label.set_target(self.inner.id());
246            }
247        }
248
249        fn handle_messages(&mut self, cx: &mut EventCx, data: &Self::Data) {
250            if let Some(kas::messages::Activate(code)) = cx.try_pop() {
251                self.inner.toggle(cx, data);
252                cx.depress_with_key(self.inner.id(), code);
253            }
254        }
255    }
256
257    impl Self {
258        /// Construct a check button with the given `label`
259        ///
260        /// - `label` is displayed to the left or right (according to text direction)
261        /// - `state_fn` extracts the current state from input data
262        #[inline]
263        pub fn new(
264            label: impl Into<AccessString>,
265            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
266        ) -> Self {
267            CheckButton {
268                core: Default::default(),
269                inner: CheckBox::new(state_fn),
270                label: AccessLabel::new(label.into()),
271            }
272        }
273
274        /// Call the handler `f` on toggle
275        #[inline]
276        #[must_use]
277        pub fn with(self, f: impl Fn(&mut EventCx, &A, bool) + 'static) -> Self {
278            CheckButton {
279                core: self.core,
280                inner: self.inner.with(f),
281                label: self.label,
282            }
283        }
284
285        /// Send the message generated by `f` on toggle
286        #[inline]
287        #[must_use]
288        pub fn with_msg<M>(self, f: impl Fn(bool) -> M + 'static) -> Self
289        where
290            M: std::fmt::Debug + 'static,
291        {
292            self.with(move |cx, _, state| cx.push(f(state)))
293        }
294
295        /// Construct a check button with the given `label` and `msg_fn`
296        ///
297        /// - `label` is displayed to the left or right (according to text direction)
298        /// - `state_fn` extracts the current state from input data
299        /// - A message generated by `msg_fn` is emitted when toggled
300        #[inline]
301        pub fn new_msg<M: Debug + 'static>(
302            label: impl Into<AccessString>,
303            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
304            msg_fn: impl Fn(bool) -> M + 'static,
305        ) -> Self {
306            CheckButton::new(label, state_fn).with_msg(msg_fn)
307        }
308
309        /// Set whether this widget is editable (inline)
310        #[inline]
311        #[must_use]
312        pub fn editable(mut self, editable: bool) -> Self {
313            self.inner = self.inner.with_editable(editable);
314            self
315        }
316
317        /// Get whether this widget is editable
318        #[inline]
319        pub fn is_editable(&self) -> bool {
320            self.inner.is_editable()
321        }
322
323        /// Set whether this widget is editable
324        #[inline]
325        pub fn set_editable(&mut self, editable: bool) {
326            self.inner.set_editable(editable);
327        }
328
329        fn direction(&self) -> Direction {
330            match self.label.text().text_is_rtl() {
331                false => Direction::Right,
332                true => Direction::Left,
333            }
334        }
335    }
336}