1use super::{Align, Bus, Event, Hub, Id, RenderQueue, View, ID_FEEDER};
2use crate::color::{BLACK, GRAY08, TEXT_NORMAL};
3use crate::context::Context;
4use crate::device::CURRENT_DEVICE;
5use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
6use crate::framebuffer::Framebuffer;
7use crate::geom::{BorderSpec, Rectangle};
8use crate::unit::scale_by_dpi;
9use crate::view::filler::Filler;
10use crate::view::label::Label;
11
12use super::{THICKNESS_MEDIUM, THICKNESS_SMALL};
13
14struct SelectionBox {
19 id: Id,
20 rect: Rectangle,
21 children: Vec<Box<dyn View>>,
22 target_rect: Rectangle,
23 text_width: i32,
24 visible: bool,
25}
26
27impl SelectionBox {
28 fn new(rect: Rectangle, target_rect: Rectangle, text_width: i32, visible: bool) -> Self {
29 Self {
30 id: ID_FEEDER.next(),
31 rect,
32 children: Vec::new(),
33 target_rect,
34 text_width,
35 visible,
36 }
37 }
38
39 fn set_target(&mut self, target_rect: Rectangle, text_width: i32, visible: bool) {
40 self.target_rect = target_rect;
41 self.text_width = text_width;
42 self.visible = visible;
43 }
44}
45
46impl View for SelectionBox {
47 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?_evt), ret(level=tracing::Level::TRACE)))]
48 fn handle_event(
49 &mut self,
50 _evt: &Event,
51 _hub: &Hub,
52 _bus: &mut Bus,
53 _rq: &mut RenderQueue,
54 _context: &mut Context,
55 ) -> bool {
56 false
57 }
58
59 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts), fields(rect = ?rect)))]
60 fn render(&self, fb: &mut dyn Framebuffer, rect: Rectangle, fonts: &mut Fonts) {
61 if !self.visible {
62 return;
63 }
64
65 let render_rect = rect.intersection(&self.target_rect);
66 if render_rect.is_none() {
67 return;
68 }
69
70 let dpi = CURRENT_DEVICE.dpi;
71 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
72
73 let padding = font.em() as i32 / 2 - scale_by_dpi(3.0, dpi) as i32;
74 let x_height = font.x_heights.0 as i32;
75 let border_box_height = 3 * x_height;
76 let border_box_width = self.text_width + padding;
77
78 let x_offset = padding;
79 let dy = (self.target_rect.height() as i32 - x_height) / 2;
80 let y_offset = dy + x_height - 2 * x_height;
81 let pt = self.target_rect.min + pt!(x_offset, y_offset);
82 let border_box_rect = rect![pt, pt + pt!(border_box_width, border_box_height)];
83
84 let border_thickness = scale_by_dpi(THICKNESS_SMALL, dpi) as u16;
85
86 fb.draw_rectangle_outline(
87 &border_box_rect,
88 &BorderSpec {
89 thickness: border_thickness,
90 color: BLACK,
91 },
92 );
93 }
94
95 fn rect(&self) -> &Rectangle {
96 &self.rect
97 }
98
99 fn rect_mut(&mut self) -> &mut Rectangle {
100 &mut self.rect
101 }
102
103 fn children(&self) -> &Vec<Box<dyn View>> {
104 &self.children
105 }
106
107 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
108 &mut self.children
109 }
110
111 fn id(&self) -> Id {
112 self.id
113 }
114}
115
116pub struct Toggle {
192 id: Id,
193 rect: Rectangle,
194 children: Vec<Box<dyn View>>,
195 enabled: bool,
196 event: Event,
197 left_label_index: usize,
198 right_label_index: usize,
199 selection_box_index: usize,
200 left_text_width: i32,
201 right_text_width: i32,
202}
203
204impl Toggle {
205 pub fn new(
220 rect: Rectangle,
221 text_enabled: &str,
222 text_disabled: &str,
223 enabled: bool,
224 event: Event,
225 fonts: &mut Fonts,
226 align: Align,
227 ) -> Toggle {
228 let dpi = CURRENT_DEVICE.dpi;
229 let separator_width = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
230
231 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
232 let padding = font.em() as i32;
233 let left_plan = font.plan(text_enabled, None, None);
234 let right_plan = font.plan(text_disabled, None, None);
235 let left_text_width = left_plan.width;
236 let right_text_width = right_plan.width;
237 let left_width = left_text_width + padding;
238 let right_width = right_text_width + padding;
239 let total_width = left_width + separator_width + right_width;
240
241 let x_offset = rect.width() as i32 - total_width;
242
243 let mut children = Vec::new();
244
245 let left_rect = rect![
246 rect.min.x + x_offset,
247 rect.min.y,
248 rect.min.x + x_offset + left_width,
249 rect.max.y
250 ];
251 let left_label = Label::new(left_rect, text_enabled.to_string(), Align::Center)
252 .scheme(TEXT_NORMAL)
253 .event(Some(event.clone()));
254 children.push(Box::new(left_label) as Box<dyn View>);
255 let left_label_index = children.len() - 1;
256
257 let separator_height = rect.height() as i32;
258 let separator_padding = separator_height / 4;
259 let separator_rect = rect![
260 rect.min.x + x_offset + left_width,
261 rect.min.y + separator_padding,
262 rect.min.x + x_offset + left_width + separator_width,
263 rect.max.y - separator_padding
264 ];
265 let separator = Filler::new(separator_rect, GRAY08);
266 children.push(Box::new(separator) as Box<dyn View>);
267
268 let right_rect = rect![
269 rect.min.x + x_offset + left_width + separator_width,
270 rect.min.y,
271 rect.max.x,
272 rect.max.y
273 ];
274 let right_label = Label::new(right_rect, text_disabled.to_string(), align)
275 .scheme(TEXT_NORMAL)
276 .event(Some(event.clone()));
277 children.push(Box::new(right_label) as Box<dyn View>);
278 let right_label_index = children.len() - 1;
279
280 let selected_rect = if enabled { left_rect } else { right_rect };
281 let selected_text_width = if enabled {
282 left_text_width
283 } else {
284 right_text_width
285 };
286 let selection_box = SelectionBox::new(rect, selected_rect, selected_text_width, true);
287 children.push(Box::new(selection_box) as Box<dyn View>);
288 let selection_box_index = children.len() - 1;
289
290 Toggle {
291 id: ID_FEEDER.next(),
292 rect,
293 children,
294 enabled,
295 event,
296 left_label_index,
297 right_label_index,
298 selection_box_index,
299 left_text_width,
300 right_text_width,
301 }
302 }
303
304 fn request_rerender(&mut self, rq: &mut RenderQueue) {
305 rq.add(crate::view::RenderData::new(
306 self.id,
307 self.rect,
308 crate::framebuffer::UpdateMode::Gui,
309 ));
310 }
311
312 fn update_selection_box(&mut self, rq: &mut RenderQueue) {
313 let selected_label_index = if self.enabled {
314 self.left_label_index
315 } else {
316 self.right_label_index
317 };
318
319 let text_width = if self.enabled {
320 self.left_text_width
321 } else {
322 self.right_text_width
323 };
324
325 let selected_rect = *self.children[selected_label_index].rect();
326
327 if let Some(selection_box) =
328 self.children[self.selection_box_index].downcast_mut::<SelectionBox>()
329 {
330 selection_box.set_target(selected_rect, text_width, true);
331 }
332 self.request_rerender(rq);
333 }
334
335 #[cfg(test)]
336 pub fn is_enabled(&self) -> bool {
337 self.enabled
338 }
339}
340
341impl View for Toggle {
342 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
343 fn handle_event(
344 &mut self,
345 evt: &Event,
346 _hub: &Hub,
347 bus: &mut Bus,
348 rq: &mut RenderQueue,
349 _context: &mut Context,
350 ) -> bool {
351 if std::mem::discriminant(evt) == std::mem::discriminant(&self.event) {
352 self.enabled = !self.enabled;
353 self.update_selection_box(rq);
354 bus.push_back(evt.clone());
355 return true;
356 }
357
358 false
359 }
360
361 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
362 fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
363
364 fn rect(&self) -> &Rectangle {
365 &self.rect
366 }
367
368 fn rect_mut(&mut self) -> &mut Rectangle {
369 &mut self.rect
370 }
371
372 fn children(&self) -> &Vec<Box<dyn View>> {
373 &self.children
374 }
375
376 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
377 &mut self.children
378 }
379
380 fn id(&self) -> Id {
381 self.id
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::context::test_helpers::create_test_context;
389 use crate::view::{ToggleEvent, ViewId};
390 use std::collections::VecDeque;
391 use std::sync::mpsc::channel;
392
393 #[test]
394 fn test_toggle_starts_in_enabled_state() {
395 let mut context = create_test_context();
396 let rect = rect![0, 0, 200, 50];
397 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
398 let toggle = Toggle::new(
399 rect,
400 "On",
401 "Off",
402 true,
403 toggle_event,
404 &mut context.fonts,
405 Align::Center,
406 );
407 assert!(toggle.is_enabled());
408 }
409
410 #[test]
411 fn test_toggle_starts_in_disabled_state() {
412 let mut context = create_test_context();
413 let rect = rect![0, 0, 200, 50];
414 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
415 let toggle = Toggle::new(
416 rect,
417 "On",
418 "Off",
419 false,
420 toggle_event,
421 &mut context.fonts,
422 Align::Center,
423 );
424 assert!(!toggle.is_enabled());
425 }
426
427 #[test]
428 fn test_toggle_event_intercepted_and_state_flipped() {
429 let mut context = create_test_context();
430 let rect = rect![0, 0, 200, 50];
431 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
432 let mut toggle = Toggle::new(
433 rect,
434 "On",
435 "Off",
436 true,
437 toggle_event.clone(),
438 &mut context.fonts,
439 Align::Center,
440 );
441
442 let (hub, _receiver) = channel();
443 let mut bus = VecDeque::new();
444 let mut rq = RenderQueue::new();
445
446 let handled = toggle.handle_event(&toggle_event, &hub, &mut bus, &mut rq, &mut context);
447
448 assert!(handled);
449 assert!(!toggle.is_enabled());
450
451 assert_eq!(bus.len(), 1);
452 assert!(matches!(
453 bus.pop_front(),
454 Some(Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu)))
455 ));
456
457 assert!(!rq.is_empty());
458 }
459
460 #[test]
461 fn test_labels_have_correct_events_configured() {
462 let mut context = create_test_context();
463 let rect = rect![0, 0, 200, 50];
464 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
465 let toggle = Toggle::new(
466 rect,
467 "On",
468 "Off",
469 true,
470 toggle_event,
471 &mut context.fonts,
472 Align::Center,
473 );
474
475 let left_label = toggle.children[0].downcast_ref::<Label>().unwrap();
476 assert!(left_label.text() == "On");
477
478 let right_label = toggle.children[2].downcast_ref::<Label>().unwrap();
479 assert!(right_label.text() == "Off");
480 }
481
482 #[test]
483 fn test_labels_use_normal_scheme() {
484 let mut context = create_test_context();
485 let rect = rect![0, 0, 200, 50];
486 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
487 let toggle = Toggle::new(
488 rect,
489 "On",
490 "Off",
491 true,
492 toggle_event,
493 &mut context.fonts,
494 Align::Center,
495 );
496
497 let left_label = toggle.children[0].downcast_ref::<Label>().unwrap();
498 assert_eq!(left_label.get_scheme(), TEXT_NORMAL);
499
500 let right_label = toggle.children[2].downcast_ref::<Label>().unwrap();
501 assert_eq!(right_label.get_scheme(), TEXT_NORMAL);
502 }
503
504 #[test]
505 fn test_filler_separator_is_present() {
506 let mut context = create_test_context();
507 let rect = rect![0, 0, 200, 50];
508 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
509 let toggle = Toggle::new(
510 rect,
511 "On",
512 "Off",
513 true,
514 toggle_event,
515 &mut context.fonts,
516 Align::Center,
517 );
518
519 assert!(toggle.children[1].is::<Filler>());
520 }
521
522 #[test]
523 fn test_multiple_toggles_flips_state_multiple_times() {
524 let mut context = create_test_context();
525 let rect = rect![0, 0, 200, 50];
526 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
527 let mut toggle = Toggle::new(
528 rect,
529 "On",
530 "Off",
531 true,
532 toggle_event.clone(),
533 &mut context.fonts,
534 Align::Center,
535 );
536
537 let (hub, _receiver) = channel();
538 let mut bus = VecDeque::new();
539 let mut rq = RenderQueue::new();
540
541 toggle.handle_event(&toggle_event, &hub, &mut bus, &mut rq, &mut context);
542 assert!(!toggle.is_enabled());
543
544 toggle.handle_event(&toggle_event, &hub, &mut bus, &mut rq, &mut context);
545 assert!(toggle.is_enabled());
546
547 toggle.handle_event(&toggle_event, &hub, &mut bus, &mut rq, &mut context);
548 assert!(!toggle.is_enabled());
549 }
550
551 #[test]
552 fn test_non_toggle_events_are_ignored() {
553 let mut context = create_test_context();
554 let rect = rect![0, 0, 200, 50];
555 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
556 let mut toggle = Toggle::new(
557 rect,
558 "On",
559 "Off",
560 true,
561 toggle_event,
562 &mut context.fonts,
563 Align::Center,
564 );
565
566 let (hub, _receiver) = channel();
567 let mut bus = VecDeque::new();
568 let mut rq = RenderQueue::new();
569
570 let other_event = Event::Back;
571 let handled = toggle.handle_event(&other_event, &hub, &mut bus, &mut rq, &mut context);
572
573 assert!(!handled);
574 assert!(toggle.is_enabled());
575 assert_eq!(bus.len(), 0);
576 }
577
578 #[test]
579 fn test_event_bubbling_continues_after_toggle() {
580 let mut context = create_test_context();
581 let rect = rect![0, 0, 200, 50];
582 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
583 let mut toggle = Toggle::new(
584 rect,
585 "On",
586 "Off",
587 true,
588 toggle_event.clone(),
589 &mut context.fonts,
590 Align::Center,
591 );
592
593 let (hub, _receiver) = channel();
594 let mut bus = VecDeque::new();
595 let mut rq = RenderQueue::new();
596
597 toggle.handle_event(&toggle_event, &hub, &mut bus, &mut rq, &mut context);
598
599 assert_eq!(bus.len(), 1);
600 let emitted_event = bus.pop_front().unwrap();
601 assert!(matches!(
602 emitted_event,
603 Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu))
604 ));
605 }
606
607 #[test]
608 fn test_has_four_children() {
609 let mut context = create_test_context();
610 let rect = rect![0, 0, 200, 50];
611 let toggle_event = Event::NewToggle(ToggleEvent::View(ViewId::SettingsMenu));
612 let toggle = Toggle::new(
613 rect,
614 "On",
615 "Off",
616 true,
617 toggle_event,
618 &mut context.fonts,
619 Align::Center,
620 );
621
622 assert_eq!(toggle.children.len(), 4);
623 assert!(toggle.children[0].is::<Label>());
624 assert!(toggle.children[1].is::<Filler>());
625 assert!(toggle.children[2].is::<Label>());
626 assert!(toggle.children[3].is::<SelectionBox>());
627 }
628}