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 let (Event::Toggle(incoming), Event::Toggle(stored)) = (evt, &self.event) {
352 if incoming == stored {
353 self.enabled = !self.enabled;
354 self.update_selection_box(rq);
355 bus.push_back(evt.clone());
356
357 return true;
358 }
359 }
360
361 false
362 }
363
364 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
365 fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
366
367 fn rect(&self) -> &Rectangle {
368 &self.rect
369 }
370
371 fn rect_mut(&mut self) -> &mut Rectangle {
372 &mut self.rect
373 }
374
375 fn children(&self) -> &Vec<Box<dyn View>> {
376 &self.children
377 }
378
379 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
380 &mut self.children
381 }
382
383 fn id(&self) -> Id {
384 self.id
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::context::test_helpers::create_test_context;
392 use crate::view::{ToggleEvent, ViewId};
393 use std::collections::VecDeque;
394 use std::sync::mpsc::channel;
395
396 #[test]
397 fn test_toggle_starts_in_enabled_state() {
398 let mut context = create_test_context();
399 let rect = rect![0, 0, 200, 50];
400 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
401 let toggle = Toggle::new(
402 rect,
403 "On",
404 "Off",
405 true,
406 toggle_event,
407 &mut context.fonts,
408 Align::Center,
409 );
410 assert!(toggle.is_enabled());
411 }
412
413 #[test]
414 fn test_toggle_starts_in_disabled_state() {
415 let mut context = create_test_context();
416 let rect = rect![0, 0, 200, 50];
417 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
418 let toggle = Toggle::new(
419 rect,
420 "On",
421 "Off",
422 false,
423 toggle_event,
424 &mut context.fonts,
425 Align::Center,
426 );
427 assert!(!toggle.is_enabled());
428 }
429
430 #[test]
431 fn test_toggle_event_intercepted_and_state_flipped() {
432 let mut context = create_test_context();
433 let rect = rect![0, 0, 200, 50];
434 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
435 let mut toggle = Toggle::new(
436 rect,
437 "On",
438 "Off",
439 true,
440 toggle_event.clone(),
441 &mut context.fonts,
442 Align::Center,
443 );
444
445 let (hub, _receiver) = channel();
446 let mut bus = VecDeque::new();
447 let mut rq = RenderQueue::new();
448
449 let handled = toggle.handle_event(&toggle_event, &hub, &mut bus, &mut rq, &mut context);
450
451 assert!(handled);
452 assert!(!toggle.is_enabled());
453
454 assert_eq!(bus.len(), 1);
455 assert!(matches!(
456 bus.pop_front(),
457 Some(Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu)))
458 ));
459
460 assert!(!rq.is_empty());
461 }
462
463 #[test]
464 fn test_labels_have_correct_events_configured() {
465 let mut context = create_test_context();
466 let rect = rect![0, 0, 200, 50];
467 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
468 let toggle = Toggle::new(
469 rect,
470 "On",
471 "Off",
472 true,
473 toggle_event,
474 &mut context.fonts,
475 Align::Center,
476 );
477
478 let left_label = toggle.children[0].downcast_ref::<Label>().unwrap();
479 assert!(left_label.text() == "On");
480
481 let right_label = toggle.children[2].downcast_ref::<Label>().unwrap();
482 assert!(right_label.text() == "Off");
483 }
484
485 #[test]
486 fn test_labels_use_normal_scheme() {
487 let mut context = create_test_context();
488 let rect = rect![0, 0, 200, 50];
489 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
490 let toggle = Toggle::new(
491 rect,
492 "On",
493 "Off",
494 true,
495 toggle_event,
496 &mut context.fonts,
497 Align::Center,
498 );
499
500 let left_label = toggle.children[0].downcast_ref::<Label>().unwrap();
501 assert_eq!(left_label.get_scheme(), TEXT_NORMAL);
502
503 let right_label = toggle.children[2].downcast_ref::<Label>().unwrap();
504 assert_eq!(right_label.get_scheme(), TEXT_NORMAL);
505 }
506
507 #[test]
508 fn test_filler_separator_is_present() {
509 let mut context = create_test_context();
510 let rect = rect![0, 0, 200, 50];
511 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
512 let toggle = Toggle::new(
513 rect,
514 "On",
515 "Off",
516 true,
517 toggle_event,
518 &mut context.fonts,
519 Align::Center,
520 );
521
522 assert!(toggle.children[1].is::<Filler>());
523 }
524
525 #[test]
526 fn test_multiple_toggles_flips_state_multiple_times() {
527 let mut context = create_test_context();
528 let rect = rect![0, 0, 200, 50];
529 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
530 let mut toggle = Toggle::new(
531 rect,
532 "On",
533 "Off",
534 true,
535 toggle_event.clone(),
536 &mut context.fonts,
537 Align::Center,
538 );
539
540 let (hub, _receiver) = channel();
541 let mut bus = VecDeque::new();
542 let mut rq = RenderQueue::new();
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 toggle.handle_event(&toggle_event, &hub, &mut bus, &mut rq, &mut context);
551 assert!(!toggle.is_enabled());
552 }
553
554 #[test]
555 fn test_non_toggle_events_are_ignored() {
556 let mut context = create_test_context();
557 let rect = rect![0, 0, 200, 50];
558 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
559 let mut toggle = Toggle::new(
560 rect,
561 "On",
562 "Off",
563 true,
564 toggle_event,
565 &mut context.fonts,
566 Align::Center,
567 );
568
569 let (hub, _receiver) = channel();
570 let mut bus = VecDeque::new();
571 let mut rq = RenderQueue::new();
572
573 let other_event = Event::Back;
574 let handled = toggle.handle_event(&other_event, &hub, &mut bus, &mut rq, &mut context);
575
576 assert!(!handled);
577 assert!(toggle.is_enabled());
578 assert_eq!(bus.len(), 0);
579 }
580
581 #[test]
582 fn test_event_bubbling_continues_after_toggle() {
583 let mut context = create_test_context();
584 let rect = rect![0, 0, 200, 50];
585 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
586 let mut toggle = Toggle::new(
587 rect,
588 "On",
589 "Off",
590 true,
591 toggle_event.clone(),
592 &mut context.fonts,
593 Align::Center,
594 );
595
596 let (hub, _receiver) = channel();
597 let mut bus = VecDeque::new();
598 let mut rq = RenderQueue::new();
599
600 toggle.handle_event(&toggle_event, &hub, &mut bus, &mut rq, &mut context);
601
602 assert_eq!(bus.len(), 1);
603 let emitted_event = bus.pop_front().unwrap();
604 assert!(matches!(
605 emitted_event,
606 Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu))
607 ));
608 }
609
610 #[test]
611 fn test_has_four_children() {
612 let mut context = create_test_context();
613 let rect = rect![0, 0, 200, 50];
614 let toggle_event = Event::Toggle(ToggleEvent::View(ViewId::SettingsMenu));
615 let toggle = Toggle::new(
616 rect,
617 "On",
618 "Off",
619 true,
620 toggle_event,
621 &mut context.fonts,
622 Align::Center,
623 );
624
625 assert_eq!(toggle.children.len(), 4);
626 assert!(toggle.children[0].is::<Label>());
627 assert!(toggle.children[1].is::<Filler>());
628 assert!(toggle.children[2].is::<Label>());
629 assert!(toggle.children[3].is::<SelectionBox>());
630 }
631}