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