Skip to main content

floem/views/
navigation_stack.rs

1#![deny(missing_docs)]
2//! Navigation stack view driven by an explicit path.
3//!
4//! `NavigationStack` is Floem's path-based navigation primitive. The caller owns
5//! the navigation path and the stack reacts to that path over time, retaining
6//! views for the shared prefix and rebuilding only the changed tail.
7//!
8//! The API is intentionally builder-first:
9//! - [`NavigationStack::new`] accepts a path source
10//! - [`NavigationStackBuilder::key`] is optional when the item itself can be used
11//!   as the stable identity
12//! - [`NavigationStackBuilder::view`] is optional when the item itself implements
13//!   [`IntoView`]
14//!
15//! This means the common cases read naturally:
16//!
17//! ```rust
18//! # use floem::prelude::*;
19//! # use floem::views::NavigationStack;
20//! # #[derive(Clone, Hash, Eq, PartialEq)]
21//! # struct Route(String);
22//! # impl IntoView for Route {
23//! #     type V = floem::views::Label;
24//! #     type Intermediate = floem::view::LazyView<String>;
25//! #     fn into_intermediate(self) -> Self::Intermediate { floem::view::LazyView::new(self.0) }
26//! # }
27//! let path = RwSignal::new(vec![Route("Home".to_string())]);
28//!
29//! let stack = NavigationStack::new(path);
30//! ```
31//!
32//! Or with explicit identity and destination view construction:
33//!
34//! ```rust
35//! # use floem::prelude::*;
36//! # use floem::views::NavigationStack;
37//! # #[derive(Clone)]
38//! # enum Route { Home, Detail(u64) }
39//! let path = RwSignal::new(vec![Route::Home]);
40//!
41//! let stack = NavigationStack::new(path)
42//!     .key(|route| match route {
43//!         Route::Home => "home".to_string(),
44//!         Route::Detail(id) => format!("detail:{id}"),
45//!     })
46//!     .view(|route| match route {
47//!         Route::Home => "Home".into_any(),
48//!         Route::Detail(id) => format!("Detail {id}").into_any(),
49//!     });
50//! ```
51//!
52//! ## Path semantics
53//!
54//! The path is ordered root to top:
55//! - `[]` means no visible destination
56//! - `[A]` shows `A`
57//! - `[A, B, C]` shows `C`
58//!
59//! On updates, the stack compares the old and new path item keys:
60//! - matching prefix entries are retained
61//! - diverging entries and their descendants are disposed
62//! - new trailing entries are created
63//!
64//! This preserves local view state for retained destinations, which is the main
65//! reason to model navigation as a stack rather than rebuilding a single current
66//! destination every update.
67//!
68//! ## Path sources
69//!
70//! [`NavigationPath`] intentionally accepts several caller shapes:
71//! - plain `Vec<T>`
72//! - closures returning an iterable path
73//! - `ReadSignal<P>`
74//! - `RwSignal<P>`
75//!
76//! Signal-backed paths currently materialize owned path items on each read,
77//! because the navigation view update must own the items it compares and may
78//! need to construct new child views from them. Internally the module uses a
79//! `SmallVec` for the common short-path case.
80//!
81//! ## Manual refresh
82//!
83//! Most callers should rely on reactive path sources. For non-reactive sources,
84//! or when a caller wants to force a resync, send
85//! [`RefreshNavigationStackPath`] to the stack's [`ViewId`].
86//!
87// TODO: Tighten naming and lift NavigationPath into a reusable collection-source
88// abstraction shared by NavigationStack, Tab, and VirtualStack.
89
90use std::{any::Any, hash::Hash, marker::PhantomData, rc::Rc};
91
92use floem_reactive::{Effect, ReadSignal, RwSignal, Scope, SignalWith};
93use smallvec::SmallVec;
94
95use crate::{
96    context::{StyleCx, UpdateCx},
97    style::recalc::StyleReason,
98    view::{HasViewId, IntoView, View, ViewId},
99};
100
101type ViewFn<T> = Rc<dyn Fn(T) -> (Box<dyn View>, Scope)>;
102type PathItems<T> = SmallVec<[T; 8]>;
103type PathReader<T> = Rc<dyn Fn() -> PathItems<T>>;
104type KeyFn<T> = Rc<dyn Fn(&T) -> Box<dyn ErasedNavKey>>;
105
106/// Marker type for navigation stacks that use the item itself as the key.
107pub struct AutoNavigationKey;
108
109/// Marker type for navigation stacks that use an explicit key function.
110pub struct CustomNavigationKey<T> {
111    key_fn: KeyFn<T>,
112}
113
114/// Marker type for navigation stacks that use the item itself as the destination view.
115pub struct AutoNavigationView;
116
117/// Marker type for navigation stacks that use an explicit destination view function.
118pub struct CustomNavigationView<T> {
119    view_fn: ViewFn<T>,
120}
121
122/// Type-erased equality key used to compare retained path prefixes across updates.
123///
124/// `NavigationStack` is generic over the path item `T`, but not over the user's
125/// chosen key type `K`. The builder therefore erases `K` behind this trait and
126/// only retains the equality operation needed for prefix reuse.
127trait ErasedNavKey: Any {
128    fn as_any(&self) -> &dyn Any;
129    fn equals(&self, other: &dyn ErasedNavKey) -> bool;
130}
131
132impl<K> ErasedNavKey for K
133where
134    K: Eq + 'static,
135{
136    fn as_any(&self) -> &dyn Any {
137        self
138    }
139
140    fn equals(&self, other: &dyn ErasedNavKey) -> bool {
141        other.as_any().downcast_ref::<K>() == Some(self)
142    }
143}
144
145/// Retained navigation entry for one path segment.
146///
147/// The stack keeps the child `ViewId` and `Scope` alive while that segment
148/// remains in the shared path prefix, and drops both once the segment falls
149/// out of the stack.
150struct NavChild {
151    key: Box<dyn ErasedNavKey>,
152    id: ViewId,
153    scope: Scope,
154}
155
156enum NavigationStackState<T> {
157    SetPath(PathItems<T>),
158}
159
160/// Message that can be sent to a [`NavigationStack`] [`ViewId`] to force the stack
161/// to re-read its current path source.
162pub struct RefreshNavigationStackPath;
163
164/// A path source that can produce the current navigation path on demand.
165///
166/// This supports plain vectors, closures, and reactive signals whose values can
167/// be converted into iterators of path items.
168pub trait NavigationPath<T>: 'static {
169    /// Convert this source into an owned path reader.
170    fn into_path_reader(self) -> PathReader<T>;
171}
172
173impl<T> NavigationPath<T> for Vec<T>
174where
175    T: Clone + 'static,
176{
177    fn into_path_reader(self) -> PathReader<T> {
178        Rc::new(move || self.iter().cloned().collect())
179    }
180}
181
182impl<T, F, I> NavigationPath<T> for F
183where
184    F: Fn() -> I + 'static,
185    I: IntoIterator<Item = T>,
186{
187    fn into_path_reader(self) -> PathReader<T> {
188        Rc::new(move || self().into_iter().collect::<PathItems<_>>())
189    }
190}
191
192impl<T, P, S> NavigationPath<T> for ReadSignal<P, S>
193where
194    ReadSignal<P, S>: SignalWith<P>,
195    P: 'static,
196    for<'a> &'a P: IntoIterator<Item = &'a T>,
197    S: 'static,
198    T: Clone + 'static,
199{
200    fn into_path_reader(self) -> PathReader<T> {
201        Rc::new(move || self.with(|path| path.into_iter().cloned().collect::<PathItems<_>>()))
202    }
203}
204
205impl<T, P, S> NavigationPath<T> for RwSignal<P, S>
206where
207    RwSignal<P, S>: SignalWith<P>,
208    P: 'static,
209    for<'a> &'a P: IntoIterator<Item = &'a T>,
210    S: 'static,
211    T: Clone + 'static,
212{
213    fn into_path_reader(self) -> PathReader<T> {
214        Rc::new(move || self.with(|path| path.into_iter().cloned().collect::<PathItems<_>>()))
215    }
216}
217
218/// Builder for a [`NavigationStack`].
219///
220/// This builder supports two optional pieces of configuration:
221/// - [`Self::key`] supplies stable identity when the path item itself should not
222///   be used as the key
223/// - [`Self::view`] supplies destination view construction when the path item
224///   itself does not implement [`IntoView`]
225pub struct NavigationStackBuilder<T, K = AutoNavigationKey, V = AutoNavigationView>
226where
227    T: 'static,
228{
229    id: ViewId,
230    path_reader: PathReader<T>,
231    key: K,
232    view: V,
233}
234
235impl<T> NavigationStackBuilder<T, AutoNavigationKey, AutoNavigationView>
236where
237    T: 'static,
238{
239    fn new<P>(path: P) -> Self
240    where
241        P: NavigationPath<T>,
242    {
243        Self {
244            id: ViewId::new(),
245            path_reader: path.into_path_reader(),
246            key: AutoNavigationKey,
247            view: AutoNavigationView,
248        }
249    }
250}
251
252impl<T, K, V> NavigationStackBuilder<T, K, V>
253where
254    T: 'static,
255{
256    /// Provide a stable key function for navigation path items.
257    ///
258    /// Use this when `T` does not implement `Eq + Hash`, or when the path item
259    /// contains additional data that should not participate in destination
260    /// identity.
261    pub fn key<KF, Key>(self, key_fn: KF) -> NavigationStackBuilder<T, CustomNavigationKey<T>, V>
262    where
263        KF: Fn(&T) -> Key + 'static,
264        Key: Eq + Hash + 'static,
265    {
266        NavigationStackBuilder {
267            id: self.id,
268            path_reader: self.path_reader,
269            key: CustomNavigationKey {
270                key_fn: Rc::new(move |item| Box::new(key_fn(item))),
271            },
272            view: self.view,
273        }
274    }
275
276    /// Provide an explicit destination view function for navigation path items.
277    ///
278    /// Use this when `T` is route data rather than a view in its own right.
279    pub fn view<VF, IV>(self, view_fn: VF) -> NavigationStackBuilder<T, K, CustomNavigationView<T>>
280    where
281        VF: Fn(T) -> IV + 'static,
282        IV: IntoView + 'static,
283    {
284        NavigationStackBuilder {
285            id: self.id,
286            path_reader: self.path_reader,
287            key: self.key,
288            view: CustomNavigationView {
289                view_fn: Rc::new(
290                    Scope::current().enter_child(move |item| view_fn(item).into_any()),
291                ),
292            },
293        }
294    }
295}
296
297impl<T, K, V> HasViewId for NavigationStackBuilder<T, K, V>
298where
299    T: 'static,
300{
301    fn view_id(&self) -> ViewId {
302        self.id
303    }
304}
305
306impl<T> IntoView for NavigationStackBuilder<T, AutoNavigationKey, AutoNavigationView>
307where
308    T: Eq + Hash + Clone + IntoView + 'static,
309{
310    type V = NavigationStack<T>;
311    type Intermediate = Self;
312
313    fn into_intermediate(self) -> Self::Intermediate {
314        self
315    }
316
317    fn into_view(self) -> Self::V {
318        NavigationStack::from_parts(
319            self.id,
320            self.path_reader,
321            Rc::new(|item: &T| Box::new(item.clone())),
322            Rc::new(Scope::current().enter_child(move |item: T| item.into_any())),
323        )
324    }
325}
326
327impl<T> IntoView for NavigationStackBuilder<T, AutoNavigationKey, CustomNavigationView<T>>
328where
329    T: Eq + Hash + Clone + 'static,
330{
331    type V = NavigationStack<T>;
332    type Intermediate = Self;
333
334    fn into_intermediate(self) -> Self::Intermediate {
335        self
336    }
337
338    fn into_view(self) -> Self::V {
339        NavigationStack::from_parts(
340            self.id,
341            self.path_reader,
342            Rc::new(|item: &T| Box::new(item.clone())),
343            self.view.view_fn,
344        )
345    }
346}
347
348impl<T> IntoView for NavigationStackBuilder<T, CustomNavigationKey<T>, AutoNavigationView>
349where
350    T: IntoView + 'static,
351{
352    type V = NavigationStack<T>;
353    type Intermediate = Self;
354
355    fn into_intermediate(self) -> Self::Intermediate {
356        self
357    }
358
359    fn into_view(self) -> Self::V {
360        NavigationStack::from_parts(
361            self.id,
362            self.path_reader,
363            self.key.key_fn,
364            Rc::new(Scope::current().enter_child(move |item: T| item.into_any())),
365        )
366    }
367}
368
369impl<T> IntoView for NavigationStackBuilder<T, CustomNavigationKey<T>, CustomNavigationView<T>>
370where
371    T: 'static,
372{
373    type V = NavigationStack<T>;
374    type Intermediate = Self;
375
376    fn into_intermediate(self) -> Self::Intermediate {
377        self
378    }
379
380    fn into_view(self) -> Self::V {
381        NavigationStack::from_parts(
382            self.id,
383            self.path_reader,
384            self.key.key_fn,
385            self.view.view_fn,
386        )
387    }
388}
389
390/// A navigation view that retains a stack of destination views for an explicit path.
391///
392/// `NavigationStack` is designed for hierarchical navigation where the caller
393/// owns the full path and updates it over time. The path is interpreted from
394/// root to top:
395/// - `[A]` shows `A`
396/// - `[A, B]` shows `B`
397/// - `[A, B, C]` shows `C`
398///
399/// On each update, the stack compares the old and new path using stable keys:
400/// - the shared prefix is retained
401/// - any diverging tail is removed and disposed
402/// - any new tail is created
403///
404/// This means destination views in the shared prefix keep their local view
405/// state while deeper navigation changes happen above them.
406///
407/// `NavigationStack` is intentionally built through [`NavigationStack::new`],
408/// which returns a builder. That builder lets callers choose whether to:
409/// - use the path item itself as the stable key, or provide [`NavigationStackBuilder::key`]
410/// - use the path item itself as the destination view, or provide [`NavigationStackBuilder::view`]
411///
412/// Typical usage when the route type implements both [`IntoView`] and
413/// `Eq + Hash + Clone`:
414///
415/// ```rust
416/// # use floem::prelude::*;
417/// # use floem::views::NavigationStack;
418/// # #[derive(Clone, Eq, PartialEq, Hash)]
419/// # struct Route(String);
420/// # impl IntoView for Route {
421/// #     type V = floem::views::Label;
422/// #     type Intermediate = floem::view::LazyView<String>;
423/// #     fn into_intermediate(self) -> Self::Intermediate {
424/// #         floem::view::LazyView::new(self.0)
425/// #     }
426/// # }
427/// let path = RwSignal::new(vec![Route("Home".to_string())]);
428///
429/// let stack = NavigationStack::new(path);
430/// ```
431///
432/// Typical usage when route data needs explicit keying and destination
433/// construction:
434///
435/// ```rust
436/// # use floem::prelude::*;
437/// # use floem::views::NavigationStack;
438/// # #[derive(Clone)]
439/// # enum Route { Home, Detail(u64) }
440/// let path = RwSignal::new(vec![Route::Home]);
441///
442/// let stack = NavigationStack::new(path)
443///     .key(|route| match route {
444///         Route::Home => "home".to_string(),
445///         Route::Detail(id) => format!("detail:{id}"),
446///     })
447///     .view(|route| match route {
448///         Route::Home => "Home".to_string(),
449///         Route::Detail(id) => format!("Detail {id}"),
450///     });
451/// ```
452pub struct NavigationStack<T>
453where
454    T: 'static,
455{
456    id: ViewId,
457    path_reader: PathReader<T>,
458    key_fn: KeyFn<T>,
459    view_fn: ViewFn<T>,
460    children: Vec<NavChild>,
461    phantom: PhantomData<T>,
462}
463
464impl<T> NavigationStack<T>
465where
466    T: 'static,
467{
468    /// Start building a [`NavigationStack`] from a path source.
469    ///
470    /// This is the single entrypoint for the navigation stack API. It accepts a
471    /// path source and returns a [`NavigationStackBuilder`], which can then be
472    /// used in one of two ways:
473    ///
474    /// 1. Build directly when `T` already provides everything the stack needs:
475    ///    - `T: IntoView` for destination construction
476    ///    - `T: Eq + Hash + Clone` for stable destination identity
477    /// 2. Refine the builder with:
478    ///    - [`NavigationStackBuilder::key`] when identity should come from a
479    ///      derived key rather than the whole item
480    ///    - [`NavigationStackBuilder::view`] when the path item is route data
481    ///      rather than a view
482    ///
483    /// The `path` argument can be any supported [`NavigationPath`], including:
484    /// - a plain `Vec<T>`
485    /// - a closure returning an iterable path
486    /// - `ReadSignal<P>`
487    /// - `RwSignal<P>`
488    ///
489    /// The stack reads the full path and retains destination views for the
490    /// shared prefix across updates.
491    ///
492    /// # Example: route items are their own views
493    ///
494    /// ```rust
495    /// # use floem::prelude::*;
496    /// # use floem::views::NavigationStack;
497    /// # #[derive(Clone, Eq, PartialEq, Hash)]
498    /// # struct Route(String);
499    /// # impl IntoView for Route {
500    /// #     type V = floem::views::Label;
501    /// #     type Intermediate = floem::view::LazyView<String>;
502    /// #     fn into_intermediate(self) -> Self::Intermediate {
503    /// #         floem::view::LazyView::new(self.0)
504    /// #     }
505    /// # }
506    /// let path = RwSignal::new(vec![Route("Home".to_string())]);
507    ///
508    /// let stack = NavigationStack::new(path);
509    /// ```
510    ///
511    /// # Example: route items need explicit key and view functions
512    ///
513    /// ```rust
514    /// # use floem::prelude::*;
515    /// # use floem::views::NavigationStack;
516    /// # #[derive(Clone)]
517    /// # enum Route { Home, Detail(u64) }
518    /// let path = RwSignal::new(vec![Route::Home]);
519    ///
520    /// let stack = NavigationStack::new(path)
521    ///     .key(|route| match route {
522    ///         Route::Home => "home".to_string(),
523    ///         Route::Detail(id) => format!("detail:{id}"),
524    ///     })
525    ///     .view(|route| match route {
526    ///         Route::Home => "Home".to_string(),
527    ///         Route::Detail(id) => format!("Detail {id}"),
528    ///     });
529    /// ```
530    #[allow(clippy::new_ret_no_self)]
531    pub fn new<P>(path: P) -> NavigationStackBuilder<T>
532    where
533        P: NavigationPath<T>,
534    {
535        NavigationStackBuilder::new(path)
536    }
537
538    fn from_parts(
539        id: ViewId,
540        path_reader: PathReader<T>,
541        key_fn: KeyFn<T>,
542        view_fn: ViewFn<T>,
543    ) -> Self {
544        let effect_path_reader = path_reader.clone();
545
546        Effect::new(move |_| {
547            id.update_state(NavigationStackState::SetPath(effect_path_reader()));
548        });
549
550        Self {
551            id,
552            path_reader,
553            key_fn,
554            view_fn,
555            children: Vec::new(),
556            phantom: PhantomData,
557        }
558    }
559
560    // Reconcile the retained child stack with the current path.
561    fn sync_path(&mut self, cx: &mut UpdateCx, path: PathItems<T>) {
562        let mut keyed_items = path
563            .into_iter()
564            .map(|item| ((self.key_fn)(&item), item))
565            .collect::<SmallVec<[(_, _); 8]>>();
566
567        let shared_prefix = self
568            .children
569            .iter()
570            .zip(keyed_items.iter())
571            .take_while(|(existing, incoming)| existing.key.equals(&*incoming.0))
572            .count();
573
574        while self.children.len() > shared_prefix {
575            if let Some(child) = self.children.pop() {
576                cx.window_state.remove_view(child.id);
577                child.scope.dispose();
578            }
579        }
580
581        for (key, item) in keyed_items.drain(shared_prefix..) {
582            let (view, scope) = (self.view_fn)(item);
583            let child_id = view.id();
584            child_id.set_view(view);
585            child_id.set_parent(self.id);
586            self.children.push(NavChild {
587                key,
588                id: child_id,
589                scope,
590            });
591        }
592
593        self.id
594            .set_children_ids(self.children.iter().map(|child| child.id).collect());
595        self.id.request_style(StyleReason::style_pass());
596    }
597}
598
599impl<T> View for NavigationStack<T>
600where
601    T: 'static,
602{
603    fn id(&self) -> ViewId {
604        self.id
605    }
606
607    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
608        "NavigationStack".into()
609    }
610
611    fn update(&mut self, cx: &mut UpdateCx, state: Box<dyn Any>) {
612        match state.downcast::<NavigationStackState<T>>() {
613            Ok(state) => match *state {
614                NavigationStackState::SetPath(path) => self.sync_path(cx, path),
615            },
616            Err(state) => {
617                if state.downcast::<RefreshNavigationStackPath>().is_ok() {
618                    self.sync_path(cx, (self.path_reader)());
619                }
620            }
621        }
622    }
623
624    fn style_pass(&mut self, _cx: &mut StyleCx<'_>) {
625        let last = self.children.len().checked_sub(1);
626        for (index, child) in self.id.children().into_iter().enumerate() {
627            if Some(index) == last {
628                child.set_visible();
629            } else {
630                child.set_hidden();
631            }
632        }
633    }
634}