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