1use crate::color::SEPARATOR_NORMAL;
2use crate::context::Context;
3use crate::device::CURRENT_DEVICE;
4use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
5use crate::framebuffer::Framebuffer;
6use crate::geom::{Dir, Point, Rectangle};
7use crate::unit::scale_by_dpi;
8use crate::view::filler::Filler;
9use crate::view::UpdateMode;
10use crate::view::{Bus, Event, Hub, Id, RenderData, RenderQueue, View, ID_FEEDER};
11use crate::view::{SMALL_BAR_HEIGHT, THICKNESS_MEDIUM};
12use std::collections::BTreeMap;
13
14pub trait NavigationProvider {
27 type LevelKey: Eq + Ord + Clone;
29
30 type LevelData;
32
33 type Bar: View;
35
36 fn selected_leaf_key(&self, selected: &Self::LevelKey) -> Self::LevelKey {
40 selected.clone()
41 }
42
43 fn leaf_for_bar_traversal(
49 &self,
50 selected: &Self::LevelKey,
51 _context: &Context,
52 ) -> Self::LevelKey {
53 self.selected_leaf_key(selected)
54 }
55
56 fn parent(&self, current: &Self::LevelKey) -> Option<Self::LevelKey>;
58
59 fn is_ancestor(&self, ancestor: &Self::LevelKey, descendant: &Self::LevelKey) -> bool;
61
62 fn is_root(&self, key: &Self::LevelKey, context: &Context) -> bool;
64
65 fn fetch_level_data(&self, key: &Self::LevelKey, context: &mut Context) -> Self::LevelData;
67
68 fn estimate_line_count(&self, key: &Self::LevelKey, data: &Self::LevelData) -> usize;
91
92 fn create_bar(&self, rect: Rectangle, key: &Self::LevelKey) -> Self::Bar;
135
136 fn bar_key(&self, bar: &Self::Bar) -> Self::LevelKey;
138
139 fn update_bar(
141 &self,
142 bar: &mut Self::Bar,
143 data: &Self::LevelData,
144 selected: &Self::LevelKey,
145 fonts: &mut Fonts,
146 );
147
148 fn update_bar_selection(&self, bar: &mut Self::Bar, selected: &Self::LevelKey);
150
151 fn resize_bar_by(&self, bar: &mut Self::Bar, delta_y: i32, fonts: &mut Fonts) -> i32;
173
174 fn shift_bar(&self, bar: &mut Self::Bar, delta: Point);
176}
177
178#[derive(Debug)]
251pub struct StackNavigationBar<P: NavigationProvider + 'static> {
252 id: Id,
254 pub rect: Rectangle,
256 children: Vec<Box<dyn View>>,
258 selected: P::LevelKey,
260 pub vertical_limit: i32,
262 max_levels: usize,
264 provider: P,
266 enable_resize: bool,
268}
269
270impl<P: NavigationProvider + 'static> StackNavigationBar<P> {
271 pub fn new(
283 rect: Rectangle,
284 vertical_limit: i32,
285 max_levels: usize,
286 provider: P,
287 selected: P::LevelKey,
288 ) -> Self {
289 Self {
290 id: ID_FEEDER.next(),
291 rect,
292 children: Vec::new(),
293 selected,
294 vertical_limit,
295 max_levels,
296 provider,
297 enable_resize: true,
298 }
299 }
300
301 pub fn disable_resize(mut self) -> Self {
302 self.enable_resize = false;
303 self
304 }
305
306 pub fn clear(&mut self) {
308 self.children.clear();
309 }
310
311 pub fn selected(&self) -> &P::LevelKey {
313 &self.selected
314 }
315
316 pub fn set_selected(
337 &mut self,
338 selected: P::LevelKey,
339 rq: &mut RenderQueue,
340 context: &mut Context,
341 ) {
342 let layout = Layout::new(context);
343
344 let first_key = self.first_bar_key();
345 let mut last_key = self.last_bar_key();
346
347 self.trim_trailing_children(&selected, &mut last_key);
348
349 let data_by_level = self.prefetch_needed_levels(&selected, context);
350 let leaf = self.provider.leaf_for_bar_traversal(&selected, context);
351
352 let mut levels = 1usize;
353 let mut index = self.children.len();
354 let mut y_max = self.vertical_limit;
355
356 let mut current = leaf.clone();
357 loop {
358 if self.can_reuse_existing(&first_key, &last_key, ¤t) {
359 let db_index = index - 1;
360
361 let (next_index, new_y_max) =
362 self.reuse_existing_bar_and_separator(index, y_max, layout.thickness);
363
364 if self.children[db_index].rect().min.y < self.rect.min.y {
365 break;
366 }
367
368 index = next_index;
369 y_max = new_y_max;
370 levels += 1;
371 } else if self.should_insert_bar(&selected, ¤t, &data_by_level) {
372 let Some(data) = data_by_level.get(¤t) else {
373 break;
374 };
375
376 let (height, ok) = self.compute_bar_height(&layout, ¤t, data, y_max);
377 if !ok {
378 break;
379 }
380
381 self.insert_bar_and_separator(&layout, ¤t, height, &mut index, &mut y_max);
382 levels += 1;
383 }
384
385 if levels > self.max_levels || self.provider.is_root(¤t, context) {
386 break;
387 }
388
389 let Some(parent) = self.provider.parent(¤t) else {
390 break;
391 };
392
393 current = parent;
394 }
395
396 self.children.drain(..index);
397
398 self.ensure_minimum_bar(&layout, &selected);
399 self.remove_extra_leading_separator();
400
401 self.position_and_populate_children(
402 &selected,
403 &leaf,
404 &data_by_level,
405 &first_key,
406 &last_key,
407 rq,
408 &mut context.fonts,
409 );
410
411 self.rect.max.y = self.children[self.children.len() - 1].rect().max.y;
412 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Partial));
413
414 self.selected = selected;
415 }
416
417 #[inline]
418 fn first_bar_key(&self) -> Option<P::LevelKey> {
419 self.children
420 .first()
421 .and_then(|child| child.downcast_ref::<P::Bar>())
422 .map(|bar| self.provider.bar_key(bar))
423 }
424
425 #[inline]
426 fn last_bar_key(&self) -> Option<P::LevelKey> {
427 self.children
428 .last()
429 .and_then(|child| child.downcast_ref::<P::Bar>())
430 .map(|bar| self.provider.bar_key(bar))
431 }
432
433 #[inline]
447 fn trim_trailing_children(
448 &mut self,
449 selected: &P::LevelKey,
450 last_key: &mut Option<P::LevelKey>,
451 ) {
452 let Some(last) = last_key.clone() else {
453 return;
454 };
455
456 let Some((leftovers, ancestor)) =
457 find_closest_ancestor_by_provider(&self.provider, &last, selected)
458 else {
459 return;
460 };
461
462 if leftovers == 0 {
463 return;
464 }
465
466 self.children
467 .drain(self.children.len().saturating_sub(2 * leftovers)..);
468 *last_key = Some(ancestor);
469 }
470
471 #[inline]
472 fn prefetch_needed_levels(
473 &self,
474 selected: &P::LevelKey,
475 context: &mut Context,
476 ) -> BTreeMap<P::LevelKey, P::LevelData> {
477 let leaf_key = self.provider.selected_leaf_key(selected);
478 let mut data_by_level = BTreeMap::new();
479 let mut current = leaf_key.clone();
480
481 loop {
482 let data = self.provider.fetch_level_data(¤t, context);
483 data_by_level.insert(current.clone(), data);
484
485 if data_by_level.len() >= self.max_levels {
486 break;
487 }
488
489 if self.provider.is_root(¤t, context) {
490 break;
491 }
492
493 let Some(parent) = self.provider.parent(¤t) else {
494 break;
495 };
496
497 current = parent;
498 }
499
500 data_by_level
501 }
502
503 #[inline]
515 fn can_reuse_existing(
516 &self,
517 first: &Option<P::LevelKey>,
518 last: &Option<P::LevelKey>,
519 current: &P::LevelKey,
520 ) -> bool {
521 let (Some(first), Some(last)) = (first.as_ref(), last.as_ref()) else {
522 return false;
523 };
524
525 self.provider.is_ancestor(current, last) && self.provider.is_ancestor(first, current)
526 }
527
528 #[inline]
529 fn reuse_existing_bar_and_separator(
530 &mut self,
531 index: usize,
532 y_max: i32,
533 thickness: i32,
534 ) -> (usize, i32) {
535 let db_index = index - 1;
536 let sep_index = index.saturating_sub(2);
537
538 let y_shift = y_max - self.children[db_index].rect().max.y;
539 if let Some(bar) = self.children[db_index].downcast_mut::<P::Bar>() {
540 self.provider.shift_bar(bar, pt!(0, y_shift));
541 }
542
543 let mut next_y_max = y_max - self.children[db_index].rect().height() as i32;
544
545 if sep_index != db_index {
546 let y_shift = next_y_max - self.children[sep_index].rect().max.y;
547 *self.children[sep_index].rect_mut() += pt!(0, y_shift);
548 next_y_max -= thickness;
549 }
550
551 (sep_index, next_y_max)
552 }
553
554 #[inline]
567 fn should_insert_bar(
568 &self,
569 selected: &P::LevelKey,
570 current: &P::LevelKey,
571 data_by_level: &BTreeMap<P::LevelKey, P::LevelData>,
572 ) -> bool {
573 if current != selected {
574 return true;
575 }
576
577 data_by_level
578 .get(selected)
579 .map(|data| self.provider.estimate_line_count(selected, data) > 0)
580 .unwrap_or(false)
581 }
582
583 #[inline]
608 fn compute_bar_height(
609 &self,
610 layout: &Layout,
611 key: &P::LevelKey,
612 data: &P::LevelData,
613 y_max: i32,
614 ) -> (i32, bool) {
615 let count = self.provider.estimate_line_count(key, data).max(1) as i32;
616 let height = count * layout.x_height + (count + 1) * layout.padding / 2;
617
618 if y_max - height - layout.thickness < self.rect.min.y {
619 return (height, false);
620 }
621
622 (height, true)
623 }
624
625 #[inline]
658 fn insert_bar_and_separator(
659 &mut self,
660 layout: &Layout,
661 key: &P::LevelKey,
662 height: i32,
663 index: &mut usize,
664 y_max: &mut i32,
665 ) {
666 if self
667 .children
668 .get(*index)
669 .is_none_or(|child| child.is::<Filler>())
670 {
671 let rect = rect![self.rect.min.x, *y_max - height, self.rect.max.x, *y_max];
672 self.children
673 .insert(*index, Box::new(self.provider.create_bar(rect, key)));
674 *y_max -= height;
675
676 let sep_rect = rect![
677 self.rect.min.x,
678 *y_max - layout.thickness,
679 self.rect.max.x,
680 *y_max
681 ];
682 self.children
683 .insert(*index, Box::new(Filler::new(sep_rect, SEPARATOR_NORMAL)));
684 *y_max -= layout.thickness;
685
686 return;
687 }
688
689 let sep_rect = rect![
690 self.rect.min.x,
691 *y_max - layout.thickness,
692 self.rect.max.x,
693 *y_max
694 ];
695 self.children
696 .insert(*index, Box::new(Filler::new(sep_rect, SEPARATOR_NORMAL)));
697 *y_max -= layout.thickness;
698
699 let rect = rect![self.rect.min.x, *y_max - height, self.rect.max.x, *y_max];
700 self.children
701 .insert(*index, Box::new(self.provider.create_bar(rect, key)));
702 *y_max -= height;
703 }
704
705 #[inline]
706 fn ensure_minimum_bar(&mut self, layout: &Layout, selected: &P::LevelKey) {
707 if !self.children.is_empty() {
708 return;
709 }
710
711 let rect = rect![
712 self.rect.min.x,
713 self.rect.min.y,
714 self.rect.max.x,
715 self.rect.min.y + layout.min_height
716 ];
717
718 self.children
719 .push(Box::new(self.provider.create_bar(rect, selected)));
720 }
721
722 #[inline]
723 fn remove_extra_leading_separator(&mut self) {
724 if self.children.len().is_multiple_of(2) {
725 self.children.remove(0);
726 }
727 }
728
729 #[inline]
730 #[allow(clippy::too_many_arguments)]
731 fn position_and_populate_children(
732 &mut self,
733 selected: &P::LevelKey,
734 leaf: &P::LevelKey,
735 data_by_level: &BTreeMap<P::LevelKey, P::LevelData>,
736 first: &Option<P::LevelKey>,
737 last: &Option<P::LevelKey>,
738 rq: &mut RenderQueue,
739 fonts: &mut Fonts,
740 ) {
741 let mut current = leaf.clone();
742 let y_shift = self.rect.min.y - self.children[0].rect().min.y;
743
744 let mut index = self.children.len();
745 while index > 0 {
746 index -= 1;
747
748 if self.children[index].is::<Filler>() {
749 *self.children[index].rect_mut() += pt!(0, y_shift);
750 continue;
751 }
752
753 let bar = self.children[index].downcast_mut::<P::Bar>().unwrap();
754 self.provider.shift_bar(bar, pt!(0, y_shift));
755
756 let reuse_ok = first
757 .as_ref()
758 .zip(last.as_ref())
759 .is_some_and(|(first, last)| {
760 self.provider.is_ancestor(¤t, last)
761 && self.provider.is_ancestor(first, ¤t)
762 });
763
764 if !reuse_ok {
765 if let Some(data) = data_by_level.get(¤t) {
766 self.provider.update_bar(bar, data, selected, fonts);
767 }
768 } else if last.as_ref().is_some_and(|last| *last == current) {
769 self.provider.update_bar_selection(bar, selected);
770 }
771
772 let Some(parent) = self.provider.parent(¤t) else {
773 break;
774 };
775
776 current = parent;
777 }
778
779 self.rect.max.y = self.children[self.children.len() - 1].rect().max.y;
780 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Partial));
781 }
782
783 pub fn shift(&mut self, delta: Point) {
787 for child in &mut self.children {
788 if let Some(bar) = child.downcast_mut::<P::Bar>() {
789 self.provider.shift_bar(bar, delta);
790 } else {
791 *child.rect_mut() += delta;
792 }
793 }
794
795 self.rect += delta;
796 }
797
798 pub fn shrink(&mut self, delta_y: i32, fonts: &mut Fonts) -> i32 {
815 let layout = Layout::new_for_fonts(fonts);
816 let bars_count = self.children.len().div_ceil(2);
817 let mut values = vec![0; bars_count];
818
819 for (i, value) in values.iter_mut().enumerate().take(bars_count) {
820 *value = self.children[2 * i].rect().height() as i32 - layout.min_height;
821 }
822
823 let sum: i32 = values.iter().sum();
824 let mut y_shift = 0;
825
826 if sum > 0 {
827 for i in (0..bars_count).rev() {
828 let local_delta_y = ((values[i] as f32 / sum as f32) * delta_y as f32) as i32;
829 y_shift += self.resize_child(2 * i, local_delta_y, fonts);
830 if y_shift <= delta_y {
831 break;
832 }
833 }
834 }
835
836 while self.children.len() > 1 && y_shift > delta_y {
837 let mut dy = 0;
838 for child in self.children.drain(0..2) {
839 dy += child.rect().height() as i32;
840 }
841
842 for child in &mut self.children {
843 if let Some(bar) = child.downcast_mut::<P::Bar>() {
844 self.provider.shift_bar(bar, pt!(0, -dy));
845 } else {
846 *child.rect_mut() += pt!(0, -dy);
847 }
848 }
849
850 y_shift -= dy;
851 }
852
853 self.rect.max.y = self.children[self.children.len() - 1].rect().max.y;
854
855 y_shift
856 }
857
858 #[inline]
859 fn resize_child(&mut self, child_index: usize, delta_y: i32, fonts: &mut Fonts) -> i32 {
860 let layout = Layout::new_for_fonts(fonts);
861 let rect = *self.children[child_index].rect();
862
863 let delta_y_max = (self.vertical_limit - self.rect.max.y).max(0);
864 let y_max = (rect.max.y + delta_y.min(delta_y_max)).max(rect.min.y + layout.min_height);
865
866 let height = y_max - rect.min.y;
867
868 let count = ((height - layout.padding / 2) / (layout.x_height + layout.padding / 2)).max(1);
869 let height = count * layout.x_height + (count + 1) * layout.padding / 2;
870 let y_max = rect.min.y + height;
871
872 let y_shift = y_max - rect.max.y;
873
874 let bar = self.children[child_index].downcast_mut::<P::Bar>().unwrap();
875 let resized = self.provider.resize_bar_by(bar, y_shift, fonts);
876
877 for i in child_index + 1..self.children.len() {
878 if let Some(bar) = self.children[i].downcast_mut::<P::Bar>() {
879 self.provider.shift_bar(bar, pt!(0, resized));
880 } else {
881 *self.children[i].rect_mut() += pt!(0, resized);
882 }
883 }
884
885 self.rect.max.y = self.children[self.children.len() - 1].rect().max.y;
886
887 resized
888 }
889}
890
891#[derive(Debug, Clone, Copy)]
903struct Layout {
904 thickness: i32,
906 min_height: i32,
908 x_height: i32,
910 padding: i32,
912}
913
914impl Layout {
915 fn new(context: &mut Context) -> Self {
916 let dpi = CURRENT_DEVICE.dpi;
917 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
918 let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
919 let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
920 let x_height = font.x_heights.0 as i32;
921 let padding = min_height - x_height;
922
923 Self {
924 thickness,
925 min_height,
926 x_height,
927 padding,
928 }
929 }
930
931 fn new_for_fonts(fonts: &mut Fonts) -> Self {
932 let dpi = CURRENT_DEVICE.dpi;
933 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
934 let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
935 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
936 let x_height = font.x_heights.0 as i32;
937 let padding = min_height - x_height;
938
939 Self {
940 thickness,
941 min_height,
942 x_height,
943 padding,
944 }
945 }
946}
947
948#[inline]
974fn find_closest_ancestor_by_provider<P: NavigationProvider>(
975 provider: &P,
976 last: &P::LevelKey,
977 selected: &P::LevelKey,
978) -> Option<(usize, P::LevelKey)> {
979 let mut count = 0usize;
980 let mut current = last.clone();
981
982 while count < 128 {
983 if provider.is_ancestor(¤t, selected) {
984 return Some((count, current));
985 }
986
987 let parent = provider.parent(¤t)?;
988
989 current = parent;
990 count += 1;
991 }
992
993 None
994}
995
996impl<P: NavigationProvider + 'static> View for StackNavigationBar<P> {
997 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, _rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
998 fn handle_event(
999 &mut self,
1000 evt: &Event,
1001 _hub: &Hub,
1002 bus: &mut Bus,
1003 _rq: &mut RenderQueue,
1004 context: &mut Context,
1005 ) -> bool {
1006 match *evt {
1007 Event::Gesture(crate::gesture::GestureEvent::Swipe {
1008 dir, start, end, ..
1009 }) if self.enable_resize && (self.rect.includes(start) || self.rect.includes(end)) => {
1010 match dir {
1011 Dir::North | Dir::South => {
1012 let pt = if dir == Dir::North { end } else { start };
1013
1014 let bar_index = (0..self.children.len())
1015 .step_by(2)
1016 .find(|&index| self.children[index].rect().includes(pt));
1017
1018 if let Some(index) = bar_index {
1019 let delta_y = end.y - start.y;
1020 let resized = self.resize_child(index, delta_y, &mut context.fonts);
1021 bus.push_back(Event::NavigationBarResized(resized));
1022 }
1023
1024 true
1025 }
1026 _ => false,
1027 }
1028 }
1029 _ => false,
1030 }
1031 }
1032
1033 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
1034 fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
1035
1036 fn rect(&self) -> &Rectangle {
1037 &self.rect
1038 }
1039
1040 fn rect_mut(&mut self) -> &mut Rectangle {
1041 &mut self.rect
1042 }
1043
1044 fn children(&self) -> &Vec<Box<dyn View>> {
1045 &self.children
1046 }
1047
1048 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
1049 &mut self.children
1050 }
1051
1052 fn id(&self) -> Id {
1053 self.id
1054 }
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060
1061 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1062 struct Key(i32);
1063
1064 struct Provider;
1065
1066 impl NavigationProvider for Provider {
1067 type LevelKey = Key;
1068 type LevelData = usize;
1069 type Bar = Filler;
1070
1071 fn parent(&self, current: &Self::LevelKey) -> Option<Self::LevelKey> {
1072 if current.0 == 0 {
1073 return None;
1074 }
1075
1076 Some(Key(current.0 - 1))
1077 }
1078
1079 fn is_ancestor(&self, ancestor: &Self::LevelKey, descendant: &Self::LevelKey) -> bool {
1080 ancestor.0 <= descendant.0
1081 }
1082
1083 fn is_root(&self, key: &Self::LevelKey, _context: &Context) -> bool {
1084 key.0 == 0
1085 }
1086
1087 fn fetch_level_data(
1088 &self,
1089 key: &Self::LevelKey,
1090 _context: &mut Context,
1091 ) -> Self::LevelData {
1092 key.0 as usize
1093 }
1094
1095 fn estimate_line_count(&self, _key: &Self::LevelKey, data: &Self::LevelData) -> usize {
1096 *data
1097 }
1098
1099 fn create_bar(&self, rect: Rectangle, _key: &Self::LevelKey) -> Self::Bar {
1100 Filler::new(rect, SEPARATOR_NORMAL)
1101 }
1102
1103 fn bar_key(&self, _bar: &Self::Bar) -> Self::LevelKey {
1104 Key(0)
1105 }
1106
1107 fn update_bar(
1108 &self,
1109 _bar: &mut Self::Bar,
1110 _data: &Self::LevelData,
1111 _selected: &Self::LevelKey,
1112 _fonts: &mut Fonts,
1113 ) {
1114 }
1115
1116 fn update_bar_selection(&self, _bar: &mut Self::Bar, _selected: &Self::LevelKey) {}
1117
1118 fn resize_bar_by(&self, bar: &mut Self::Bar, delta_y: i32, _fonts: &mut Fonts) -> i32 {
1119 let rect = *bar.rect();
1120 let dpi = CURRENT_DEVICE.dpi;
1121 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
1122 let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
1123
1124 let y_max = (rect.max.y + delta_y).max(rect.min.y + min_height);
1125 let resized = y_max - rect.max.y;
1126
1127 bar.rect_mut().max.y = y_max;
1128
1129 resized
1130 }
1131
1132 fn shift_bar(&self, bar: &mut Self::Bar, delta: Point) {
1133 *bar.rect_mut() += delta;
1134 }
1135 }
1136
1137 #[test]
1138 fn closest_ancestor_count_is_distance() {
1139 let provider = Provider;
1140 let last = Key(5);
1141 let selected = Key(3);
1142
1143 let (count, ancestor) =
1144 find_closest_ancestor_by_provider(&provider, &last, &selected).unwrap();
1145 assert_eq!(count, 2);
1146 assert_eq!(ancestor, Key(3));
1147 }
1148
1149 #[test]
1150 fn closest_ancestor_is_none_when_unrelated() {
1151 let provider = Provider;
1152 let last = Key(5);
1153 let selected = Key(-1);
1154
1155 assert!(find_closest_ancestor_by_provider(&provider, &last, &selected).is_none());
1156 }
1157
1158 fn create_test_context_for_nav_bar() -> Context {
1159 use crate::battery::{Battery, FakeBattery};
1160 use crate::framebuffer::Pixmap;
1161 use crate::frontlight::{Frontlight, LightLevels};
1162 use crate::lightsensor::LightSensor;
1163 use std::env;
1164 use std::path::Path;
1165
1166 let fb = Box::new(Pixmap::new(600, 800, 1)) as Box<dyn Framebuffer>;
1167 let battery = Box::new(FakeBattery::new()) as Box<dyn Battery>;
1168 let frontlight = Box::new(LightLevels::default()) as Box<dyn Frontlight>;
1169 let lightsensor = Box::new(0u16) as Box<dyn LightSensor>;
1170
1171 let fonts = crate::font::Fonts::load_from(
1172 Path::new(
1173 &env::var("TEST_ROOT_DIR").expect("TEST_ROOT_DIR must be set for this test."),
1174 )
1175 .to_path_buf(),
1176 )
1177 .expect(
1178 "Failed to load fonts. Tests require font files to be present. \
1179 Run tests from the project root directory.",
1180 );
1181
1182 Context::new(
1183 fb,
1184 None,
1185 crate::library::Library::new(Path::new("/tmp"), crate::settings::LibraryMode::Database)
1186 .unwrap(),
1187 crate::settings::Settings::default(),
1188 fonts,
1189 battery,
1190 frontlight,
1191 lightsensor,
1192 )
1193 }
1194
1195 #[test]
1196 fn set_selected_with_single_child_no_panic() {
1197 let mut context = create_test_context_for_nav_bar();
1198
1199 let provider = Provider;
1200 let rect = rect![0, 0, 600, 100];
1201 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1202 let mut rq = RenderQueue::new();
1203
1204 nav_bar.set_selected(Key(0), &mut rq, &mut context);
1205 assert!(!nav_bar.children.is_empty());
1206
1207 nav_bar.set_selected(Key(1), &mut rq, &mut context);
1208 assert!(!nav_bar.children.is_empty());
1209 }
1210
1211 #[test]
1212 fn set_selected_from_empty_state() {
1213 let mut context = create_test_context_for_nav_bar();
1214
1215 let provider = Provider;
1216 let rect = rect![0, 0, 600, 100];
1217 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1218 let mut rq = RenderQueue::new();
1219
1220 assert!(nav_bar.children.is_empty());
1221
1222 nav_bar.set_selected(Key(3), &mut rq, &mut context);
1223
1224 assert!(!nav_bar.children.is_empty());
1225 assert_eq!(nav_bar.selected, Key(3));
1226 }
1227
1228 #[test]
1229 fn set_selected_reuses_existing_bars() {
1230 let mut context = create_test_context_for_nav_bar();
1231
1232 let provider = Provider;
1233 let rect = rect![0, 0, 600, 200];
1234 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1235 let mut rq = RenderQueue::new();
1236
1237 nav_bar.set_selected(Key(2), &mut rq, &mut context);
1238 assert!(!nav_bar.children.is_empty());
1239
1240 nav_bar.set_selected(Key(3), &mut rq, &mut context);
1241
1242 assert!(!nav_bar.children.is_empty());
1243 assert_eq!(nav_bar.selected, Key(3));
1244 }
1245
1246 #[test]
1247 fn set_selected_to_parent_reduces_bars() {
1248 let mut context = create_test_context_for_nav_bar();
1249
1250 let provider = Provider;
1251 let rect = rect![0, 0, 600, 200];
1252 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1253 let mut rq = RenderQueue::new();
1254
1255 nav_bar.set_selected(Key(5), &mut rq, &mut context);
1256 assert!(!nav_bar.children.is_empty());
1257
1258 nav_bar.set_selected(Key(2), &mut rq, &mut context);
1259
1260 assert!(!nav_bar.children.is_empty());
1261 assert_eq!(nav_bar.selected, Key(2));
1262 }
1263
1264 #[test]
1265 fn set_selected_handles_max_levels() {
1266 let mut context = create_test_context_for_nav_bar();
1267
1268 let provider = Provider;
1269 let rect = rect![0, 0, 600, 200];
1270 let max_levels = 3;
1271 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, max_levels, provider, Key(0));
1272 let mut rq = RenderQueue::new();
1273
1274 nav_bar.set_selected(Key(10), &mut rq, &mut context);
1275
1276 assert!(!nav_bar.children.is_empty());
1277 }
1278
1279 #[test]
1280 fn resize_child_with_aggressive_north_swipe_maintains_minimum_height() {
1281 let mut context = create_test_context_for_nav_bar();
1282
1283 let provider = Provider;
1284 let rect = rect![0, 68, 600, 590];
1285 let vertical_limit = 642;
1286 let mut nav_bar = StackNavigationBar::new(rect, vertical_limit, 1, provider, Key(0));
1287 let mut rq = RenderQueue::new();
1288
1289 nav_bar.set_selected(Key(0), &mut rq, &mut context);
1290 assert_eq!(nav_bar.children.len(), 1);
1291
1292 let initial_rect = *nav_bar.children[0].rect();
1293 let initial_height = initial_rect.height() as i32;
1294
1295 let aggressive_delta_y = -(initial_height * 2);
1296 nav_bar.resize_child(0, aggressive_delta_y, &mut context.fonts);
1297
1298 let dpi = CURRENT_DEVICE.dpi;
1299 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
1300 let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
1301
1302 let final_child_rect = *nav_bar.children[0].rect();
1303 let final_height = final_child_rect.height() as i32;
1304
1305 assert!(
1306 final_height >= min_height,
1307 "Child bar height {} should be at least min_height {}",
1308 final_height,
1309 min_height
1310 );
1311
1312 let container_height = nav_bar.rect.max.y - nav_bar.rect.min.y;
1313 assert!(
1314 container_height >= min_height,
1315 "Container height {} should be at least min_height {}. Container rect: {:?}",
1316 container_height,
1317 min_height,
1318 nav_bar.rect
1319 );
1320
1321 assert_eq!(
1322 nav_bar.rect.max.y, final_child_rect.max.y,
1323 "Container max.y should match last child's max.y"
1324 );
1325 }
1326
1327 #[test]
1328 fn shrink_proportionally_distributes_across_multiple_bars() {
1329 let mut context = create_test_context_for_nav_bar();
1330
1331 let provider = Provider;
1332 let rect = rect![0, 0, 600, 400];
1333 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1334 let mut rq = RenderQueue::new();
1335
1336 nav_bar.set_selected(Key(3), &mut rq, &mut context);
1337
1338 let initial_heights: Vec<i32> = (0..nav_bar.children.len())
1339 .step_by(2)
1340 .map(|i| nav_bar.children[i].rect().height() as i32)
1341 .collect();
1342
1343 let shrink_amount = -50;
1344 let actual_shrink = nav_bar.shrink(shrink_amount, &mut context.fonts);
1345
1346 let final_heights: Vec<i32> = (0..nav_bar.children.len())
1347 .step_by(2)
1348 .map(|i| nav_bar.children[i].rect().height() as i32)
1349 .collect();
1350
1351 assert!(actual_shrink <= 0, "Should return negative shrink amount");
1352 assert!(
1353 actual_shrink <= shrink_amount,
1354 "Actual shrink should be at most the requested amount (more negative = more shrink)"
1355 );
1356
1357 for (initial, final_h) in initial_heights.iter().zip(final_heights.iter()) {
1358 assert!(
1359 final_h <= initial,
1360 "Each bar should shrink or stay same: initial={}, final={}",
1361 initial,
1362 final_h
1363 );
1364 }
1365 }
1366
1367 #[test]
1368 fn shrink_removes_bars_when_exceeding_available_space() {
1369 let mut context = create_test_context_for_nav_bar();
1370
1371 let provider = Provider;
1372 let rect = rect![0, 0, 600, 300];
1373 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1374 let mut rq = RenderQueue::new();
1375
1376 nav_bar.set_selected(Key(3), &mut rq, &mut context);
1377
1378 let initial_bar_count = nav_bar.children.len().div_ceil(2);
1379
1380 let aggressive_shrink = -500;
1381 nav_bar.shrink(aggressive_shrink, &mut context.fonts);
1382
1383 let final_bar_count = nav_bar.children.len().div_ceil(2);
1384
1385 assert!(
1386 final_bar_count <= initial_bar_count,
1387 "Bar count should decrease or stay same when shrinking aggressively"
1388 );
1389 assert!(final_bar_count >= 1, "Should always keep at least one bar");
1390 }
1391
1392 #[test]
1393 fn shrink_handles_all_bars_at_minimum_height() {
1394 let mut context = create_test_context_for_nav_bar();
1395
1396 let provider = Provider;
1397 let rect = rect![0, 0, 600, 100];
1398 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 2, provider, Key(0));
1399 let mut rq = RenderQueue::new();
1400
1401 nav_bar.set_selected(Key(1), &mut rq, &mut context);
1402
1403 let dpi = CURRENT_DEVICE.dpi;
1404 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
1405 let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
1406
1407 for i in (0..nav_bar.children.len()).step_by(2) {
1408 let bar = nav_bar.children[i].downcast_mut::<Filler>().unwrap();
1409 bar.rect_mut().max.y = bar.rect().min.y + min_height;
1410 }
1411
1412 let shrink_amount = -20;
1413 let actual_shrink = nav_bar.shrink(shrink_amount, &mut context.fonts);
1414
1415 assert!(
1416 actual_shrink <= 0,
1417 "When all bars at minimum, shrink should remove bars or do nothing"
1418 );
1419 }
1420
1421 #[test]
1422 fn resize_child_expansion_respects_vertical_limit() {
1423 let mut context = create_test_context_for_nav_bar();
1424
1425 let provider = Provider;
1426 let rect = rect![0, 0, 600, 200];
1427 let vertical_limit = 250;
1428 let mut nav_bar = StackNavigationBar::new(rect, vertical_limit, 3, provider, Key(0));
1429 let mut rq = RenderQueue::new();
1430
1431 nav_bar.set_selected(Key(2), &mut rq, &mut context);
1432
1433 let last_bar_index = ((nav_bar.children.len() - 1) / 2) * 2;
1434 let initial_container_max = nav_bar.rect.max.y;
1435
1436 let large_expansion = 200;
1437 let actual_resize =
1438 nav_bar.resize_child(last_bar_index, large_expansion, &mut context.fonts);
1439
1440 let final_container_max = nav_bar.rect.max.y;
1441 let expected_max = (initial_container_max + actual_resize).min(vertical_limit);
1442
1443 assert!(
1444 final_container_max <= vertical_limit,
1445 "Navigation bar should not exceed vertical_limit: {} > {}",
1446 final_container_max,
1447 vertical_limit
1448 );
1449
1450 assert_eq!(
1451 final_container_max, expected_max,
1452 "Container should expand by actual_resize amount or hit vertical_limit"
1453 );
1454 }
1455
1456 #[test]
1457 fn resize_child_expansion_shifts_subsequent_children() {
1458 let mut context = create_test_context_for_nav_bar();
1459
1460 let provider = Provider;
1461 let rect = rect![0, 0, 600, 300];
1462 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1463 let mut rq = RenderQueue::new();
1464
1465 nav_bar.set_selected(Key(3), &mut rq, &mut context);
1466
1467 if nav_bar.children.len() < 4 {
1468 return;
1469 }
1470
1471 let target_index = 0;
1472 let initial_rects: Vec<Rectangle> = nav_bar
1473 .children
1474 .iter()
1475 .skip(target_index + 1)
1476 .map(|child| *child.rect())
1477 .collect();
1478
1479 let expansion = 20;
1480 let actual_resize = nav_bar.resize_child(target_index, expansion, &mut context.fonts);
1481
1482 let final_rects: Vec<Rectangle> = nav_bar
1483 .children
1484 .iter()
1485 .skip(target_index + 1)
1486 .map(|child| *child.rect())
1487 .collect();
1488
1489 for (initial, final_rect) in initial_rects.iter().zip(final_rects.iter()) {
1490 let shift = final_rect.min.y - initial.min.y;
1491 assert_eq!(
1492 shift, actual_resize,
1493 "All subsequent children should shift by the actual resize amount"
1494 );
1495 }
1496 }
1497
1498 #[test]
1499 fn shift_moves_all_children_and_container() {
1500 let mut context = create_test_context_for_nav_bar();
1501
1502 let provider = Provider;
1503 let rect = rect![0, 0, 600, 200];
1504 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1505 let mut rq = RenderQueue::new();
1506
1507 nav_bar.set_selected(Key(2), &mut rq, &mut context);
1508
1509 let initial_container = nav_bar.rect;
1510 let initial_child_rects: Vec<Rectangle> =
1511 nav_bar.children.iter().map(|c| *c.rect()).collect();
1512
1513 let delta = pt!(10, 20);
1514 nav_bar.shift(delta);
1515
1516 assert_eq!(
1517 nav_bar.rect,
1518 initial_container + delta,
1519 "Container should shift by delta"
1520 );
1521
1522 for (i, initial_rect) in initial_child_rects.iter().enumerate() {
1523 let expected = *initial_rect + delta;
1524 assert_eq!(
1525 *nav_bar.children[i].rect(),
1526 expected,
1527 "Child {} should shift by delta",
1528 i
1529 );
1530 }
1531 }
1532
1533 #[test]
1534 fn handle_event_north_swipe_resizes_bar() {
1535 use crate::gesture::GestureEvent;
1536
1537 let mut context = create_test_context_for_nav_bar();
1538
1539 let provider = Provider;
1540 let rect = rect![0, 100, 600, 300];
1541 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1542 let mut rq = RenderQueue::new();
1543
1544 nav_bar.set_selected(Key(2), &mut rq, &mut context);
1545
1546 let (tx, _rx) = std::sync::mpsc::channel();
1547 let hub = tx;
1548 let mut bus = std::collections::VecDeque::new();
1549
1550 let start = pt!(300, 200);
1551 let end = pt!(300, 150);
1552
1553 let event = Event::Gesture(GestureEvent::Swipe {
1554 dir: Dir::North,
1555 start,
1556 end,
1557 });
1558
1559 let handled = nav_bar.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
1560
1561 assert!(handled, "North swipe should be handled");
1562
1563 let events: Vec<Event> = bus.drain(..).collect();
1564 assert!(
1565 events
1566 .iter()
1567 .any(|e| matches!(e, Event::NavigationBarResized(_))),
1568 "Should emit NavigationBarResized event"
1569 );
1570 }
1571
1572 #[test]
1573 fn handle_event_south_swipe_resizes_bar() {
1574 use crate::gesture::GestureEvent;
1575
1576 let mut context = create_test_context_for_nav_bar();
1577
1578 let provider = Provider;
1579 let rect = rect![0, 100, 600, 300];
1580 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1581 let mut rq = RenderQueue::new();
1582
1583 nav_bar.set_selected(Key(2), &mut rq, &mut context);
1584
1585 let (tx, _rx) = std::sync::mpsc::channel();
1586 let hub = tx;
1587 let mut bus = std::collections::VecDeque::new();
1588
1589 let start = pt!(300, 150);
1590 let end = pt!(300, 200);
1591
1592 let event = Event::Gesture(GestureEvent::Swipe {
1593 dir: Dir::South,
1594 start,
1595 end,
1596 });
1597
1598 let handled = nav_bar.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
1599
1600 assert!(handled, "South swipe should be handled");
1601
1602 let events: Vec<Event> = bus.drain(..).collect();
1603 assert!(
1604 events
1605 .iter()
1606 .any(|e| matches!(e, Event::NavigationBarResized(_))),
1607 "Should emit NavigationBarResized event"
1608 );
1609 }
1610
1611 #[test]
1612 fn handle_event_ignores_swipe_outside_rect() {
1613 use crate::gesture::GestureEvent;
1614
1615 let mut context = create_test_context_for_nav_bar();
1616
1617 let provider = Provider;
1618 let rect = rect![0, 100, 600, 300];
1619 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1620 let mut rq = RenderQueue::new();
1621
1622 nav_bar.set_selected(Key(2), &mut rq, &mut context);
1623
1624 let (tx, _rx) = std::sync::mpsc::channel();
1625 let hub = tx;
1626 let mut bus = std::collections::VecDeque::new();
1627
1628 let start = pt!(300, 50);
1629 let end = pt!(300, 10);
1630
1631 let event = Event::Gesture(GestureEvent::Swipe {
1632 dir: Dir::North,
1633 start,
1634 end,
1635 });
1636
1637 let handled = nav_bar.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
1638
1639 assert!(
1640 !handled,
1641 "Swipe outside rect should not be handled when both points are outside"
1642 );
1643 }
1644
1645 #[test]
1646 fn handle_event_ignores_horizontal_swipe() {
1647 use crate::gesture::GestureEvent;
1648
1649 let mut context = create_test_context_for_nav_bar();
1650
1651 let provider = Provider;
1652 let rect = rect![0, 100, 600, 300];
1653 let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1654 let mut rq = RenderQueue::new();
1655
1656 nav_bar.set_selected(Key(2), &mut rq, &mut context);
1657
1658 let (tx, _rx) = std::sync::mpsc::channel();
1659 let hub = tx;
1660 let mut bus = std::collections::VecDeque::new();
1661
1662 let start = pt!(200, 200);
1663 let end = pt!(400, 200);
1664
1665 let event = Event::Gesture(GestureEvent::Swipe {
1666 dir: Dir::East,
1667 start,
1668 end,
1669 });
1670
1671 let handled = nav_bar.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
1672
1673 assert!(!handled, "Horizontal swipe should not be handled");
1674 }
1675
1676 #[test]
1677 fn set_selected_handles_vertical_limit_constraint() {
1678 let mut context = create_test_context_for_nav_bar();
1679
1680 let provider = Provider;
1681 let rect = rect![0, 0, 600, 50];
1682 let vertical_limit = 100;
1683 let mut nav_bar = StackNavigationBar::new(rect, vertical_limit, 10, provider, Key(0));
1684 let mut rq = RenderQueue::new();
1685
1686 nav_bar.set_selected(Key(10), &mut rq, &mut context);
1687
1688 assert!(
1689 nav_bar.rect.max.y <= vertical_limit,
1690 "Navigation bar should respect vertical_limit even with many levels"
1691 );
1692
1693 assert!(
1694 !nav_bar.children.is_empty(),
1695 "Should have at least one bar even with tight constraints"
1696 );
1697 }
1698}