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
14impl_scope! {
15    /// A bare check box (no label)
16    ///
17    /// See also [`CheckButton`] which includes a label.
18    #[autoimpl(Debug ignore self.state_fn, self.on_toggle)]
19    #[widget{
20        navigable = true;
21        hover_highlight = true;
22    }]
23    pub struct CheckBox<A> {
24        core: widget_core!(),
25        state: bool,
26        editable: bool,
27        last_change: Option<Instant>,
28        state_fn: Box<dyn Fn(&ConfigCx, &A) -> bool>,
29        on_toggle: Option<Box<dyn Fn(&mut EventCx, &A, bool)>>,
30    }
31
32    impl Events for Self {
33        type Data = A;
34
35        fn update(&mut self, cx: &mut ConfigCx, data: &A) {
36            let new_state = (self.state_fn)(cx, data);
37            if self.state != new_state {
38                self.state = new_state;
39                self.last_change = Some(Instant::now());
40                cx.redraw(self);
41            }
42        }
43
44        fn handle_event(&mut self, cx: &mut EventCx, data: &A, event: Event) -> IsUsed {
45            event.on_activate(cx, self.id(), |cx| {
46                self.toggle(cx, data);
47                Used
48            })
49        }
50    }
51
52    impl Layout for Self {
53        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
54            sizer.feature(Feature::CheckBox, axis)
55        }
56
57        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
58            let rect = cx.align_feature(Feature::CheckBox, rect, hints.complete_center());
59            self.core.rect = rect;
60        }
61
62        fn draw(&mut self, mut draw: DrawCx) {
63            draw.check_box(self.rect(), self.state, self.last_change);
64        }
65    }
66
67    impl Self {
68        /// Construct a check box
69        ///
70        /// - `state_fn` extracts the current state from input data
71        #[inline]
72        pub fn new(state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static) -> Self {
73            CheckBox {
74                core: Default::default(),
75                state: false,
76                editable: true,
77                last_change: None,
78                state_fn: Box::new(state_fn),
79                on_toggle: None,
80            }
81        }
82
83        /// Call the handler `f` on toggle
84        #[inline]
85        #[must_use]
86        pub fn with(mut self, f: impl Fn(&mut EventCx, &A, bool) + 'static) -> Self {
87            debug_assert!(self.on_toggle.is_none());
88            self.on_toggle = Some(Box::new(f));
89            self
90        }
91
92        /// Send the message generated by `f` on toggle
93        #[inline]
94        #[must_use]
95        pub fn with_msg<M>(self, f: impl Fn(bool) -> M + 'static) -> Self
96        where
97            M: std::fmt::Debug + 'static,
98        {
99            self.with(move |cx, _, state| cx.push(f(state)))
100        }
101
102        /// Construct a check box
103        ///
104        /// - `state_fn` extracts the current state from input data
105        /// - A message generated by `msg_fn` is emitted when toggled
106        #[inline]
107        pub fn new_msg<M: Debug + 'static>(
108            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
109            msg_fn: impl Fn(bool) -> M + 'static,
110        ) -> Self {
111            CheckBox::new(state_fn).with_msg(msg_fn)
112        }
113
114        /// Set whether this widget is editable (inline)
115        #[inline]
116        #[must_use]
117        pub fn with_editable(mut self, editable: bool) -> Self {
118            self.editable = editable;
119            self
120        }
121
122        /// Get whether this widget is editable
123        #[inline]
124        pub fn is_editable(&self) -> bool {
125            self.editable
126        }
127
128        /// Set whether this widget is editable
129        #[inline]
130        pub fn set_editable(&mut self, editable: bool) {
131            self.editable = editable;
132        }
133
134        /// Toggle the check box
135        pub fn toggle(&mut self, cx: &mut EventCx, data: &A) {
136            // Note: do not update self.state; that is the responsibility of update.
137            self.state = !self.state;
138            if let Some(f) = self.on_toggle.as_ref() {
139                // Pass what should be the new value of state here:
140                f(cx, data, self.state);
141            }
142
143            // Do animate (even if state never changes):
144            self.last_change = Some(Instant::now());
145            cx.redraw(self);
146        }
147    }
148}
149
150// Shrink left/right edge to only make portion with text clickable.
151// This is a little hacky since neither Label widgets nor row
152// layouts shrink self due to unused space.
153// We don't shrink vertically since normally that isn't an issue.
154pub(crate) fn shrink_to_text(rect: &mut Rect, direction: Direction, label: &AccessLabel) {
155    if let Ok(bb) = label.text().bounding_box() {
156        match direction {
157            Direction::Right => {
158                let offset = label.rect().pos.0 - rect.pos.0;
159                let text_right: i32 = ((bb.1).0).cast_ceil();
160                rect.size.0 = offset + text_right;
161            }
162            Direction::Left => {
163                let text_left: i32 = ((bb.0).0).cast_floor();
164                rect.pos.0 += text_left;
165                rect.size.0 -= text_left
166            }
167            _ => (),
168        }
169    }
170}
171
172impl_scope! {
173    /// A check button with label
174    ///
175    /// This is a [`CheckBox`] with a label.
176    #[widget{
177        layout = list!(self.direction(), [self.inner, non_navigable!(self.label)]);
178    }]
179    pub struct CheckButton<A> {
180        core: widget_core!(),
181        #[widget]
182        inner: CheckBox<A>,
183        #[widget(&())]
184        label: AccessLabel,
185    }
186
187    impl Layout for Self {
188        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
189            self.core.rect = rect;
190            self.layout_visitor().set_rect(cx, rect, hints);
191            let dir = self.direction();
192            shrink_to_text(&mut self.core.rect, dir, &self.label);
193        }
194
195        fn find_id(&mut self, coord: Coord) -> Option<Id> {
196            self.rect().contains(coord).then(|| self.inner.id())
197        }
198    }
199
200    impl Events for Self {
201        type Data = A;
202
203        fn handle_messages(&mut self, cx: &mut EventCx, data: &Self::Data) {
204            if let Some(kas::messages::Activate(code)) = cx.try_pop() {
205                self.inner.toggle(cx, data);
206                cx.depress_with_key(self.inner.id(), code);
207            }
208        }
209    }
210
211    impl Self {
212        /// Construct a check button with the given `label`
213        ///
214        /// - `label` is displayed to the left or right (according to text direction)
215        /// - `state_fn` extracts the current state from input data
216        #[inline]
217        pub fn new(
218            label: impl Into<AccessString>,
219            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
220        ) -> Self {
221            CheckButton {
222                core: Default::default(),
223                inner: CheckBox::new(state_fn),
224                label: AccessLabel::new(label.into()),
225            }
226        }
227
228        /// Call the handler `f` on toggle
229        #[inline]
230        #[must_use]
231        pub fn with(self, f: impl Fn(&mut EventCx, &A, bool) + 'static) -> Self {
232            CheckButton {
233                core: self.core,
234                inner: self.inner.with(f),
235                label: self.label,
236            }
237        }
238
239        /// Send the message generated by `f` on toggle
240        #[inline]
241        #[must_use]
242        pub fn with_msg<M>(self, f: impl Fn(bool) -> M + 'static) -> Self
243        where
244            M: std::fmt::Debug + 'static,
245        {
246            self.with(move |cx, _, state| cx.push(f(state)))
247        }
248
249        /// Construct a check button with the given `label` and `msg_fn`
250        ///
251        /// - `label` is displayed to the left or right (according to text direction)
252        /// - `state_fn` extracts the current state from input data
253        /// - A message generated by `msg_fn` is emitted when toggled
254        #[inline]
255        pub fn new_msg<M: Debug + 'static>(
256            label: impl Into<AccessString>,
257            state_fn: impl Fn(&ConfigCx, &A) -> bool + 'static,
258            msg_fn: impl Fn(bool) -> M + 'static,
259        ) -> Self {
260            CheckButton::new(label, state_fn).with_msg(msg_fn)
261        }
262
263        /// Set whether this widget is editable (inline)
264        #[inline]
265        #[must_use]
266        pub fn editable(mut self, editable: bool) -> Self {
267            self.inner = self.inner.with_editable(editable);
268            self
269        }
270
271        /// Get whether this widget is editable
272        #[inline]
273        pub fn is_editable(&self) -> bool {
274            self.inner.is_editable()
275        }
276
277        /// Set whether this widget is editable
278        #[inline]
279        pub fn set_editable(&mut self, editable: bool) {
280            self.inner.set_editable(editable);
281        }
282
283        fn direction(&self) -> Direction {
284            match self.label.text().text_is_rtl() {
285                false => Direction::Right,
286                true => Direction::Left,
287            }
288        }
289    }
290}