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)]
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 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 }
86 }
87
88 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 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 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}