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}