Skip to main content

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