cadmus_core/view/navigation/
stack_navigation_bar.rs

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
14/// Domain adapter for [`StackNavigationBar`].
15///
16/// A `NavigationProvider` tells the container how to traverse hierarchical levels
17/// (e.g. directory parents), and how to populate each bar with pre-fetched data.
18/// This trait abstracts the domain-specific logic from the navigation bar's layout
19/// and interaction logic.
20///
21/// # Implementation Notes
22///
23/// When implementing `resize_bar_by()`, ensure the bar respects minimum height
24/// constraints (typically `SMALL_BAR_HEIGHT - THICKNESS_MEDIUM` scaled by DPI).
25/// The method should return the actual resize amount after applying constraints.
26pub trait NavigationProvider {
27    /// Key that identifies a level in the stack.
28    type LevelKey: Eq + Ord + Clone;
29
30    /// Data needed to render a level.
31    type LevelData;
32
33    /// Concrete view used to render a level.
34    type Bar: View;
35
36    /// Returns the key to consider "selected".
37    ///
38    /// Some domains want to select the parent when the leaf level is empty.
39    fn selected_leaf_key(&self, selected: &Self::LevelKey) -> Self::LevelKey {
40        selected.clone()
41    }
42
43    /// Returns the starting key for bar traversal.
44    ///
45    /// This may differ from `selected` when the selected level is empty.
46    /// For example, if a directory has no subdirectories, this might return
47    /// the parent directory to start the bar hierarchy from there.
48    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    /// Returns the parent key, if any.
57    fn parent(&self, current: &Self::LevelKey) -> Option<Self::LevelKey>;
58
59    /// Returns true if `ancestor` is an ancestor of `descendant`.
60    fn is_ancestor(&self, ancestor: &Self::LevelKey, descendant: &Self::LevelKey) -> bool;
61
62    /// Returns true if the key is the root of the stack.
63    fn is_root(&self, key: &Self::LevelKey, context: &Context) -> bool;
64
65    /// Fetch the data for a level.
66    fn fetch_level_data(&self, key: &Self::LevelKey, context: &mut Context) -> Self::LevelData;
67
68    /// Estimates how many visual lines (rows) the bar will need to display its content.
69    ///
70    /// This value is used to calculate the vertical height of the bar. Each line
71    /// corresponds to one row in the visual layout:
72    /// - For vertical layouts (e.g., DirectoriesBar), this typically equals the
73    ///   number of items to display since each item occupies one line.
74    /// - For horizontal layouts (e.g., CategoryNavigationBar), this should return
75    ///   `1` since all items are arranged horizontally on a single line.
76    ///
77    /// The height formula is:
78    /// ```rust,ignore
79    /// height = line_count * x_height + (line_count + 1) * padding / 2
80    /// ```
81    ///
82    /// # Returns
83    ///
84    /// The number of visual lines needed.
85    ///
86    /// Returning `0` indicates that the level has no visible content and allows
87    /// `StackNavigationBar` to treat this level as empty (for example, by not
88    /// inserting a bar for it). Values `>= 1` correspond to the number of visual
89    /// lines that should be allocated for the bar's content.
90    fn estimate_line_count(&self, key: &Self::LevelKey, data: &Self::LevelData) -> usize;
91
92    /// Creates a new empty bar for the given level key.
93    ///
94    /// This method is responsible for instantiating a concrete bar view that will
95    /// display content for a specific level in the navigation hierarchy. The bar is
96    /// created with an initial rectangle and is positioned by `StackNavigationBar`.
97    ///
98    /// The returned bar should be empty or minimally initialized, its content will be
99    /// populated later via `update_bar()` once the necessary data is fetched from the
100    /// domain layer. This separation allows bars to be created before their content
101    /// is available, enabling flexible reuse and repositioning strategies.
102    ///
103    /// # Arguments
104    ///
105    /// * `rect` - The initial rectangle where the bar will be positioned. This
106    ///   rectangle is computed by `StackNavigationBar` based on layout metrics and
107    ///   available space. The bar should use this rect as its initial bounds.
108    /// * `key` - The level identifier (e.g., a directory path or category ID) that
109    ///   uniquely identifies which level this bar represents in the hierarchy.
110    ///
111    /// # Returns
112    ///
113    /// A new bar view instance initialized with the provided rectangle. The bar's
114    /// content should be empty or a placeholder at this point.
115    ///
116    /// # Implementation Notes
117    ///
118    /// - The bar's rectangle **must** be stored and accessible via the `View` trait's
119    ///   `rect()` and `rect_mut()` methods.
120    /// - Do not fetch or populate content in this method; that happens in `update_bar()`.
121    /// - The `key` parameter is provided for reference but typically stored separately
122    ///   by the domain layer (see `bar_key()` to retrieve it).
123    /// - If the concrete bar type needs additional context (e.g., fonts or device info)
124    ///   during creation, access it from a shared source rather than requiring it as
125    ///   a method parameter.
126    ///
127    /// # Example
128    ///
129    /// ```rust,ignore
130    /// fn create_bar(&self, rect: Rectangle, key: &Self::LevelKey) -> Self::Bar {
131    ///     MyBar::new(rect, key.clone())
132    /// }
133    /// ```
134    fn create_bar(&self, rect: Rectangle, key: &Self::LevelKey) -> Self::Bar;
135
136    /// Returns the key that is currently displayed by a bar.
137    fn bar_key(&self, bar: &Self::Bar) -> Self::LevelKey;
138
139    /// Update bar content using only fonts (no context borrowing).
140    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    /// Update bar selection when the content is unchanged.
149    fn update_bar_selection(&self, bar: &mut Self::Bar, selected: &Self::LevelKey);
150
151    /// Apply a vertical resize delta to a bar.
152    ///
153    /// This method should mutate the bar's rectangle and update its content to
154    /// reflect the new size. The bar must enforce minimum height constraints
155    /// (typically `SMALL_BAR_HEIGHT - THICKNESS_MEDIUM` scaled by DPI).
156    ///
157    /// # Arguments
158    ///
159    /// * `bar` - The bar to resize
160    /// * `delta_y` - The vertical resize amount (positive = grow, negative = shrink)
161    /// * `fonts` - Font registry for text rendering calculations
162    ///
163    /// # Returns
164    ///
165    /// The actual resize amount applied after enforcing constraints. This may differ
166    /// from `delta_y` if minimum/maximum height limits are reached.
167    ///
168    /// # Important
169    ///
170    /// Do NOT pre-modify the bar's rect before calling this method. The provider
171    /// will handle the entire resize operation, including constraint enforcement.
172    fn resize_bar_by(&self, bar: &mut Self::Bar, delta_y: i32, fonts: &mut Fonts) -> i32;
173
174    /// Shift a bar by a delta.
175    fn shift_bar(&self, bar: &mut Self::Bar, delta: Point);
176}
177
178/// A vertically-stacked navigation bar with dynamic height and level management.
179///
180/// `StackNavigationBar` displays a stack of navigation levels (e.g., directory hierarchy)
181/// with separators between them. It supports interactive resizing via swipe gestures,
182/// automatic level management based on available space, and reuse of existing bars
183/// when navigating to related items.
184///
185/// # Architecture
186///
187/// The navigation bar uses a generic `NavigationProvider` trait to abstract domain-specific
188/// logic (e.g., file system navigation, category hierarchies). This separation allows the
189/// same container to work with different hierarchical data structures.
190///
191/// # Layout Structure
192///
193/// Children are stored in alternating order:
194/// - Even indices (0, 2, 4...): Navigation bars for each level
195/// - Odd indices (1, 3, 5...): Separator fillers between bars
196///
197/// The container's rect is dynamically adjusted to match the total height of all children.
198///
199/// ## ASCII illustration (top = smaller y, bottom = larger y):
200///
201/// ```txt
202///   container.rect.min.y
203///   +--------------------------------------+
204///   | Bar (index 0)                        |  <-- even indices are bars (level 0)
205///   +--------------------------------------+
206///   | Separator (index 1)                  |  <-- odd indices are separators
207///   +--------------------------------------+
208///   | Bar (index 2)                        |  <-- even indices are bars (level 1)
209///   +--------------------------------------+
210///   | Separator (index 3)                  |
211///   +--------------------------------------+
212///   | Bar (index 4)                        |  <-- deeper level / leaf
213///   +--------------------------------------+
214///   container.rect.max.y
215/// ```
216///
217/// The diagram shows the alternating bar/separator pattern and how the container's
218/// min.y and max.y encompass the stacked children.
219///
220/// # Interactive Resize
221///
222/// Users can resize individual bars via vertical (up/down) swipe gestures. The container:
223/// 1. Calculates the desired size based on grid-snapped line counts
224/// 2. Delegates actual resize to the provider via `resize_bar_by()`
225/// 3. Updates the container rect to match the last child's position
226///
227/// Minimum height constraints are enforced by the provider to prevent 1px collapse bugs.
228///
229/// # Level Management
230///
231/// When `set_selected()` is called:
232/// 1. Existing bars are reused when navigating to ancestors/descendants
233/// 2. New bars are created only when needed
234/// 3. Excess bars (beyond `max_levels`) are trimmed
235/// 4. Empty levels are skipped unless they're the selected level
236///
237/// # Type Parameters
238///
239/// * `P` - The navigation provider that implements domain-specific traversal logic
240///
241/// # Why `P: 'static`?
242///
243/// The view tree stores views as owned trait objects (`Box<dyn View>`) inside containers.
244/// Those boxed trait objects are used without borrowing from caller stack frames or
245/// tied lifetimes, so the concrete view types placed in the boxes must not contain
246/// non-'static references. `StackNavigationBar` owns its `provider: P` field directly,
247/// therefore to safely store `StackNavigationBar<P>` as a boxed view the provider type
248/// must be `'static`. This keeps the view-tree API simple and avoids needing to
249/// propagate lifetimes through the entire view hierarchy.
250#[derive(Debug)]
251pub struct StackNavigationBar<P: NavigationProvider + 'static> {
252    /// Unique view identifier
253    id: Id,
254    /// Container rectangle (dynamically adjusted to fit children)
255    pub rect: Rectangle,
256    /// Child views: bars at even indices, separators at odd indices
257    children: Vec<Box<dyn View>>,
258    /// Currently selected level key
259    selected: P::LevelKey,
260    /// Maximum Y coordinate for the navigation bar's bottom edge
261    pub vertical_limit: i32,
262    /// Maximum number of levels to display simultaneously
263    max_levels: usize,
264    /// Domain-specific navigation logic provider
265    provider: P,
266    /// If this bar type should allow resizing via gesture
267    enable_resize: bool,
268}
269
270impl<P: NavigationProvider + 'static> StackNavigationBar<P> {
271    /// Creates a new navigation bar.
272    ///
273    /// The bar starts empty and must be populated via `set_selected()`.
274    ///
275    /// # Arguments
276    ///
277    /// * `rect` - Initial container rectangle
278    /// * `vertical_limit` - Maximum Y coordinate for the bar's bottom edge
279    /// * `max_levels` - Maximum number of hierarchy levels to display
280    /// * `provider` - Domain-specific navigation provider
281    /// * `selected` - Initial selected level (bar remains empty until `set_selected()` is called)
282    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    /// Removes all child bars and separators.
307    pub fn clear(&mut self) {
308        self.children.clear();
309    }
310
311    /// Returns the currently selected level key.
312    pub fn selected(&self) -> &P::LevelKey {
313        &self.selected
314    }
315
316    /// Updates the selected level and rebuilds the navigation bar hierarchy.
317    ///
318    /// This method reuses existing bars when navigating to related
319    /// levels (ancestors or descendants) to minimize rendering work. New bars are
320    /// created only when necessary, and excess bars are trimmed.
321    ///
322    /// # Algorithm
323    ///
324    /// 1. Trim trailing bars that are no longer ancestors of the selected level
325    /// 2. Prefetch data for all levels from selected up to root (or max_levels)
326    /// 3. Build bar hierarchy bottom-up from leaf to root
327    /// 4. Reuse existing bars when they're still valid
328    /// 5. Position all bars starting from container's min.y
329    /// 6. Update container rect to match total children height
330    ///
331    /// # Arguments
332    ///
333    /// * `selected` - The new selected level key
334    /// * `rq` - Render queue for scheduling redraws
335    /// * `context` - Application context with fonts and other resources
336    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, &current) {
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, &current, &data_by_level) {
372                let Some(data) = data_by_level.get(&current) else {
373                    break;
374                };
375
376                let (height, ok) = self.compute_bar_height(&layout, &current, data, y_max);
377                if !ok {
378                    break;
379                }
380
381                self.insert_bar_and_separator(&layout, &current, height, &mut index, &mut y_max);
382                levels += 1;
383            }
384
385            if levels > self.max_levels || self.provider.is_root(&current, context) {
386                break;
387            }
388
389            let Some(parent) = self.provider.parent(&current) 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    /// Trim trailing children that are no longer ancestors of `selected`.
434    ///
435    /// `children` stores views in alternating order: bar views at even indices and
436    /// separator filler views at odd indices. That means one logical navigation
437    /// "level" corresponds to **two** entries in `children` (bar + separator).
438    ///
439    /// `leftovers` is the number of logical levels that should be removed from the
440    /// end, so we drain `2 * leftovers` entries.
441    ///
442    /// `saturating_sub` ensures we never underflow if `2 * leftovers > children.len()`
443    /// (in that case we simply drain from `0..` and clear the vector).
444    ///
445    // TODO(ogkevin): it might be beneficial to refactor this so that `(bar + separator)` is a single component.
446    #[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(&current, 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(&current, context) {
490                break;
491            }
492
493            let Some(parent) = self.provider.parent(&current) else {
494                break;
495            };
496
497            current = parent;
498        }
499
500        data_by_level
501    }
502
503    /// Returns true if an existing contiguous range of bars can be reused for
504    /// the given `current` level when rebuilding the navigation stack.
505    ///
506    /// Reuse is possible only when both `first` and `last` boundaries are
507    /// present and `current` lies between them in the ancestry chain. Concretely,
508    /// this method returns true when:
509    /// - `provider.is_ancestor(current, last)` (i.e. `current` is an ancestor of `last`)
510    /// - `provider.is_ancestor(first, current)` (i.e. `first` is an ancestor of `current`)
511    ///
512    /// If either `first` or `last` is `None`, reuse is not possible and the
513    /// function returns `false`.
514    #[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    /// Decide whether a bar for `current` should be created while rebuilding the stack.
555    ///
556    /// Rules:
557    /// - If `current` is not the `selected` level, we always insert a bar for it.
558    ///   This ensures ancestor/ancestor-sibling levels remain visible when traversing.
559    /// - If `current` is the `selected` level, we only insert a bar when there is
560    ///   content to show. That is determined by consulting `data_by_level` and
561    ///   calling the provider's `estimate_line_count`; an estimate > 0 indicates
562    ///   the selected level is non-empty and should be represented by a bar.
563    ///
564    /// If `data_by_level` does not contain an entry for `selected`, the function
565    /// conservatively returns `false` (do not insert).
566    #[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    /// Compute the visual height for a bar representing `key` with `data`, and
584    /// indicate whether that bar can be placed without overlapping the container's
585    /// top edge.
586    ///
587    /// Calculation details:
588    /// - The provider's `estimate_line_count` is used to determine how many lines
589    ///   the bar should display. The count is clamped to a minimum of 1.
590    /// - The height formula is:
591    ///   height = count * layout.x_height + (count + 1) * layout.padding / 2
592    ///   which accounts for per-line x-height and vertical padding between/around lines.
593    /// - The returned boolean is `true` when the bar fits between `self.rect.min.y`
594    ///   and `y_max` after reserving space for a separator (layout.thickness). If
595    ///   placing the bar would push it above `self.rect.min.y` the function returns
596    ///   `(height, false)` to signal that the bar cannot be created at the requested
597    ///   position.
598    ///
599    /// Parameters:
600    /// - `layout` : Precomputed layout metrics (x_height, padding, thickness).
601    /// - `key` / `data` : Provider-specific level identifier and data used to estimate lines.
602    /// - `y_max` : The candidate bottom y coordinate (inclusive) where the bar would end.
603    ///
604    /// Returns:
605    /// - `(height, ok)` where `height` is the computed pixel height for the bar and `ok`
606    ///   indicates whether the bar can be placed without exceeding the top bound.
607    #[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    /// Insert a bar and its separator into the children vector at the given insertion
626    /// index, updating the available bottom coordinate (`y_max`) accordingly.
627    ///
628    /// The function ensures the visual ordering is correct (bar immediately above
629    /// its separator) and handles two insertion scenarios:
630    /// - If the current element at `*index` is absent or already a `Filler`, the
631    ///   bar is inserted first followed by the separator so the resulting sequence
632    ///   is: [bar, separator, ...].
633    /// - Otherwise the separator is inserted first and the bar after it so that the
634    ///   separator sits directly at `y_max` and the bar sits immediately above it.
635    ///
636    /// After inserting each element the function subtracts its height from `y_max`
637    /// so the caller can continue inserting further elements above the ones just
638    /// added. Note that `index` itself is not modified to account for the inserted
639    /// children; callers should update it if they need a different insertion anchor.
640    ///
641    /// ```txt
642    ///   layout when a bar and separator are added:
643    ///   +----------------------+  <- top (smaller y)
644    ///   | BAR (newly inserted) |
645    ///   +----------------------+  <- separator immediately below the bar
646    ///   | SEPARATOR (filler)   |
647    ///   +----------------------+  <- bottom (larger y)
648    /// ```
649    ///
650    /// Vector ordering note:
651    /// - Conceptually the visual stack is Bar above Separator (top -> bottom).
652    /// - Depending on insertion order and index arithmetic, the vector indices may
653    ///   be impacted by the insert() semantics (inserting at the same index shifts
654    ///   previously-inserted items to the right). The implementation below follows
655    ///   the established convention used by this container to maintain the
656    ///   alternating bar/filler pattern.
657    #[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(&current, last)
761                        && self.provider.is_ancestor(first, &current)
762                });
763
764            if !reuse_ok {
765                if let Some(data) = data_by_level.get(&current) {
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(&current) 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    /// Shifts the entire navigation bar and all its children by a delta.
784    ///
785    /// This is typically used when repositioning the bar within the parent view.
786    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    /// Shrinks the navigation bar by distributing resize across all bars.
799    ///
800    /// This method proportionally shrinks all bars based on their available space
801    /// (height minus minimum height). Bars that cannot shrink further are left at
802    /// minimum height. If needed, entire bar+separator pairs are removed from the
803    /// top of the stack.
804    ///
805    /// # Arguments
806    ///
807    /// * `delta_y` - Target shrink amount (negative number)
808    /// * `fonts` - Font registry for resize calculations
809    ///
810    /// # Returns
811    ///
812    /// Actual shrink amount achieved (maybe less than requested if minimum heights
813    /// prevent further shrinking)
814    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/// Layout measurements used by StackNavigationBar to compute bar sizes and spacing.
892///
893/// This small value object caches DPI- and font-dependent sizing parameters that
894/// are computed once and reused across layout and resizing logic:
895/// - `thickness`: thickness of the separator between bars (scaled by DPI)
896/// - `min_height`: minimum allowed height for a bar (usually SMALL_BAR_HEIGHT - thickness)
897/// - `x_height`: font x-height used to compute line heights
898/// - `padding`: extra vertical padding inside a bar (derived from min_height and x_height)
899///
900/// Keeping these values together makes it easier to reason about sizing and to
901/// pass a consistent set of layout metrics into functions that need them.
902#[derive(Debug, Clone, Copy)]
903struct Layout {
904    /// Thickness of the separators between bars (in pixels).
905    thickness: i32,
906    /// Minimum height of a bar (in pixels).
907    min_height: i32,
908    /// Font x-height used to compute per-line heights (in pixels).
909    x_height: i32,
910    /// Vertical padding used inside bars (in pixels).
911    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/// Walks up the ancestry chain starting from `last`, looking for the closest
949/// ancestor that is also an ancestor of `selected`.
950///
951/// This utility uses the provided `NavigationProvider` to traverse parent
952/// relationships and to check ancestry. It returns the number of steps taken
953/// from `last` to the matching ancestor along with that ancestor key.
954///
955/// # Arguments
956///
957/// * `provider` - The domain-specific navigation provider used to query parents
958///   and ancestry relationships.
959/// * `last` - The starting key from which to walk upwards.
960/// * `selected` - The key that we want to find an ancestor for.
961///
962/// # Returns
963///
964/// Returns `Some((distance, ancestor_key))` where `distance` is the number of
965/// parent hops from `last` to `ancestor_key`. If no such ancestor is found
966/// (either because the chain terminates or the search exceeds a safety bound),
967/// returns `None`.
968///
969/// # Safety / limits
970///
971/// The search is bounded by a fixed iteration limit (128) to avoid pathological
972/// or cyclic provider implementations causing an infinite loop.
973#[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(&current, selected) {
984            return Some((count, current));
985        }
986
987        let parent = provider.parent(&current)?;
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}