Skip to main content

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, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules {
36            cx.feature(Feature::CheckBox, axis)
37        }
38
39        fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) {
40            let rect = cx.align_feature(Feature::CheckBox, rect, hints.complete_center());
41            self.core.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();
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();
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 SizeCx, rect: Rect, hints: AlignHints) {
211            let _ = &self.core; // silence proc-macro warning
212            kas::MacroDefinedLayout::set_rect(self, cx, rect, hints);
213            let dir = self.direction();
214            shrink_to_text(&mut self.rect(), dir, &self.label);
215        }
216    }
217
218    impl Tile for Self {
219        fn role_child_properties(&self, cx: &mut dyn RoleCx, index: usize) {
220            if index == widget_index!(self.inner) {
221                cx.set_label(self.label.id());
222            }
223        }
224
225        fn nav_next(&self, _: bool, from: Option<usize>) -> Option<usize> {
226            from.xor(Some(widget_index!(self.inner)))
227        }
228    }
229
230    impl Events for Self {
231        type Data = A;
232
233        fn probe(&self, _: Coord) -> Id {
234            self.inner.id()
235        }
236
237        fn post_configure(&mut self, _: &mut ConfigCx) {
238            self.label.set_target(self.inner.id());
239        }
240
241        fn handle_messages(&mut self, cx: &mut EventCx, data: &Self::Data) {
242            if let Some(kas::messages::Activate(code)) = cx.try_pop() {
243                self.inner.toggle(cx, data);
244                cx.depress_with_key(self.inner.id(), code);
245            }
246        }
247    }
248
249    impl Self {
250        /// Construct a check button with the given `label`
251        ///
252        /// - `label` is displayed to the left or right (according to text direction)
253        /// - `state_fn` extracts the current state from input data
254        #[inline]
255        pub fn new(
256            label: impl Into<AccessString>,
257            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
258        ) -> Self {
259            CheckButton {
260                core: Default::default(),
261                inner: CheckBox::new(state_fn),
262                label: AccessLabel::new(label.into()),
263            }
264        }
265
266        /// Call the handler `f` on toggle
267        #[inline]
268        #[must_use]
269        pub fn with(self, f: impl Fn(&mut EventCx, &A, bool) + 'static) -> Self {
270            CheckButton {
271                core: self.core,
272                inner: self.inner.with(f),
273                label: self.label,
274            }
275        }
276
277        /// Send the message generated by `f` on toggle
278        #[inline]
279        #[must_use]
280        pub fn with_msg<M>(self, f: impl Fn(bool) -> M + 'static) -> Self
281        where
282            M: std::fmt::Debug + 'static,
283        {
284            self.with(move |cx, _, state| cx.push(f(state)))
285        }
286
287        /// Construct a check button with the given `label` and `msg_fn`
288        ///
289        /// - `label` is displayed to the left or right (according to text direction)
290        /// - `state_fn` extracts the current state from input data
291        /// - A message generated by `msg_fn` is emitted when toggled
292        #[inline]
293        pub fn new_msg<M: Debug + 'static>(
294            label: impl Into<AccessString>,
295            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
296            msg_fn: impl Fn(bool) -> M + 'static,
297        ) -> Self {
298            CheckButton::new(label, state_fn).with_msg(msg_fn)
299        }
300
301        /// Set whether this widget is editable (inline)
302        #[inline]
303        #[must_use]
304        pub fn editable(mut self, editable: bool) -> Self {
305            self.inner = self.inner.with_editable(editable);
306            self
307        }
308
309        /// Get whether this widget is editable
310        #[inline]
311        pub fn is_editable(&self) -> bool {
312            self.inner.is_editable()
313        }
314
315        /// Set whether this widget is editable
316        #[inline]
317        pub fn set_editable(&mut self, editable: bool) {
318            self.inner.set_editable(editable);
319        }
320
321        fn direction(&self) -> Direction {
322            match self.label.text().text_is_rtl() {
323                false => Direction::Right,
324                true => Direction::Left,
325            }
326        }
327    }
328}