1use super::Widget;
18use alloc::{boxed::Box, string::String};
19use core::marker::PhantomData;
20use embedded_graphics::{
21 pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
22};
23use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, UiAction, WidgetId};
24use zest_theme::Theme;
25
26const BOX_SIZE: u32 = 20;
28const LABEL_GAP: u32 = 6;
30
31pub struct Checkbox<'a, C: PixelColor, M: Clone> {
34 rect: Rectangle,
35 checked: bool,
36 label: Option<String>,
37 id: Option<WidgetId>,
38 on_toggle: Option<Box<dyn Fn(bool) -> M + 'a>>,
39 focused: bool,
40 pressed: bool,
41 width: Length,
42 height: Length,
43 _color: PhantomData<C>,
44}
45
46impl<'a, C: PixelColor, M: Clone> Checkbox<'a, C, M> {
47 pub fn new(checked: bool) -> Self {
50 Self {
51 rect: Rectangle::zero(),
52 checked,
53 label: None,
54 id: None,
55 on_toggle: None,
56 focused: false,
57 pressed: false,
58 width: Length::Shrink,
59 height: Length::Fixed(BOX_SIZE),
60 _color: PhantomData,
61 }
62 }
63
64 #[must_use]
66 pub fn label(mut self, label: impl Into<String>) -> Self {
67 self.label = Some(label.into());
68 self
69 }
70
71 #[must_use]
74 pub fn on_toggle<F: Fn(bool) -> M + 'a>(mut self, f: F) -> Self {
75 self.on_toggle = Some(Box::new(f));
76 self
77 }
78
79 #[must_use]
81 pub fn id(mut self, id: WidgetId) -> Self {
82 self.id = Some(id);
83 self
84 }
85
86 #[must_use]
88 pub fn width(mut self, width: impl Into<Length>) -> Self {
89 self.width = width.into();
90 self
91 }
92
93 #[must_use]
95 pub fn height(mut self, height: impl Into<Length>) -> Self {
96 self.height = height.into();
97 self
98 }
99
100 pub fn is_enabled(&self) -> bool {
103 self.on_toggle.is_some()
104 }
105
106 fn intrinsic(&self) -> Size {
107 let label_w = self
110 .label
111 .as_ref()
112 .map_or(0, |l| LABEL_GAP + l.chars().count() as u32 * 8);
113 Size::new(BOX_SIZE + label_w, BOX_SIZE)
114 }
115
116 fn box_rect(&self) -> Rectangle {
117 let y = self.rect.top_left.y + (self.rect.size.height.saturating_sub(BOX_SIZE) / 2) as i32;
118 Rectangle::new(
119 Point::new(self.rect.top_left.x, y),
120 Size::new(BOX_SIZE, BOX_SIZE),
121 )
122 }
123
124 fn hit_test(&self, point: Point) -> bool {
125 let tl = self.rect.top_left;
126 let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
127 point.x >= tl.x && point.x < br.x && point.y >= tl.y && point.y < br.y
128 }
129}
130
131impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Checkbox<'a, C, M> {
132 fn measure(&mut self, constraints: Constraints) -> Size {
133 let intrinsic = self.intrinsic();
134 let w = self.width.resolve(intrinsic.width, constraints.max.width);
135 let h = self
136 .height
137 .resolve(intrinsic.height, constraints.max.height);
138 constraints.clamp(Size::new(w, h))
139 }
140
141 fn preferred_size(&self) -> (Length, Length) {
142 (self.width, self.height)
143 }
144
145 fn arrange(&mut self, rect: Rectangle) {
146 self.rect = rect;
147 }
148
149 fn rect(&self) -> Rectangle {
150 self.rect
151 }
152
153 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
154 if !self.is_enabled() || !self.hit_test(point) {
155 if matches!(phase, TouchPhase::Up | TouchPhase::Moved) {
156 self.pressed = false;
157 }
158 return None;
159 }
160 match phase {
161 TouchPhase::Down => {
162 self.pressed = true;
163 None
164 }
165 TouchPhase::Up => {
166 if self.pressed {
167 self.pressed = false;
168 self.on_toggle.as_ref().map(|cb| cb(!self.checked))
169 } else {
170 None
171 }
172 }
173 TouchPhase::Moved => None,
174 }
175 }
176
177 fn mark_pressed(&mut self, point: Point) {
178 if self.is_enabled() && self.hit_test(point) {
179 self.pressed = true;
180 }
181 }
182
183 fn widget_id(&self) -> Option<WidgetId> {
184 self.id
185 }
186
187 fn is_focusable(&self) -> bool {
188 self.id.is_some() && self.is_enabled()
189 }
190
191 fn handle_action(&mut self, action: UiAction) -> Option<M> {
192 if !self.is_enabled() {
193 return None;
194 }
195
196 match action {
197 UiAction::Activate => self.on_toggle.as_ref().map(|cb| cb(!self.checked)),
198 _ => None,
199 }
200 }
201
202 fn sync_focus(&mut self, focused: Option<WidgetId>) {
203 self.focused = self.id.is_some() && self.id == focused;
204 }
205
206 fn focus_at(&self, point: Point) -> Option<WidgetId> {
207 if self.is_focusable() && self.hit_test(point) {
208 self.id
209 } else {
210 None
211 }
212 }
213
214 fn draw<'t>(
215 &self,
216 renderer: &mut dyn Renderer<C>,
217 theme: &Theme<'t, C>,
218 ) -> Result<(), RenderError> {
219 let accent = &theme.accent;
220 let box_rect = self.box_rect();
221
222 if self.checked {
223 let fill = if self.pressed {
224 accent.pressed
225 } else {
226 accent.base
227 };
228 let border = if self.focused {
229 accent.base
230 } else {
231 accent.border
232 };
233 renderer.fill_rect(box_rect, fill)?;
234 renderer.stroke_rect(box_rect, border)?;
235 let x = box_rect.top_left.x;
237 let y = box_rect.top_left.y;
238 let s = BOX_SIZE as i32;
239 renderer.stroke_line(
240 Point::new(x + s * 3 / 16, y + s / 2),
241 Point::new(x + s * 7 / 16, y + s * 11 / 16),
242 accent.on_base,
243 2,
244 )?;
245 renderer.stroke_line(
246 Point::new(x + s * 7 / 16, y + s * 11 / 16),
247 Point::new(x + s * 13 / 16, y + s * 5 / 16),
248 accent.on_base,
249 2,
250 )?;
251 } else {
252 let bg = if self.pressed {
253 accent.pressed
254 } else {
255 theme.background.base
256 };
257 let border = if self.focused {
258 accent.base
259 } else {
260 accent.border
261 };
262 renderer.fill_rect(box_rect, bg)?;
263 renderer.stroke_rect(box_rect, border)?;
264 }
265
266 if let Some(label) = &self.label {
267 let font = theme.default_font();
268 let text_x = box_rect.top_left.x + BOX_SIZE as i32 + LABEL_GAP as i32;
269 let center_y = self.rect.top_left.y
270 + self.rect.size.height as i32 / 2
271 + font.character_size.height as i32 / 3;
272 let color = if !self.is_enabled() {
273 theme.palette.neutral_2
274 } else if self.focused {
275 theme.accent.base
276 } else {
277 theme.background.on_base
278 };
279 renderer.draw_text(
280 label,
281 Point::new(text_x, center_y),
282 font,
283 color,
284 Alignment::Left,
285 )?;
286 }
287
288 Ok(())
289 }
290}