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    /// Returns a mutable reference to the navigation provider.
317    pub fn provider_mut(&mut self) -> &mut P {
318        &mut self.provider
319    }
320
321    /// Updates the selected level and rebuilds the navigation bar hierarchy.
322    ///
323    /// This method reuses existing bars when navigating to related
324    /// levels (ancestors or descendants) to minimize rendering work. New bars are
325    /// created only when necessary, and excess bars are trimmed.
326    ///
327    /// # Algorithm
328    ///
329    /// 1. Trim trailing bars that are no longer ancestors of the selected level
330    /// 2. Prefetch data for all levels from selected up to root (or max_levels)
331    /// 3. Build bar hierarchy bottom-up from leaf to root
332    /// 4. Reuse existing bars when they're still valid
333    /// 5. Position all bars starting from container's min.y
334    /// 6. Update container rect to match total children height
335    ///
336    /// # Arguments
337    ///
338    /// * `selected` - The new selected level key
339    /// * `rq` - Render queue for scheduling redraws
340    /// * `context` - Application context with fonts and other resources
341    pub fn set_selected(
342        &mut self,
343        selected: P::LevelKey,
344        rq: &mut RenderQueue,
345        context: &mut Context,
346    ) {
347        let layout = Layout::new(context);
348
349        let first_key = self.first_bar_key();
350        let mut last_key = self.last_bar_key();
351
352        self.trim_trailing_children(&selected, &mut last_key);
353
354        let data_by_level = self.prefetch_needed_levels(&selected, context);
355        let leaf = self.provider.leaf_for_bar_traversal(&selected, context);
356
357        let mut levels = 1usize;
358        let mut index = self.children.len();
359        let mut y_max = self.vertical_limit;
360
361        let mut current = leaf.clone();
362        loop {
363            if self.can_reuse_existing(&first_key, &last_key, &current) {
364                let db_index = index - 1;
365
366                let (next_index, new_y_max) =
367                    self.reuse_existing_bar_and_separator(index, y_max, layout.thickness);
368
369                if self.children[db_index].rect().min.y < self.rect.min.y {
370                    break;
371                }
372
373                index = next_index;
374                y_max = new_y_max;
375                levels += 1;
376            } else if self.should_insert_bar(&selected, &current, &data_by_level) {
377                let Some(data) = data_by_level.get(&current) else {
378                    break;
379                };
380
381                let (height, ok) = self.compute_bar_height(&layout, &current, data, y_max);
382                if !ok {
383                    break;
384                }
385
386                self.insert_bar_and_separator(&layout, &current, height, &mut index, &mut y_max);
387                levels += 1;
388            }
389
390            if levels > self.max_levels || self.provider.is_root(&current, context) {
391                break;
392            }
393
394            let Some(parent) = self.provider.parent(&current) else {
395                break;
396            };
397
398            current = parent;
399        }
400
401        self.children.drain(..index);
402
403        self.ensure_minimum_bar(&layout, &selected);
404        self.remove_extra_leading_separator();
405
406        self.position_and_populate_children(
407            &selected,
408            &leaf,
409            &data_by_level,
410            &first_key,
411            &last_key,
412            rq,
413            &mut context.fonts,
414        );
415
416        self.rect.max.y = self.children[self.children.len() - 1].rect().max.y;
417        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Partial));
418
419        self.selected = selected;
420    }
421
422    #[inline]
423    fn first_bar_key(&self) -> Option<P::LevelKey> {
424        self.children
425            .first()
426            .and_then(|child| child.downcast_ref::<P::Bar>())
427            .map(|bar| self.provider.bar_key(bar))
428    }
429
430    #[inline]
431    fn last_bar_key(&self) -> Option<P::LevelKey> {
432        self.children
433            .last()
434            .and_then(|child| child.downcast_ref::<P::Bar>())
435            .map(|bar| self.provider.bar_key(bar))
436    }
437
438    /// Trim trailing children that are no longer ancestors of `selected`.
439    ///
440    /// `children` stores views in alternating order: bar views at even indices and
441    /// separator filler views at odd indices. That means one logical navigation
442    /// "level" corresponds to **two** entries in `children` (bar + separator).
443    ///
444    /// `leftovers` is the number of logical levels that should be removed from the
445    /// end, so we drain `2 * leftovers` entries.
446    ///
447    /// `saturating_sub` ensures we never underflow if `2 * leftovers > children.len()`
448    /// (in that case we simply drain from `0..` and clear the vector).
449    ///
450    // TODO(ogkevin): it might be beneficial to refactor this so that `(bar + separator)` is a single component.
451    #[inline]
452    fn trim_trailing_children(
453        &mut self,
454        selected: &P::LevelKey,
455        last_key: &mut Option<P::LevelKey>,
456    ) {
457        let Some(last) = last_key.clone() else {
458            return;
459        };
460
461        let Some((leftovers, ancestor)) =
462            find_closest_ancestor_by_provider(&self.provider, &last, selected)
463        else {
464            return;
465        };
466
467        if leftovers == 0 {
468            return;
469        }
470
471        self.children
472            .drain(self.children.len().saturating_sub(2 * leftovers)..);
473        *last_key = Some(ancestor);
474    }
475
476    #[inline]
477    fn prefetch_needed_levels(
478        &self,
479        selected: &P::LevelKey,
480        context: &mut Context,
481    ) -> BTreeMap<P::LevelKey, P::LevelData> {
482        let leaf_key = self.provider.selected_leaf_key(selected);
483        let mut data_by_level = BTreeMap::new();
484        let mut current = leaf_key.clone();
485
486        loop {
487            let data = self.provider.fetch_level_data(&current, context);
488            data_by_level.insert(current.clone(), data);
489
490            if data_by_level.len() >= self.max_levels {
491                break;
492            }
493
494            if self.provider.is_root(&current, context) {
495                break;
496            }
497
498            let Some(parent) = self.provider.parent(&current) else {
499                break;
500            };
501
502            current = parent;
503        }
504
505        data_by_level
506    }
507
508    /// Returns true if an existing contiguous range of bars can be reused for
509    /// the given `current` level when rebuilding the navigation stack.
510    ///
511    /// Reuse is possible only when both `first` and `last` boundaries are
512    /// present and `current` lies between them in the ancestry chain. Concretely,
513    /// this method returns true when:
514    /// - `provider.is_ancestor(current, last)` (i.e. `current` is an ancestor of `last`)
515    /// - `provider.is_ancestor(first, current)` (i.e. `first` is an ancestor of `current`)
516    ///
517    /// If either `first` or `last` is `None`, reuse is not possible and the
518    /// function returns `false`.
519    #[inline]
520    fn can_reuse_existing(
521        &self,
522        first: &Option<P::LevelKey>,
523        last: &Option<P::LevelKey>,
524        current: &P::LevelKey,
525    ) -> bool {
526        let (Some(first), Some(last)) = (first.as_ref(), last.as_ref()) else {
527            return false;
528        };
529
530        self.provider.is_ancestor(current, last) && self.provider.is_ancestor(first, current)
531    }
532
533    #[inline]
534    fn reuse_existing_bar_and_separator(
535        &mut self,
536        index: usize,
537        y_max: i32,
538        thickness: i32,
539    ) -> (usize, i32) {
540        let db_index = index - 1;
541        let sep_index = index.saturating_sub(2);
542
543        let y_shift = y_max - self.children[db_index].rect().max.y;
544        if let Some(bar) = self.children[db_index].downcast_mut::<P::Bar>() {
545            self.provider.shift_bar(bar, pt!(0, y_shift));
546        }
547
548        let mut next_y_max = y_max - self.children[db_index].rect().height() as i32;
549
550        if sep_index != db_index {
551            let y_shift = next_y_max - self.children[sep_index].rect().max.y;
552            *self.children[sep_index].rect_mut() += pt!(0, y_shift);
553            next_y_max -= thickness;
554        }
555
556        (sep_index, next_y_max)
557    }
558
559    /// Decide whether a bar for `current` should be created while rebuilding the stack.
560    ///
561    /// Rules:
562    /// - If `current` is not the `selected` level, we always insert a bar for it.
563    ///   This ensures ancestor/ancestor-sibling levels remain visible when traversing.
564    /// - If `current` is the `selected` level, we only insert a bar when there is
565    ///   content to show. That is determined by consulting `data_by_level` and
566    ///   calling the provider's `estimate_line_count`; an estimate > 0 indicates
567    ///   the selected level is non-empty and should be represented by a bar.
568    ///
569    /// If `data_by_level` does not contain an entry for `selected`, the function
570    /// conservatively returns `false` (do not insert).
571    #[inline]
572    fn should_insert_bar(
573        &self,
574        selected: &P::LevelKey,
575        current: &P::LevelKey,
576        data_by_level: &BTreeMap<P::LevelKey, P::LevelData>,
577    ) -> bool {
578        if current != selected {
579            return true;
580        }
581
582        data_by_level
583            .get(selected)
584            .map(|data| self.provider.estimate_line_count(selected, data) > 0)
585            .unwrap_or(false)
586    }
587
588    /// Compute the visual height for a bar representing `key` with `data`, and
589    /// indicate whether that bar can be placed without overlapping the container's
590    /// top edge.
591    ///
592    /// Calculation details:
593    /// - The provider's `estimate_line_count` is used to determine how many lines
594    ///   the bar should display. The count is clamped to a minimum of 1.
595    /// - The height formula is:
596    ///   height = count * layout.x_height + (count + 1) * layout.padding / 2
597    ///   which accounts for per-line x-height and vertical padding between/around lines.
598    /// - The returned boolean is `true` when the bar fits between `self.rect.min.y`
599    ///   and `y_max` after reserving space for a separator (layout.thickness). If
600    ///   placing the bar would push it above `self.rect.min.y` the function returns
601    ///   `(height, false)` to signal that the bar cannot be created at the requested
602    ///   position.
603    ///
604    /// Parameters:
605    /// - `layout` : Precomputed layout metrics (x_height, padding, thickness).
606    /// - `key` / `data` : Provider-specific level identifier and data used to estimate lines.
607    /// - `y_max` : The candidate bottom y coordinate (inclusive) where the bar would end.
608    ///
609    /// Returns:
610    /// - `(height, ok)` where `height` is the computed pixel height for the bar and `ok`
611    ///   indicates whether the bar can be placed without exceeding the top bound.
612    #[inline]
613    fn compute_bar_height(
614        &self,
615        layout: &Layout,
616        key: &P::LevelKey,
617        data: &P::LevelData,
618        y_max: i32,
619    ) -> (i32, bool) {
620        let count = self.provider.estimate_line_count(key, data).max(1) as i32;
621        let height = count * layout.x_height + (count + 1) * layout.padding / 2;
622
623        if y_max - height - layout.thickness < self.rect.min.y {
624            return (height, false);
625        }
626
627        (height, true)
628    }
629
630    /// Insert a bar and its separator into the children vector at the given insertion
631    /// index, updating the available bottom coordinate (`y_max`) accordingly.
632    ///
633    /// The function ensures the visual ordering is correct (bar immediately above
634    /// its separator) and handles two insertion scenarios:
635    /// - If the current element at `*index` is absent or already a `Filler`, the
636    ///   bar is inserted first followed by the separator so the resulting sequence
637    ///   is: [bar, separator, ...].
638    /// - Otherwise the separator is inserted first and the bar after it so that the
639    ///   separator sits directly at `y_max` and the bar sits immediately above it.
640    ///
641    /// After inserting each element the function subtracts its height from `y_max`
642    /// so the caller can continue inserting further elements above the ones just
643    /// added. Note that `index` itself is not modified to account for the inserted
644    /// children; callers should update it if they need a different insertion anchor.
645    ///
646    /// ```txt
647    ///   layout when a bar and separator are added:
648    ///   +----------------------+  <- top (smaller y)
649    ///   | BAR (newly inserted) |
650    ///   +----------------------+  <- separator immediately below the bar
651    ///   | SEPARATOR (filler)   |
652    ///   +----------------------+  <- bottom (larger y)
653    /// ```
654    ///
655    /// Vector ordering note:
656    /// - Conceptually the visual stack is Bar above Separator (top -> bottom).
657    /// - Depending on insertion order and index arithmetic, the vector indices may
658    ///   be impacted by the insert() semantics (inserting at the same index shifts
659    ///   previously-inserted items to the right). The implementation below follows
660    ///   the established convention used by this container to maintain the
661    ///   alternating bar/filler pattern.
662    #[inline]
663    fn insert_bar_and_separator(
664        &mut self,
665        layout: &Layout,
666        key: &P::LevelKey,
667        height: i32,
668        index: &mut usize,
669        y_max: &mut i32,
670    ) {
671        if self
672            .children
673            .get(*index)
674            .is_none_or(|child| child.is::<Filler>())
675        {
676            let rect = rect![self.rect.min.x, *y_max - height, self.rect.max.x, *y_max];
677            self.children
678                .insert(*index, Box::new(self.provider.create_bar(rect, key)));
679            *y_max -= height;
680
681            let sep_rect = rect![
682                self.rect.min.x,
683                *y_max - layout.thickness,
684                self.rect.max.x,
685                *y_max
686            ];
687            self.children
688                .insert(*index, Box::new(Filler::new(sep_rect, SEPARATOR_NORMAL)));
689            *y_max -= layout.thickness;
690
691            return;
692        }
693
694        let sep_rect = rect![
695            self.rect.min.x,
696            *y_max - layout.thickness,
697            self.rect.max.x,
698            *y_max
699        ];
700        self.children
701            .insert(*index, Box::new(Filler::new(sep_rect, SEPARATOR_NORMAL)));
702        *y_max -= layout.thickness;
703
704        let rect = rect![self.rect.min.x, *y_max - height, self.rect.max.x, *y_max];
705        self.children
706            .insert(*index, Box::new(self.provider.create_bar(rect, key)));
707        *y_max -= height;
708    }
709
710    #[inline]
711    fn ensure_minimum_bar(&mut self, layout: &Layout, selected: &P::LevelKey) {
712        if !self.children.is_empty() {
713            return;
714        }
715
716        let rect = rect![
717            self.rect.min.x,
718            self.rect.min.y,
719            self.rect.max.x,
720            self.rect.min.y + layout.min_height
721        ];
722
723        self.children
724            .push(Box::new(self.provider.create_bar(rect, selected)));
725    }
726
727    #[inline]
728    fn remove_extra_leading_separator(&mut self) {
729        if self.children.len().is_multiple_of(2) {
730            self.children.remove(0);
731        }
732    }
733
734    #[inline]
735    #[allow(clippy::too_many_arguments)]
736    fn position_and_populate_children(
737        &mut self,
738        selected: &P::LevelKey,
739        leaf: &P::LevelKey,
740        data_by_level: &BTreeMap<P::LevelKey, P::LevelData>,
741        first: &Option<P::LevelKey>,
742        last: &Option<P::LevelKey>,
743        rq: &mut RenderQueue,
744        fonts: &mut Fonts,
745    ) {
746        let mut current = leaf.clone();
747        let y_shift = self.rect.min.y - self.children[0].rect().min.y;
748
749        let mut index = self.children.len();
750        while index > 0 {
751            index -= 1;
752
753            if self.children[index].is::<Filler>() {
754                *self.children[index].rect_mut() += pt!(0, y_shift);
755                continue;
756            }
757
758            let bar = self.children[index].downcast_mut::<P::Bar>().unwrap();
759            self.provider.shift_bar(bar, pt!(0, y_shift));
760
761            let reuse_ok = first
762                .as_ref()
763                .zip(last.as_ref())
764                .is_some_and(|(first, last)| {
765                    self.provider.is_ancestor(&current, last)
766                        && self.provider.is_ancestor(first, &current)
767                });
768
769            if !reuse_ok {
770                if let Some(data) = data_by_level.get(&current) {
771                    self.provider.update_bar(bar, data, selected, fonts);
772                }
773            } else if last.as_ref().is_some_and(|last| *last == current) {
774                self.provider.update_bar_selection(bar, selected);
775            }
776
777            let Some(parent) = self.provider.parent(&current) else {
778                break;
779            };
780
781            current = parent;
782        }
783
784        self.rect.max.y = self.children[self.children.len() - 1].rect().max.y;
785        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Partial));
786    }
787
788    /// Shifts the entire navigation bar and all its children by a delta.
789    ///
790    /// This is typically used when repositioning the bar within the parent view.
791    pub fn shift(&mut self, delta: Point) {
792        for child in &mut self.children {
793            if let Some(bar) = child.downcast_mut::<P::Bar>() {
794                self.provider.shift_bar(bar, delta);
795            } else {
796                *child.rect_mut() += delta;
797            }
798        }
799
800        self.rect += delta;
801    }
802
803    /// Shrinks the navigation bar by distributing resize across all bars.
804    ///
805    /// This method proportionally shrinks all bars based on their available space
806    /// (height minus minimum height). Bars that cannot shrink further are left at
807    /// minimum height. If needed, entire bar+separator pairs are removed from the
808    /// top of the stack.
809    ///
810    /// # Arguments
811    ///
812    /// * `delta_y` - Target shrink amount (negative number)
813    /// * `fonts` - Font registry for resize calculations
814    ///
815    /// # Returns
816    ///
817    /// Actual shrink amount achieved (maybe less than requested if minimum heights
818    /// prevent further shrinking)
819    pub fn shrink(&mut self, delta_y: i32, fonts: &mut Fonts) -> i32 {
820        let layout = Layout::new_for_fonts(fonts);
821        let bars_count = self.children.len().div_ceil(2);
822        let mut values = vec![0; bars_count];
823
824        for (i, value) in values.iter_mut().enumerate().take(bars_count) {
825            *value = self.children[2 * i].rect().height() as i32 - layout.min_height;
826        }
827
828        let sum: i32 = values.iter().sum();
829        let mut y_shift = 0;
830
831        if sum > 0 {
832            for i in (0..bars_count).rev() {
833                let local_delta_y = ((values[i] as f32 / sum as f32) * delta_y as f32) as i32;
834                y_shift += self.resize_child(2 * i, local_delta_y, fonts);
835                if y_shift <= delta_y {
836                    break;
837                }
838            }
839        }
840
841        while self.children.len() > 1 && y_shift > delta_y {
842            let mut dy = 0;
843            for child in self.children.drain(0..2) {
844                dy += child.rect().height() as i32;
845            }
846
847            for child in &mut self.children {
848                if let Some(bar) = child.downcast_mut::<P::Bar>() {
849                    self.provider.shift_bar(bar, pt!(0, -dy));
850                } else {
851                    *child.rect_mut() += pt!(0, -dy);
852                }
853            }
854
855            y_shift -= dy;
856        }
857
858        self.rect.max.y = self.children[self.children.len() - 1].rect().max.y;
859
860        y_shift
861    }
862
863    #[inline]
864    fn resize_child(&mut self, child_index: usize, delta_y: i32, fonts: &mut Fonts) -> i32 {
865        let layout = Layout::new_for_fonts(fonts);
866        let rect = *self.children[child_index].rect();
867
868        let delta_y_max = (self.vertical_limit - self.rect.max.y).max(0);
869        let y_max = (rect.max.y + delta_y.min(delta_y_max)).max(rect.min.y + layout.min_height);
870
871        let height = y_max - rect.min.y;
872
873        let count = ((height - layout.padding / 2) / (layout.x_height + layout.padding / 2)).max(1);
874        let height = count * layout.x_height + (count + 1) * layout.padding / 2;
875        let y_max = rect.min.y + height;
876
877        let y_shift = y_max - rect.max.y;
878
879        let bar = self.children[child_index].downcast_mut::<P::Bar>().unwrap();
880        let resized = self.provider.resize_bar_by(bar, y_shift, fonts);
881
882        for i in child_index + 1..self.children.len() {
883            if let Some(bar) = self.children[i].downcast_mut::<P::Bar>() {
884                self.provider.shift_bar(bar, pt!(0, resized));
885            } else {
886                *self.children[i].rect_mut() += pt!(0, resized);
887            }
888        }
889
890        self.rect.max.y = self.children[self.children.len() - 1].rect().max.y;
891
892        resized
893    }
894}
895
896/// Layout measurements used by StackNavigationBar to compute bar sizes and spacing.
897///
898/// This small value object caches DPI- and font-dependent sizing parameters that
899/// are computed once and reused across layout and resizing logic:
900/// - `thickness`: thickness of the separator between bars (scaled by DPI)
901/// - `min_height`: minimum allowed height for a bar (usually SMALL_BAR_HEIGHT - thickness)
902/// - `x_height`: font x-height used to compute line heights
903/// - `padding`: extra vertical padding inside a bar (derived from min_height and x_height)
904///
905/// Keeping these values together makes it easier to reason about sizing and to
906/// pass a consistent set of layout metrics into functions that need them.
907#[derive(Debug, Clone, Copy)]
908struct Layout {
909    /// Thickness of the separators between bars (in pixels).
910    thickness: i32,
911    /// Minimum height of a bar (in pixels).
912    min_height: i32,
913    /// Font x-height used to compute per-line heights (in pixels).
914    x_height: i32,
915    /// Vertical padding used inside bars (in pixels).
916    padding: i32,
917}
918
919impl Layout {
920    fn new(context: &mut Context) -> Self {
921        let dpi = CURRENT_DEVICE.dpi;
922        let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
923        let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
924        let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
925        let x_height = font.x_heights.0 as i32;
926        let padding = min_height - x_height;
927
928        Self {
929            thickness,
930            min_height,
931            x_height,
932            padding,
933        }
934    }
935
936    fn new_for_fonts(fonts: &mut Fonts) -> Self {
937        let dpi = CURRENT_DEVICE.dpi;
938        let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
939        let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
940        let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
941        let x_height = font.x_heights.0 as i32;
942        let padding = min_height - x_height;
943
944        Self {
945            thickness,
946            min_height,
947            x_height,
948            padding,
949        }
950    }
951}
952
953/// Walks up the ancestry chain starting from `last`, looking for the closest
954/// ancestor that is also an ancestor of `selected`.
955///
956/// This utility uses the provided `NavigationProvider` to traverse parent
957/// relationships and to check ancestry. It returns the number of steps taken
958/// from `last` to the matching ancestor along with that ancestor key.
959///
960/// # Arguments
961///
962/// * `provider` - The domain-specific navigation provider used to query parents
963///   and ancestry relationships.
964/// * `last` - The starting key from which to walk upwards.
965/// * `selected` - The key that we want to find an ancestor for.
966///
967/// # Returns
968///
969/// Returns `Some((distance, ancestor_key))` where `distance` is the number of
970/// parent hops from `last` to `ancestor_key`. If no such ancestor is found
971/// (either because the chain terminates or the search exceeds a safety bound),
972/// returns `None`.
973///
974/// # Safety / limits
975///
976/// The search is bounded by a fixed iteration limit (128) to avoid pathological
977/// or cyclic provider implementations causing an infinite loop.
978#[inline]
979fn find_closest_ancestor_by_provider<P: NavigationProvider>(
980    provider: &P,
981    last: &P::LevelKey,
982    selected: &P::LevelKey,
983) -> Option<(usize, P::LevelKey)> {
984    let mut count = 0usize;
985    let mut current = last.clone();
986
987    while count < 128 {
988        if provider.is_ancestor(&current, selected) {
989            return Some((count, current));
990        }
991
992        let parent = provider.parent(&current)?;
993
994        current = parent;
995        count += 1;
996    }
997
998    None
999}
1000
1001impl<P: NavigationProvider + 'static> View for StackNavigationBar<P> {
1002    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, _rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
1003    fn handle_event(
1004        &mut self,
1005        evt: &Event,
1006        _hub: &Hub,
1007        bus: &mut Bus,
1008        _rq: &mut RenderQueue,
1009        context: &mut Context,
1010    ) -> bool {
1011        match *evt {
1012            Event::Gesture(crate::gesture::GestureEvent::Swipe {
1013                dir, start, end, ..
1014            }) if self.enable_resize && (self.rect.includes(start) || self.rect.includes(end)) => {
1015                match dir {
1016                    Dir::North | Dir::South => {
1017                        let pt = if dir == Dir::North { end } else { start };
1018
1019                        let bar_index = (0..self.children.len())
1020                            .step_by(2)
1021                            .find(|&index| self.children[index].rect().includes(pt));
1022
1023                        if let Some(index) = bar_index {
1024                            let delta_y = end.y - start.y;
1025                            let resized = self.resize_child(index, delta_y, &mut context.fonts);
1026                            bus.push_back(Event::NavigationBarResized(resized));
1027                        }
1028
1029                        true
1030                    }
1031                    _ => false,
1032                }
1033            }
1034            _ => false,
1035        }
1036    }
1037
1038    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
1039    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
1040
1041    fn rect(&self) -> &Rectangle {
1042        &self.rect
1043    }
1044
1045    fn rect_mut(&mut self) -> &mut Rectangle {
1046        &mut self.rect
1047    }
1048
1049    fn children(&self) -> &Vec<Box<dyn View>> {
1050        &self.children
1051    }
1052
1053    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
1054        &mut self.children
1055    }
1056
1057    fn id(&self) -> Id {
1058        self.id
1059    }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065    use crate::context::test_helpers::create_test_context;
1066
1067    #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1068    struct Key(i32);
1069
1070    struct Provider;
1071
1072    impl NavigationProvider for Provider {
1073        type LevelKey = Key;
1074        type LevelData = usize;
1075        type Bar = Filler;
1076
1077        fn parent(&self, current: &Self::LevelKey) -> Option<Self::LevelKey> {
1078            if current.0 == 0 {
1079                return None;
1080            }
1081
1082            Some(Key(current.0 - 1))
1083        }
1084
1085        fn is_ancestor(&self, ancestor: &Self::LevelKey, descendant: &Self::LevelKey) -> bool {
1086            ancestor.0 <= descendant.0
1087        }
1088
1089        fn is_root(&self, key: &Self::LevelKey, _context: &Context) -> bool {
1090            key.0 == 0
1091        }
1092
1093        fn fetch_level_data(
1094            &self,
1095            key: &Self::LevelKey,
1096            _context: &mut Context,
1097        ) -> Self::LevelData {
1098            key.0 as usize
1099        }
1100
1101        fn estimate_line_count(&self, _key: &Self::LevelKey, data: &Self::LevelData) -> usize {
1102            *data
1103        }
1104
1105        fn create_bar(&self, rect: Rectangle, _key: &Self::LevelKey) -> Self::Bar {
1106            Filler::new(rect, SEPARATOR_NORMAL)
1107        }
1108
1109        fn bar_key(&self, _bar: &Self::Bar) -> Self::LevelKey {
1110            Key(0)
1111        }
1112
1113        fn update_bar(
1114            &self,
1115            _bar: &mut Self::Bar,
1116            _data: &Self::LevelData,
1117            _selected: &Self::LevelKey,
1118            _fonts: &mut Fonts,
1119        ) {
1120        }
1121
1122        fn update_bar_selection(&self, _bar: &mut Self::Bar, _selected: &Self::LevelKey) {}
1123
1124        fn resize_bar_by(&self, bar: &mut Self::Bar, delta_y: i32, _fonts: &mut Fonts) -> i32 {
1125            let rect = *bar.rect();
1126            let dpi = CURRENT_DEVICE.dpi;
1127            let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
1128            let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
1129
1130            let y_max = (rect.max.y + delta_y).max(rect.min.y + min_height);
1131            let resized = y_max - rect.max.y;
1132
1133            bar.rect_mut().max.y = y_max;
1134
1135            resized
1136        }
1137
1138        fn shift_bar(&self, bar: &mut Self::Bar, delta: Point) {
1139            *bar.rect_mut() += delta;
1140        }
1141    }
1142
1143    #[test]
1144    fn closest_ancestor_count_is_distance() {
1145        let provider = Provider;
1146        let last = Key(5);
1147        let selected = Key(3);
1148
1149        let (count, ancestor) =
1150            find_closest_ancestor_by_provider(&provider, &last, &selected).unwrap();
1151        assert_eq!(count, 2);
1152        assert_eq!(ancestor, Key(3));
1153    }
1154
1155    #[test]
1156    fn closest_ancestor_is_none_when_unrelated() {
1157        let provider = Provider;
1158        let last = Key(5);
1159        let selected = Key(-1);
1160
1161        assert!(find_closest_ancestor_by_provider(&provider, &last, &selected).is_none());
1162    }
1163
1164    #[test]
1165    fn set_selected_with_single_child_no_panic() {
1166        let mut context = create_test_context();
1167
1168        let provider = Provider;
1169        let rect = rect![0, 0, 600, 100];
1170        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1171        let mut rq = RenderQueue::new();
1172
1173        nav_bar.set_selected(Key(0), &mut rq, &mut context);
1174        assert!(!nav_bar.children.is_empty());
1175
1176        nav_bar.set_selected(Key(1), &mut rq, &mut context);
1177        assert!(!nav_bar.children.is_empty());
1178    }
1179
1180    #[test]
1181    fn set_selected_from_empty_state() {
1182        let mut context = create_test_context();
1183
1184        let provider = Provider;
1185        let rect = rect![0, 0, 600, 100];
1186        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1187        let mut rq = RenderQueue::new();
1188
1189        assert!(nav_bar.children.is_empty());
1190
1191        nav_bar.set_selected(Key(3), &mut rq, &mut context);
1192
1193        assert!(!nav_bar.children.is_empty());
1194        assert_eq!(nav_bar.selected, Key(3));
1195    }
1196
1197    #[test]
1198    fn set_selected_reuses_existing_bars() {
1199        let mut context = create_test_context();
1200
1201        let provider = Provider;
1202        let rect = rect![0, 0, 600, 200];
1203        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1204        let mut rq = RenderQueue::new();
1205
1206        nav_bar.set_selected(Key(2), &mut rq, &mut context);
1207        assert!(!nav_bar.children.is_empty());
1208
1209        nav_bar.set_selected(Key(3), &mut rq, &mut context);
1210
1211        assert!(!nav_bar.children.is_empty());
1212        assert_eq!(nav_bar.selected, Key(3));
1213    }
1214
1215    #[test]
1216    fn set_selected_to_parent_reduces_bars() {
1217        let mut context = create_test_context();
1218
1219        let provider = Provider;
1220        let rect = rect![0, 0, 600, 200];
1221        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1222        let mut rq = RenderQueue::new();
1223
1224        nav_bar.set_selected(Key(5), &mut rq, &mut context);
1225        assert!(!nav_bar.children.is_empty());
1226
1227        nav_bar.set_selected(Key(2), &mut rq, &mut context);
1228
1229        assert!(!nav_bar.children.is_empty());
1230        assert_eq!(nav_bar.selected, Key(2));
1231    }
1232
1233    #[test]
1234    fn set_selected_handles_max_levels() {
1235        let mut context = create_test_context();
1236
1237        let provider = Provider;
1238        let rect = rect![0, 0, 600, 200];
1239        let max_levels = 3;
1240        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, max_levels, provider, Key(0));
1241        let mut rq = RenderQueue::new();
1242
1243        nav_bar.set_selected(Key(10), &mut rq, &mut context);
1244
1245        assert!(!nav_bar.children.is_empty());
1246    }
1247
1248    #[test]
1249    fn resize_child_with_aggressive_north_swipe_maintains_minimum_height() {
1250        let mut context = create_test_context();
1251
1252        let provider = Provider;
1253        let rect = rect![0, 68, 600, 590];
1254        let vertical_limit = 642;
1255        let mut nav_bar = StackNavigationBar::new(rect, vertical_limit, 1, provider, Key(0));
1256        let mut rq = RenderQueue::new();
1257
1258        nav_bar.set_selected(Key(0), &mut rq, &mut context);
1259        assert_eq!(nav_bar.children.len(), 1);
1260
1261        let initial_rect = *nav_bar.children[0].rect();
1262        let initial_height = initial_rect.height() as i32;
1263
1264        let aggressive_delta_y = -(initial_height * 2);
1265        nav_bar.resize_child(0, aggressive_delta_y, &mut context.fonts);
1266
1267        let dpi = CURRENT_DEVICE.dpi;
1268        let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
1269        let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
1270
1271        let final_child_rect = *nav_bar.children[0].rect();
1272        let final_height = final_child_rect.height() as i32;
1273
1274        assert!(
1275            final_height >= min_height,
1276            "Child bar height {} should be at least min_height {}",
1277            final_height,
1278            min_height
1279        );
1280
1281        let container_height = nav_bar.rect.max.y - nav_bar.rect.min.y;
1282        assert!(
1283            container_height >= min_height,
1284            "Container height {} should be at least min_height {}. Container rect: {:?}",
1285            container_height,
1286            min_height,
1287            nav_bar.rect
1288        );
1289
1290        assert_eq!(
1291            nav_bar.rect.max.y, final_child_rect.max.y,
1292            "Container max.y should match last child's max.y"
1293        );
1294    }
1295
1296    #[test]
1297    fn shrink_proportionally_distributes_across_multiple_bars() {
1298        let mut context = create_test_context();
1299
1300        let provider = Provider;
1301        let rect = rect![0, 0, 600, 400];
1302        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1303        let mut rq = RenderQueue::new();
1304
1305        nav_bar.set_selected(Key(3), &mut rq, &mut context);
1306
1307        let initial_heights: Vec<i32> = (0..nav_bar.children.len())
1308            .step_by(2)
1309            .map(|i| nav_bar.children[i].rect().height() as i32)
1310            .collect();
1311
1312        let shrink_amount = -50;
1313        let actual_shrink = nav_bar.shrink(shrink_amount, &mut context.fonts);
1314
1315        let final_heights: Vec<i32> = (0..nav_bar.children.len())
1316            .step_by(2)
1317            .map(|i| nav_bar.children[i].rect().height() as i32)
1318            .collect();
1319
1320        assert!(actual_shrink <= 0, "Should return negative shrink amount");
1321        assert!(
1322            actual_shrink <= shrink_amount,
1323            "Actual shrink should be at most the requested amount (more negative = more shrink)"
1324        );
1325
1326        for (initial, final_h) in initial_heights.iter().zip(final_heights.iter()) {
1327            assert!(
1328                final_h <= initial,
1329                "Each bar should shrink or stay same: initial={}, final={}",
1330                initial,
1331                final_h
1332            );
1333        }
1334    }
1335
1336    #[test]
1337    fn shrink_removes_bars_when_exceeding_available_space() {
1338        let mut context = create_test_context();
1339
1340        let provider = Provider;
1341        let rect = rect![0, 0, 600, 300];
1342        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1343        let mut rq = RenderQueue::new();
1344
1345        nav_bar.set_selected(Key(3), &mut rq, &mut context);
1346
1347        let initial_bar_count = nav_bar.children.len().div_ceil(2);
1348
1349        let aggressive_shrink = -500;
1350        nav_bar.shrink(aggressive_shrink, &mut context.fonts);
1351
1352        let final_bar_count = nav_bar.children.len().div_ceil(2);
1353
1354        assert!(
1355            final_bar_count <= initial_bar_count,
1356            "Bar count should decrease or stay same when shrinking aggressively"
1357        );
1358        assert!(final_bar_count >= 1, "Should always keep at least one bar");
1359    }
1360
1361    #[test]
1362    fn shrink_handles_all_bars_at_minimum_height() {
1363        let mut context = create_test_context();
1364
1365        let provider = Provider;
1366        let rect = rect![0, 0, 600, 100];
1367        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 2, provider, Key(0));
1368        let mut rq = RenderQueue::new();
1369
1370        nav_bar.set_selected(Key(1), &mut rq, &mut context);
1371
1372        let dpi = CURRENT_DEVICE.dpi;
1373        let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
1374        let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
1375
1376        for i in (0..nav_bar.children.len()).step_by(2) {
1377            let bar = nav_bar.children[i].downcast_mut::<Filler>().unwrap();
1378            bar.rect_mut().max.y = bar.rect().min.y + min_height;
1379        }
1380
1381        let shrink_amount = -20;
1382        let actual_shrink = nav_bar.shrink(shrink_amount, &mut context.fonts);
1383
1384        assert!(
1385            actual_shrink <= 0,
1386            "When all bars at minimum, shrink should remove bars or do nothing"
1387        );
1388    }
1389
1390    #[test]
1391    fn resize_child_expansion_respects_vertical_limit() {
1392        let mut context = create_test_context();
1393
1394        let provider = Provider;
1395        let rect = rect![0, 0, 600, 200];
1396        let vertical_limit = 250;
1397        let mut nav_bar = StackNavigationBar::new(rect, vertical_limit, 3, provider, Key(0));
1398        let mut rq = RenderQueue::new();
1399
1400        nav_bar.set_selected(Key(2), &mut rq, &mut context);
1401
1402        let last_bar_index = ((nav_bar.children.len() - 1) / 2) * 2;
1403        let initial_container_max = nav_bar.rect.max.y;
1404
1405        let large_expansion = 200;
1406        let actual_resize =
1407            nav_bar.resize_child(last_bar_index, large_expansion, &mut context.fonts);
1408
1409        let final_container_max = nav_bar.rect.max.y;
1410        let expected_max = (initial_container_max + actual_resize).min(vertical_limit);
1411
1412        assert!(
1413            final_container_max <= vertical_limit,
1414            "Navigation bar should not exceed vertical_limit: {} > {}",
1415            final_container_max,
1416            vertical_limit
1417        );
1418
1419        assert_eq!(
1420            final_container_max, expected_max,
1421            "Container should expand by actual_resize amount or hit vertical_limit"
1422        );
1423    }
1424
1425    #[test]
1426    fn resize_child_expansion_shifts_subsequent_children() {
1427        let mut context = create_test_context();
1428
1429        let provider = Provider;
1430        let rect = rect![0, 0, 600, 300];
1431        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 5, provider, Key(0));
1432        let mut rq = RenderQueue::new();
1433
1434        nav_bar.set_selected(Key(3), &mut rq, &mut context);
1435
1436        if nav_bar.children.len() < 4 {
1437            return;
1438        }
1439
1440        let target_index = 0;
1441        let initial_rects: Vec<Rectangle> = nav_bar
1442            .children
1443            .iter()
1444            .skip(target_index + 1)
1445            .map(|child| *child.rect())
1446            .collect();
1447
1448        let expansion = 20;
1449        let actual_resize = nav_bar.resize_child(target_index, expansion, &mut context.fonts);
1450
1451        let final_rects: Vec<Rectangle> = nav_bar
1452            .children
1453            .iter()
1454            .skip(target_index + 1)
1455            .map(|child| *child.rect())
1456            .collect();
1457
1458        for (initial, final_rect) in initial_rects.iter().zip(final_rects.iter()) {
1459            let shift = final_rect.min.y - initial.min.y;
1460            assert_eq!(
1461                shift, actual_resize,
1462                "All subsequent children should shift by the actual resize amount"
1463            );
1464        }
1465    }
1466
1467    #[test]
1468    fn shift_moves_all_children_and_container() {
1469        let mut context = create_test_context();
1470
1471        let provider = Provider;
1472        let rect = rect![0, 0, 600, 200];
1473        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1474        let mut rq = RenderQueue::new();
1475
1476        nav_bar.set_selected(Key(2), &mut rq, &mut context);
1477
1478        let initial_container = nav_bar.rect;
1479        let initial_child_rects: Vec<Rectangle> =
1480            nav_bar.children.iter().map(|c| *c.rect()).collect();
1481
1482        let delta = pt!(10, 20);
1483        nav_bar.shift(delta);
1484
1485        assert_eq!(
1486            nav_bar.rect,
1487            initial_container + delta,
1488            "Container should shift by delta"
1489        );
1490
1491        for (i, initial_rect) in initial_child_rects.iter().enumerate() {
1492            let expected = *initial_rect + delta;
1493            assert_eq!(
1494                *nav_bar.children[i].rect(),
1495                expected,
1496                "Child {} should shift by delta",
1497                i
1498            );
1499        }
1500    }
1501
1502    #[test]
1503    fn handle_event_north_swipe_resizes_bar() {
1504        use crate::gesture::GestureEvent;
1505
1506        let mut context = create_test_context();
1507
1508        let provider = Provider;
1509        let rect = rect![0, 100, 600, 300];
1510        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1511        let mut rq = RenderQueue::new();
1512
1513        nav_bar.set_selected(Key(2), &mut rq, &mut context);
1514
1515        let (tx, _rx) = std::sync::mpsc::channel();
1516        let hub = tx;
1517        let mut bus = std::collections::VecDeque::new();
1518
1519        let start = pt!(300, 200);
1520        let end = pt!(300, 150);
1521
1522        let event = Event::Gesture(GestureEvent::Swipe {
1523            dir: Dir::North,
1524            start,
1525            end,
1526        });
1527
1528        let handled = nav_bar.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
1529
1530        assert!(handled, "North swipe should be handled");
1531
1532        let events: Vec<Event> = bus.drain(..).collect();
1533        assert!(
1534            events
1535                .iter()
1536                .any(|e| matches!(e, Event::NavigationBarResized(_))),
1537            "Should emit NavigationBarResized event"
1538        );
1539    }
1540
1541    #[test]
1542    fn handle_event_south_swipe_resizes_bar() {
1543        use crate::gesture::GestureEvent;
1544
1545        let mut context = create_test_context();
1546
1547        let provider = Provider;
1548        let rect = rect![0, 100, 600, 300];
1549        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1550        let mut rq = RenderQueue::new();
1551
1552        nav_bar.set_selected(Key(2), &mut rq, &mut context);
1553
1554        let (tx, _rx) = std::sync::mpsc::channel();
1555        let hub = tx;
1556        let mut bus = std::collections::VecDeque::new();
1557
1558        let start = pt!(300, 150);
1559        let end = pt!(300, 200);
1560
1561        let event = Event::Gesture(GestureEvent::Swipe {
1562            dir: Dir::South,
1563            start,
1564            end,
1565        });
1566
1567        let handled = nav_bar.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
1568
1569        assert!(handled, "South swipe should be handled");
1570
1571        let events: Vec<Event> = bus.drain(..).collect();
1572        assert!(
1573            events
1574                .iter()
1575                .any(|e| matches!(e, Event::NavigationBarResized(_))),
1576            "Should emit NavigationBarResized event"
1577        );
1578    }
1579
1580    #[test]
1581    fn handle_event_ignores_swipe_outside_rect() {
1582        use crate::gesture::GestureEvent;
1583
1584        let mut context = create_test_context();
1585
1586        let provider = Provider;
1587        let rect = rect![0, 100, 600, 300];
1588        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1589        let mut rq = RenderQueue::new();
1590
1591        nav_bar.set_selected(Key(2), &mut rq, &mut context);
1592
1593        let (tx, _rx) = std::sync::mpsc::channel();
1594        let hub = tx;
1595        let mut bus = std::collections::VecDeque::new();
1596
1597        let start = pt!(300, 50);
1598        let end = pt!(300, 10);
1599
1600        let event = Event::Gesture(GestureEvent::Swipe {
1601            dir: Dir::North,
1602            start,
1603            end,
1604        });
1605
1606        let handled = nav_bar.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
1607
1608        assert!(
1609            !handled,
1610            "Swipe outside rect should not be handled when both points are outside"
1611        );
1612    }
1613
1614    #[test]
1615    fn handle_event_ignores_horizontal_swipe() {
1616        use crate::gesture::GestureEvent;
1617
1618        let mut context = create_test_context();
1619
1620        let provider = Provider;
1621        let rect = rect![0, 100, 600, 300];
1622        let mut nav_bar = StackNavigationBar::new(rect, rect.max.y, 3, provider, Key(0));
1623        let mut rq = RenderQueue::new();
1624
1625        nav_bar.set_selected(Key(2), &mut rq, &mut context);
1626
1627        let (tx, _rx) = std::sync::mpsc::channel();
1628        let hub = tx;
1629        let mut bus = std::collections::VecDeque::new();
1630
1631        let start = pt!(200, 200);
1632        let end = pt!(400, 200);
1633
1634        let event = Event::Gesture(GestureEvent::Swipe {
1635            dir: Dir::East,
1636            start,
1637            end,
1638        });
1639
1640        let handled = nav_bar.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
1641
1642        assert!(!handled, "Horizontal swipe should not be handled");
1643    }
1644
1645    #[test]
1646    fn set_selected_handles_vertical_limit_constraint() {
1647        let mut context = create_test_context();
1648
1649        let provider = Provider;
1650        let rect = rect![0, 0, 600, 50];
1651        let vertical_limit = 100;
1652        let mut nav_bar = StackNavigationBar::new(rect, vertical_limit, 10, provider, Key(0));
1653        let mut rq = RenderQueue::new();
1654
1655        nav_bar.set_selected(Key(10), &mut rq, &mut context);
1656
1657        assert!(
1658            nav_bar.rect.max.y <= vertical_limit,
1659            "Navigation bar should respect vertical_limit even with many levels"
1660        );
1661
1662        assert!(
1663            !nav_bar.children.is_empty(),
1664            "Should have at least one bar even with tight constraints"
1665        );
1666    }
1667}