1use jag_draw::{Brush, Color, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 ElementState, EventHandler, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseClickEvent,
8 MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11
12use super::Element;
13
14pub struct Checkbox {
16 pub rect: Rect,
17 pub checked: bool,
18 pub focused: bool,
19 pub label: Option<String>,
20 pub label_size: f32,
21 pub label_color: ColorLinPremul,
22 pub box_fill: ColorLinPremul,
23 pub border_color: ColorLinPremul,
24 pub border_width: f32,
25 pub check_color: ColorLinPremul,
26 pub required: bool,
28 pub error_message: Option<String>,
30 pub validation_error: Option<String>,
32 pub focus_id: FocusId,
34}
35
36impl Checkbox {
37 pub fn new() -> Self {
39 Self {
40 rect: Rect {
41 x: 0.0,
42 y: 0.0,
43 w: 18.0,
44 h: 18.0,
45 },
46 checked: false,
47 focused: false,
48 label: None,
49 label_size: 14.0,
50 label_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
51 box_fill: ColorLinPremul::from_srgba_u8([40, 40, 40, 255]),
52 border_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
53 border_width: 1.0,
54 check_color: ColorLinPremul::from_srgba_u8([59, 130, 246, 255]),
55 required: false,
56 error_message: None,
57 validation_error: None,
58 focus_id: FocusId(0),
59 }
60 }
61
62 pub fn toggle(&mut self) {
64 self.checked = !self.checked;
65 }
66
67 pub fn hit_test_box(&self, x: f32, y: f32) -> bool {
69 x >= self.rect.x
70 && x <= self.rect.x + self.rect.w
71 && y >= self.rect.y
72 && y <= self.rect.y + self.rect.h
73 }
74
75 pub fn hit_test_label(&self, x: f32, y: f32) -> bool {
77 if let Some(label) = &self.label {
78 let label_x = self.rect.x + self.rect.w + 8.0;
79 let char_width = self.label_size * 0.5;
80 let label_width = label.len() as f32 * char_width;
81 let clickable_height = self.rect.h.max(self.label_size * 1.2);
82
83 x >= label_x
84 && x <= label_x + label_width
85 && y >= self.rect.y
86 && y <= self.rect.y + clickable_height
87 } else {
88 false
89 }
90 }
91}
92
93impl Default for Checkbox {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl Element for Checkbox {
104 fn rect(&self) -> Rect {
105 self.rect
106 }
107
108 fn set_rect(&mut self, rect: Rect) {
109 self.rect = rect;
110 }
111
112 fn render(&self, canvas: &mut Canvas, z: i32) {
113 let base_rrect = RoundedRect {
114 rect: self.rect,
115 radii: RoundedRadii {
116 tl: 2.0,
117 tr: 2.0,
118 br: 2.0,
119 bl: 2.0,
120 },
121 };
122
123 if self.border_width > 0.0 {
125 let has_error = self.validation_error.is_some();
126 let border_color = if has_error {
127 Color::rgba(220, 38, 38, 255)
128 } else {
129 self.border_color
130 };
131 let border_width = if has_error {
132 self.border_width.max(2.0)
133 } else {
134 self.border_width
135 };
136
137 jag_surface::shapes::draw_snapped_rounded_rectangle(
138 canvas,
139 base_rrect,
140 Some(Brush::Solid(self.box_fill)),
141 Some(border_width),
142 Some(Brush::Solid(border_color)),
143 z,
144 );
145 } else {
146 canvas.fill_rect(
147 self.rect.x,
148 self.rect.y,
149 self.rect.w,
150 self.rect.h,
151 Brush::Solid(self.box_fill),
152 z,
153 );
154 }
155
156 if self.focused {
158 let focus_rr = RoundedRect {
159 rect: self.rect,
160 radii: RoundedRadii {
161 tl: 2.0,
162 tr: 2.0,
163 br: 2.0,
164 bl: 2.0,
165 },
166 };
167 let focus = Brush::Solid(Color::rgba(63, 130, 246, 255));
168 jag_surface::shapes::draw_snapped_rounded_rectangle(
169 canvas,
170 focus_rr,
171 None,
172 Some(2.0),
173 Some(focus),
174 z + 2,
175 );
176 }
177
178 if self.checked {
180 let inset = 2.0_f32;
181 let inner = Rect {
182 x: (self.rect.x + inset).round(),
183 y: (self.rect.y + inset).round(),
184 w: (self.rect.w - 2.0 * inset).max(0.0).round(),
185 h: (self.rect.h - 2.0 * inset).max(0.0).round(),
186 };
187 let inner_rr = RoundedRect {
188 rect: inner,
189 radii: RoundedRadii {
190 tl: 1.5,
191 tr: 1.5,
192 br: 1.5,
193 bl: 1.5,
194 },
195 };
196 canvas.rounded_rect(inner_rr, Brush::Solid(self.check_color), z + 2);
197
198 let mark_size = inner.w * 0.7;
200 let mark_x = inner.x + (inner.w - mark_size * 0.5) * 0.5;
201 let mark_y = inner.y + inner.h * 0.75;
202 canvas.draw_text_run_weighted(
203 [mark_x, mark_y],
204 "\u{2713}".to_string(),
205 mark_size,
206 700.0,
207 ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
208 z + 3,
209 );
210 }
211
212 if let Some(text) = &self.label {
214 let tx = self.rect.x + self.rect.w + 8.0;
215 let ty = self.rect.y + self.rect.h * 0.5 + self.label_size * 0.32;
216 canvas.draw_text_run_weighted(
217 [tx, ty],
218 text.clone(),
219 self.label_size,
220 400.0,
221 self.label_color,
222 z + 3,
223 );
224 }
225
226 if let Some(ref error_msg) = self.validation_error {
228 let error_size = (self.label_size * 0.85).max(12.0);
229 let baseline_offset = error_size * 0.8;
230 let top_gap = 3.0;
231 let control_height = self.rect.h.max(self.label_size * 1.2);
232 let error_y = self.rect.y + control_height + top_gap + baseline_offset;
233 let error_color = ColorLinPremul::from_srgba_u8([220, 38, 38, 255]);
234
235 canvas.draw_text_run_weighted(
236 [self.rect.x, error_y],
237 error_msg.clone(),
238 error_size,
239 400.0,
240 error_color,
241 z + 4,
242 );
243 }
244 }
245
246 fn focus_id(&self) -> Option<FocusId> {
247 Some(self.focus_id)
248 }
249}
250
251impl EventHandler for Checkbox {
256 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
257 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
258 return EventResult::Ignored;
259 }
260 if self.hit_test_box(event.x, event.y) || self.hit_test_label(event.x, event.y) {
261 self.toggle();
262 EventResult::Handled
263 } else {
264 EventResult::Ignored
265 }
266 }
267
268 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
269 if event.state != ElementState::Pressed || !self.focused {
270 return EventResult::Ignored;
271 }
272 match event.key {
273 KeyCode::Space | KeyCode::Enter => {
274 self.toggle();
275 EventResult::Handled
276 }
277 _ => EventResult::Ignored,
278 }
279 }
280
281 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
282 EventResult::Ignored
283 }
284
285 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
286 EventResult::Ignored
287 }
288
289 fn is_focused(&self) -> bool {
290 self.focused
291 }
292
293 fn set_focused(&mut self, focused: bool) {
294 self.focused = focused;
295 }
296
297 fn contains_point(&self, x: f32, y: f32) -> bool {
298 self.hit_test_box(x, y) || self.hit_test_label(x, y)
299 }
300}
301
302#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn checkbox_toggle() {
312 let mut cb = Checkbox::new();
313 assert!(!cb.checked);
314 cb.toggle();
315 assert!(cb.checked);
316 cb.toggle();
317 assert!(!cb.checked);
318 }
319
320 #[test]
321 fn checkbox_default_is_new() {
322 let cb = Checkbox::default();
323 assert!(!cb.checked);
324 assert!(!cb.focused);
325 assert!(cb.label.is_none());
326 }
327
328 #[test]
329 fn checkbox_hit_test_box() {
330 let mut cb = Checkbox::new();
331 cb.rect = Rect {
332 x: 10.0,
333 y: 10.0,
334 w: 18.0,
335 h: 18.0,
336 };
337 assert!(cb.hit_test_box(15.0, 15.0));
338 assert!(!cb.hit_test_box(0.0, 0.0));
339 }
340
341 #[test]
342 fn checkbox_hit_test_label() {
343 let mut cb = Checkbox::new();
344 cb.rect = Rect {
345 x: 10.0,
346 y: 10.0,
347 w: 18.0,
348 h: 18.0,
349 };
350 cb.label = Some("Accept".to_string());
351 assert!(cb.hit_test_label(40.0, 15.0));
353 assert!(!cb.hit_test_label(5.0, 15.0));
354 }
355
356 #[test]
357 fn checkbox_contains_point_covers_both() {
358 let mut cb = Checkbox::new();
359 cb.rect = Rect {
360 x: 10.0,
361 y: 10.0,
362 w: 18.0,
363 h: 18.0,
364 };
365 cb.label = Some("Check".to_string());
366 assert!(cb.contains_point(15.0, 15.0));
368 assert!(cb.contains_point(40.0, 15.0));
370 assert!(!cb.contains_point(0.0, 0.0));
372 }
373
374 #[test]
375 fn checkbox_focus() {
376 let mut cb = Checkbox::new();
377 assert!(!cb.is_focused());
378 cb.set_focused(true);
379 assert!(cb.is_focused());
380 }
381
382 #[test]
383 fn checkbox_keyboard_toggle() {
384 let mut cb = Checkbox::new();
385 cb.focused = true;
386 let evt = KeyboardEvent {
387 key: KeyCode::Space,
388 state: ElementState::Pressed,
389 modifiers: Default::default(),
390 text: None,
391 };
392 assert!(!cb.checked);
393 assert_eq!(cb.handle_keyboard(&evt), EventResult::Handled);
394 assert!(cb.checked);
395 }
396
397 #[test]
398 fn checkbox_keyboard_ignored_without_focus() {
399 let mut cb = Checkbox::new();
400 cb.focused = false;
401 let evt = KeyboardEvent {
402 key: KeyCode::Space,
403 state: ElementState::Pressed,
404 modifiers: Default::default(),
405 text: None,
406 };
407 assert_eq!(cb.handle_keyboard(&evt), EventResult::Ignored);
408 assert!(!cb.checked);
409 }
410}