embedded_ui/kit/
checkbox.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use embedded_graphics::geometry::Point;
4use embedded_graphics::primitives::Rectangle;
5
6use crate::color::UiColor;
7use crate::el::{El, ElId};
8use crate::event::{Capture, CommonEvent, Event, Propagate};
9use crate::icons::IconKind;
10use crate::layout::{Layout, Viewport};
11use crate::padding::Padding;
12use crate::size::{Length, Size};
13use crate::state::{State, StateNode, StateTag};
14use crate::style::component_style;
15use crate::ui::UiCtx;
16use crate::widget::Widget;
17use crate::{block::Border, render::Renderer};
18
19use super::icon::Icon;
20
21// #[derive(Clone, Copy)]
22// pub enum CheckboxSign {
23//     Check,
24//     Dot,
25//     Cross,
26// }
27
28#[derive(Clone, Copy)]
29pub struct CheckboxState {
30    is_pressed: bool,
31    is_checked: bool,
32}
33
34impl Default for CheckboxState {
35    fn default() -> Self {
36        Self { is_pressed: false, is_checked: false }
37    }
38}
39
40#[derive(Clone, Copy)]
41pub enum CheckboxStatus {
42    Normal,
43    Pressed,
44    Focused,
45
46    // TODO: Think about compound statuses such as FocusedChecked, PressedChecked, etc.
47    Checked,
48}
49
50component_style! {
51    pub CheckboxStyle: CheckboxStyler(CheckboxStatus) {
52        background: background,
53        border: border,
54    }
55}
56
57pub struct Checkbox<'a, Message, R, S>
58where
59    R: Renderer,
60    S: CheckboxStyler<R::Color>,
61{
62    id: ElId,
63    check_icon: Icon<R>,
64    size: Size<Length>,
65    on_change: Box<dyn Fn(bool) -> Message + 'a>,
66    class: S::Class<'a>,
67}
68
69impl<'a, Message, R, S> Checkbox<'a, Message, R, S>
70where
71    R: Renderer,
72    S: CheckboxStyler<R::Color>,
73{
74    pub fn new<F>(on_change: F) -> Self
75    where
76        F: 'a + Fn(bool) -> Message,
77    {
78        Self {
79            id: ElId::unique(),
80            check_icon: Icon::new(crate::icons::IconKind::Check),
81            size: Size::fill(),
82            on_change: Box::new(on_change),
83            class: S::default(),
84            // color: R::Color::default_foreground(),
85        }
86    }
87
88    // pub fn sign(mut self, sign: CheckboxSign) -> Self {
89    //     self.sign = sign;
90    //     self
91    // }
92
93    pub fn icon(mut self, icon: IconKind) -> Self {
94        self.check_icon = Icon::new(icon);
95        self
96    }
97
98    pub fn width(mut self, width: impl Into<Length>) -> Self {
99        self.size.width = width.into();
100        self
101    }
102
103    pub fn height(mut self, height: impl Into<Length>) -> Self {
104        self.size.height = height.into();
105        self
106    }
107
108    // Helpers //
109    fn status<E: Event>(&self, ctx: &UiCtx<Message>, state: &StateNode) -> CheckboxStatus {
110        let focused = UiCtx::is_focused::<R, E, S>(&ctx, self);
111        match (state.get::<CheckboxState>(), focused) {
112            (CheckboxState { is_pressed: true, .. }, _) => CheckboxStatus::Pressed,
113            (CheckboxState { is_checked: true, .. }, false) => CheckboxStatus::Checked,
114            (CheckboxState { .. }, true) => CheckboxStatus::Focused,
115            (CheckboxState { .. }, false) => CheckboxStatus::Normal,
116        }
117    }
118}
119
120impl<'a, Message, R, E, S> Widget<Message, R, E, S> for Checkbox<'a, Message, R, S>
121where
122    R: Renderer,
123    E: Event,
124    S: CheckboxStyler<R::Color>,
125{
126    fn id(&self) -> Option<crate::el::ElId> {
127        Some(self.id)
128    }
129
130    fn tree_ids(&self) -> Vec<ElId> {
131        vec![self.id]
132    }
133
134    fn size(&self) -> crate::size::Size<crate::size::Length> {
135        self.size
136    }
137
138    fn state_tag(&self) -> crate::state::StateTag {
139        StateTag::of::<CheckboxState>()
140    }
141
142    fn state(&self) -> crate::state::State {
143        State::new(CheckboxState::default())
144    }
145
146    fn state_children(&self) -> Vec<StateNode> {
147        vec![]
148    }
149
150    fn on_event(
151        &mut self,
152        ctx: &mut UiCtx<Message>,
153        event: E,
154        state: &mut StateNode,
155    ) -> crate::event::EventResponse<E> {
156        let focused = UiCtx::is_focused::<R, E, S>(&ctx, self);
157        let current_state = state.get::<CheckboxState>();
158
159        if let Some(common) = event.as_common() {
160            match common {
161                CommonEvent::FocusMove(_) if focused => {
162                    return Propagate::BubbleUp(self.id, event).into()
163                },
164                CommonEvent::FocusClickDown if focused => {
165                    state.get_mut::<CheckboxState>().is_pressed = true;
166                    return Capture::Captured.into();
167                },
168                CommonEvent::FocusClickUp if focused => {
169                    let was_pressed = current_state.is_pressed;
170
171                    state.get_mut::<CheckboxState>().is_pressed = false;
172
173                    if was_pressed {
174                        let new_state = !state.get::<CheckboxState>().is_checked;
175                        state.get_mut::<CheckboxState>().is_checked = new_state;
176
177                        ctx.publish((self.on_change)(new_state));
178
179                        return Capture::Captured.into();
180                    }
181                },
182                CommonEvent::FocusClickDown
183                | CommonEvent::FocusClickUp
184                | CommonEvent::FocusMove(_) => {
185                    // Should we reset state on any event? Or only on common
186                    state.get_mut::<CheckboxState>().is_pressed = false;
187                },
188            }
189        }
190
191        Propagate::Ignored.into()
192    }
193
194    fn layout(
195        &self,
196        ctx: &mut UiCtx<Message>,
197        _state_tree: &mut StateNode,
198        styler: &S,
199        limits: &crate::layout::Limits,
200        viewport: &Viewport,
201    ) -> crate::layout::LayoutNode {
202        Layout::container(
203            limits,
204            self.size,
205            crate::layout::Position::Relative,
206            viewport,
207            Padding::zero(),
208            Padding::new_equal(1),
209            crate::align::Alignment::Center,
210            crate::align::Alignment::Center,
211            |limits| {
212                Widget::<Message, R, E, S>::layout(
213                    &self.check_icon,
214                    ctx,
215                    &mut StateNode::stateless(),
216                    styler,
217                    limits,
218                    viewport,
219                )
220            },
221        )
222    }
223
224    fn draw(
225        &self,
226        ctx: &mut UiCtx<Message>,
227        state: &mut StateNode,
228        renderer: &mut R,
229        styler: &S,
230        layout: crate::layout::Layout,
231    ) {
232        let style = styler.style(&self.class, self.status::<E>(ctx, state));
233        let state = state.get::<CheckboxState>();
234
235        let bounds = layout.bounds();
236
237        renderer.block(crate::block::Block {
238            border: style.border,
239            rect: Rectangle::new(bounds.position, bounds.size.into()),
240            background: style.background,
241        });
242
243        if state.is_checked {
244            Widget::<Message, R, E, S>::draw(
245                &self.check_icon,
246                ctx,
247                &mut StateNode::stateless(),
248                renderer,
249                styler,
250                layout.children().next().unwrap(),
251            )
252        }
253    }
254}
255
256impl<'a, Message, R, E, S> From<Checkbox<'a, Message, R, S>> for El<'a, Message, R, E, S>
257where
258    Message: 'a,
259    R: Renderer + 'a,
260    E: Event + 'a,
261    S: CheckboxStyler<R::Color> + 'a,
262{
263    fn from(value: Checkbox<'a, Message, R, S>) -> Self {
264        Self::new(value)
265    }
266}