Skip to main content

ferroui_controls/
navigation.rs

1//! WinUI's navigation model of [`Frame`]: the navigation modes and cache modes, the arguments of
2//! the navigation events, the journal entries and the journal itself (`Microsoft.UI.Xaml.Navigation`
3//! of the type model `dxaml/xcp/tools/XCPTypesAutoGen/XamlOM/Model/Microsoft.UI.Xaml.Navigation.cs`,
4//! `dxaml/xcp/dxaml/lib/NavigationHistory.cpp`, `PageStackEntry_Partial.cpp` and
5//! `PageStackEntryTrackerCollection.cpp`, microsoft-ui-xaml commit
6//! `7e4b0c2096be402f51cc8f7d203ebf67cff183d0`; MIT, Copyright (c) Microsoft Corporation, see
7//! NOTICE.md).
8//!
9//! The types are in the markup namespace `FerroUI.Navigation` (not in the default XML namespace
10//! of the framework), so that this module's [`NavigationEventArgs`] does not collide with the
11//! `NavigationEventArgs` of the upstream pages (docs/porting/winui-controls.md, section 7).
12
13mod page_stack_entry;
14pub use page_stack_entry::PageStackEntry;
15
16use crate::{Frame, FramePage};
17use crate::{IItemsList, ItemsChangedHandler, ItemsSource};
18use ferroui_base::collections::{CollectionChangedHandler, FerroList};
19use ferroui_base::data::core::ValueTypes;
20use ferroui_base::data::model::{CollectionChange, Event, INotifyCollectionChanged, INotifyPropertyChanged};
21use ferroui_base::metadata::{MarkupType, MarkupTyped};
22use ferroui_base::{ferro_markup_enum, ferro_markup_type, BoxedValue, Ref, TypeInfo, WeakRef};
23use std::any::Any;
24use std::cell::{Cell, RefCell};
25use std::fmt;
26use std::rc::Rc;
27
28/// How a navigation was requested (WinUI's `NavigationMode`, with its values).
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
30#[repr(i32)]
31pub enum NavigationMode {
32    /// A new navigation (`Frame::navigate`).
33    #[default]
34    New = 0,
35    /// A navigation back in the journal (`Frame::go_back`).
36    Back = 1,
37    /// A navigation forward in the journal (`Frame::go_forward`).
38    Forward = 2,
39    /// A reload of the current page.
40    Refresh = 3,
41}
42
43/// Whether a frame keeps a page of a type to show it again (WinUI's `NavigationCacheMode`, with its
44/// values).
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
46#[repr(i32)]
47pub enum NavigationCacheMode {
48    /// The page is created for each navigation.
49    #[default]
50    Disabled = 0,
51    /// The page is kept whatever the cache size of the frame.
52    Required = 1,
53    /// The page is kept while the cache of the frame has room for it (`Frame::cache_size`).
54    Enabled = 2,
55}
56
57/// A navigation or an edit of the journal that failed. Where WinUI returns a failing `HRESULT`
58/// (or throws in C#), FerroUI returns this error.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum NavigationError {
61    /// No page type was given (`SourcePageType` set to null).
62    MissingSourcePageType,
63    /// The type does not derive from [`FramePage`].
64    NotAPage(String),
65    /// The type has neither a default constructor nor a page factory of the frame.
66    MissingConstructor(String),
67    /// The page factory of the frame failed, with its message.
68    FactoryFailed(String),
69    /// The page factory created a page of another type.
70    FactoryTypeMismatch {
71        /// The type of the navigation.
72        expected: String,
73        /// The type of the page the factory created.
74        actual: String,
75    },
76    /// The journal was edited during a navigation.
77    NavigationPending,
78    /// The entry belongs to the journal of another frame.
79    EntryOwnedByAnotherFrame,
80    /// The index is outside the journal.
81    IndexOutOfRange {
82        /// The index.
83        index: usize,
84        /// The number of entries.
85        count: usize,
86    },
87    /// `go_back` with an empty back stack.
88    NoBackEntry,
89    /// `go_forward` with an empty forward stack.
90    NoForwardEntry,
91    /// A negative `CacheSize`.
92    InvalidCacheSize(i32),
93}
94
95impl fmt::Display for NavigationError {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            Self::MissingSourcePageType => f.write_str("SourcePageType must identify a page type."),
99            Self::NotAPage(name) => write!(f, "'{name}' does not derive from FerroUI.Controls.FramePage."),
100            Self::MissingConstructor(name) => write!(f, "'{name}' has no default constructor or registered page factory."),
101            Self::FactoryFailed(message) => f.write_str(message),
102            Self::FactoryTypeMismatch { expected, actual } => write!(f, "The factory for '{expected}' returned '{actual}'."),
103            Self::NavigationPending => f.write_str("Navigation history cannot be modified during a pending navigation."),
104            Self::EntryOwnedByAnotherFrame => f.write_str("The page stack entry belongs to another frame."),
105            Self::IndexOutOfRange { index, count } => write!(f, "History index {index} is out of range for {count} entries."),
106            Self::NoBackEntry => f.write_str("The frame has no back entry."),
107            Self::NoForwardEntry => f.write_str("The frame has no forward entry."),
108            Self::InvalidCacheSize(size) => write!(f, "CacheSize must be nonnegative, got {size}."),
109        }
110    }
111}
112
113impl std::error::Error for NavigationError {}
114
115/// The options of one navigation (WinUI's `FrameNavigationOptions`). Clones share the values.
116///
117/// Missing (docs/porting/winui-controls.md, section 10.4): `TransitionInfoOverride`, FerroUI's
118/// frame has no navigation transitions.
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct FrameNavigationOptions {
121    is_navigation_stack_enabled: Rc<Cell<bool>>,
122}
123
124impl Default for FrameNavigationOptions {
125    fn default() -> Self {
126        Self { is_navigation_stack_enabled: Rc::new(Cell::new(true)) }
127    }
128}
129
130impl FrameNavigationOptions {
131    /// Creates the options of a navigation that is recorded in the journal.
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// Gets whether the navigation is recorded in the journal. `true` by default.
137    pub fn is_navigation_stack_enabled(&self) -> bool {
138        self.is_navigation_stack_enabled.get()
139    }
140
141    /// Sets whether the navigation is recorded in the journal.
142    pub fn set_is_navigation_stack_enabled(&self, value: bool) {
143        self.is_navigation_stack_enabled.set(value);
144    }
145}
146
147/// The arguments of `Frame.Navigating` and `FramePage::on_navigating_from` (WinUI's
148/// `NavigatingCancelEventArgs`). Clones share the cancellation, also the copies markup handlers
149/// receive.
150#[derive(Clone)]
151pub struct NavigatingCancelEventArgs {
152    cancel: Rc<Cell<bool>>,
153    navigation_mode: NavigationMode,
154    source_page_type: &'static TypeInfo,
155    parameter: Option<BoxedValue>,
156}
157
158impl PartialEq for NavigatingCancelEventArgs {
159    fn eq(&self, other: &Self) -> bool {
160        Rc::ptr_eq(&self.cancel, &other.cancel)
161    }
162}
163
164impl NavigatingCancelEventArgs {
165    pub(crate) fn new(mode: NavigationMode, entry: &PageStackEntry) -> Self {
166        Self {
167            cancel: Rc::new(Cell::new(false)),
168            navigation_mode: mode,
169            source_page_type: entry.source_page_type(),
170            parameter: entry.parameter(),
171        }
172    }
173
174    /// Gets whether the navigation is cancelled.
175    pub fn cancel(&self) -> bool {
176        self.cancel.get()
177    }
178
179    /// Cancels the navigation (`true`) or lets it go on.
180    pub fn set_cancel(&self, value: bool) {
181        self.cancel.set(value);
182    }
183
184    /// Gets how the navigation was requested.
185    pub fn navigation_mode(&self) -> NavigationMode {
186        self.navigation_mode
187    }
188
189    /// Gets the type of the page the frame navigates to.
190    pub fn source_page_type(&self) -> &'static TypeInfo {
191        self.source_page_type
192    }
193
194    /// Gets the parameter of the navigation.
195    pub fn parameter(&self) -> Option<BoxedValue> {
196        self.parameter.clone()
197    }
198}
199
200/// The arguments of `Frame.Navigated`, `Frame.NavigationStopped` and the navigation callbacks of
201/// [`FramePage`] (WinUI's `NavigationEventArgs`). It is `ferroui_controls::navigation::NavigationEventArgs`;
202/// the view builders name it `FrameNavigationEventArgs`, beside the upstream `NavigationEventArgs`
203/// of the pages.
204#[derive(Clone)]
205pub struct NavigationEventArgs {
206    identity: Rc<()>,
207    content: Option<Ref<FramePage>>,
208    navigation_mode: NavigationMode,
209    source_page_type: &'static TypeInfo,
210    parameter: Option<BoxedValue>,
211}
212
213impl PartialEq for NavigationEventArgs {
214    fn eq(&self, other: &Self) -> bool {
215        Rc::ptr_eq(&self.identity, &other.identity)
216    }
217}
218
219impl NavigationEventArgs {
220    pub(crate) fn new(content: Option<Ref<FramePage>>, mode: NavigationMode, entry: &PageStackEntry) -> Self {
221        Self {
222            identity: Rc::new(()),
223            content,
224            navigation_mode: mode,
225            source_page_type: entry.source_page_type(),
226            parameter: entry.parameter(),
227        }
228    }
229
230    /// Gets the page the frame shows after the navigation; `None` for a stopped navigation.
231    pub fn content(&self) -> Option<Ref<FramePage>> {
232        self.content.clone()
233    }
234
235    /// Gets how the navigation was requested.
236    pub fn navigation_mode(&self) -> NavigationMode {
237        self.navigation_mode
238    }
239
240    /// Gets the type of the page of the navigation.
241    pub fn source_page_type(&self) -> &'static TypeInfo {
242        self.source_page_type
243    }
244
245    /// Gets the parameter of the navigation.
246    pub fn parameter(&self) -> Option<BoxedValue> {
247        self.parameter.clone()
248    }
249}
250
251/// The arguments of `Frame.NavigationFailed` (WinUI's `NavigationFailedEventArgs`). Clones share
252/// `Handled`.
253#[derive(Clone, Debug)]
254pub struct NavigationFailedEventArgs {
255    handled: Rc<Cell<bool>>,
256    source_page_type: &'static TypeInfo,
257    exception: NavigationError,
258}
259
260impl PartialEq for NavigationFailedEventArgs {
261    fn eq(&self, other: &Self) -> bool {
262        Rc::ptr_eq(&self.handled, &other.handled)
263    }
264}
265
266impl NavigationFailedEventArgs {
267    pub(crate) fn new(source_page_type: &'static TypeInfo, exception: NavigationError) -> Self {
268        Self { handled: Rc::new(Cell::new(false)), source_page_type, exception }
269    }
270
271    /// Gets whether a handler handled the failure.
272    pub fn handled(&self) -> bool {
273        self.handled.get()
274    }
275
276    /// Marks the failure handled: `Frame::navigate` then returns `Ok(false)` instead of the error.
277    pub fn set_handled(&self, value: bool) {
278        self.handled.set(value);
279    }
280
281    /// Gets the type of the page the navigation failed to show.
282    pub fn source_page_type(&self) -> &'static TypeInfo {
283        self.source_page_type
284    }
285
286    /// Gets the error (WinUI's `Exception`).
287    pub fn exception(&self) -> &NavigationError {
288        &self.exception
289    }
290}
291
292struct NavigationStackData {
293    items: FerroList<Ref<PageStackEntry>>,
294    frame: RefCell<Option<WeakRef<Frame>>>,
295}
296
297/// The back or the forward stack of a frame (WinUI's `IVector<PageStackEntry>` of
298/// `Frame.BackStack` and `Frame.ForwardStack`): a live, observable list of entries. Clones are the
299/// same list.
300///
301/// An edit is rejected during a navigation, with an index outside the list, or with an entry of the
302/// journal of another frame (`NavigationHistory.cpp`, `PageStackEntryTrackerCollection.cpp`); the
303/// edits return a `Result` where WinUI returns a failing `HRESULT`.
304#[derive(Clone)]
305pub struct NavigationStack(Rc<NavigationStackData>);
306
307impl PartialEq for NavigationStack {
308    fn eq(&self, other: &Self) -> bool {
309        Rc::ptr_eq(&self.0, &other.0)
310    }
311}
312
313impl NavigationStack {
314    pub(crate) fn new() -> Self {
315        Self(Rc::new(NavigationStackData { items: FerroList::new(), frame: RefCell::new(None) }))
316    }
317
318    pub(crate) fn attach(&self, frame: &Ref<Frame>) {
319        *self.0.frame.borrow_mut() = Some(frame.downgrade());
320        let weak = frame.downgrade();
321        self.0.items.add_collection_changed(Rc::new(move |_| {
322            if let Some(frame) = weak.upgrade() {
323                frame.history_changed();
324            }
325        }));
326    }
327
328    fn frame(&self) -> Option<Ref<Frame>> {
329        self.0.frame.borrow().as_ref().and_then(WeakRef::upgrade)
330    }
331
332    fn validate_change(&self) -> Result<(), NavigationError> {
333        if self.frame().is_some_and(|frame| frame.is_navigation_pending()) {
334            Err(NavigationError::NavigationPending)
335        } else {
336            Ok(())
337        }
338    }
339
340    fn validate_entry(&self, entry: &PageStackEntry) -> Result<(), NavigationError> {
341        if let Some(owner) = entry.frame() {
342            if self.frame().is_none_or(|frame| !owner.ptr_eq(&frame)) {
343                return Err(NavigationError::EntryOwnedByAnotherFrame);
344            }
345        }
346        Ok(())
347    }
348
349    fn release(&self, entry: &PageStackEntry) {
350        if let Some(frame) = self.frame() {
351            if !frame.retains_entry(entry) {
352                entry.set_frame(None);
353            }
354        } else {
355            entry.set_frame(None);
356        }
357    }
358
359    /// Gets the number of entries.
360    pub fn count(&self) -> usize {
361        self.0.items.count()
362    }
363
364    /// Gets whether the stack has no entry.
365    pub fn is_empty(&self) -> bool {
366        self.0.items.is_empty()
367    }
368
369    /// Gets the entry at `index`.
370    pub fn get(&self, index: usize) -> Option<Ref<PageStackEntry>> {
371        self.0.items.try_get(index)
372    }
373
374    /// Gets the entries, oldest first.
375    pub fn snapshot(&self) -> Rc<Vec<Ref<PageStackEntry>>> {
376        self.0.items.snapshot()
377    }
378
379    /// Adds a handler of the changes of the list; returns its token.
380    pub fn add_collection_changed(&self, handler: Rc<CollectionChangedHandler<Ref<PageStackEntry>>>) -> u64 {
381        self.0.items.add_collection_changed(handler)
382    }
383
384    /// Removes the handler of `token`.
385    pub fn remove_collection_changed(&self, token: u64) -> bool {
386        self.0.items.remove_collection_changed(token)
387    }
388
389    /// Adds an entry at the end (the newest).
390    pub fn add(&self, entry: Ref<PageStackEntry>) -> Result<(), NavigationError> {
391        self.insert(self.count(), entry)
392    }
393
394    /// Inserts an entry at `index`.
395    pub fn insert(&self, index: usize, entry: Ref<PageStackEntry>) -> Result<(), NavigationError> {
396        self.validate_change()?;
397        self.validate_entry(&entry)?;
398        if index > self.count() {
399            return Err(NavigationError::IndexOutOfRange { index, count: self.count() });
400        }
401        entry.set_frame(self.frame().as_ref());
402        self.0.items.insert(index, entry);
403        Ok(())
404    }
405
406    /// Replaces the entry at `index`.
407    pub fn set(&self, index: usize, entry: Ref<PageStackEntry>) -> Result<(), NavigationError> {
408        self.validate_change()?;
409        self.validate_entry(&entry)?;
410        let old = self.get(index).ok_or(NavigationError::IndexOutOfRange { index, count: self.count() })?;
411        entry.set_frame(self.frame().as_ref());
412        self.0.items.set(index, entry);
413        self.release(&old);
414        Ok(())
415    }
416
417    /// Removes the entry at `index`.
418    pub fn remove_at(&self, index: usize) -> Result<(), NavigationError> {
419        self.validate_change()?;
420        let old = self.get(index).ok_or(NavigationError::IndexOutOfRange { index, count: self.count() })?;
421        self.0.items.remove_at(index);
422        self.release(&old);
423        Ok(())
424    }
425
426    /// Removes every entry.
427    pub fn clear(&self) -> Result<(), NavigationError> {
428        self.validate_change()?;
429        self.clear_internal();
430        Ok(())
431    }
432
433    pub(crate) fn push_internal(&self, entry: Ref<PageStackEntry>) {
434        entry.set_frame(self.frame().as_ref());
435        self.0.items.add(entry);
436    }
437
438    pub(crate) fn pop_internal(&self) {
439        if let Some(index) = self.count().checked_sub(1) {
440            let old = self.0.items.get(index);
441            self.0.items.remove_at(index);
442            self.release(&old);
443        }
444    }
445
446    pub(crate) fn clear_internal(&self) {
447        let old = self.snapshot();
448        self.0.items.clear();
449        for entry in old.iter() {
450            self.release(entry);
451        }
452    }
453}
454
455impl INotifyCollectionChanged for NavigationStack {
456    fn collection_changed(&self) -> &Event<CollectionChange> {
457        self.0.items.collection_changed()
458    }
459}
460
461impl INotifyPropertyChanged for NavigationStack {
462    fn property_changed(&self) -> &Event<str> {
463        self.0.items.property_changed()
464    }
465}
466
467impl IItemsList for NavigationStack {
468    fn count(&self) -> usize {
469        NavigationStack::count(self)
470    }
471    fn get_at(&self, index: usize) -> Option<BoxedValue> {
472        self.0.items.get_at(index)
473    }
474    fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
475        IItemsList::index_of(&self.0.items, item)
476    }
477    fn is_notifying(&self) -> bool {
478        true
479    }
480    fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
481        IItemsList::add_collection_changed(&self.0.items, handler)
482    }
483    fn remove_collection_changed(&self, token: u64) {
484        IItemsList::remove_collection_changed(&self.0.items, token);
485    }
486    fn identity(&self) -> Option<usize> {
487        Some(Rc::as_ptr(&self.0) as usize)
488    }
489    fn as_any(&self) -> &dyn Any {
490        self
491    }
492}
493
494impl From<NavigationStack> for ItemsSource {
495    fn from(stack: NavigationStack) -> Self {
496        let list: Rc<dyn IItemsList> = Rc::new(stack);
497        Self::from(list)
498    }
499}
500
501/// The index of an indexer of markup as an index of the list; a negative index is outside it.
502fn stack_entry(stack: &NavigationStack, index: i32) -> Result<Ref<PageStackEntry>, NavigationError> {
503    let count = stack.count();
504    usize::try_from(index)
505        .ok()
506        .and_then(|index| stack.get(index))
507        .ok_or(NavigationError::IndexOutOfRange { index: usize::try_from(index).unwrap_or(usize::MAX), count })
508}
509
510ferro_markup_enum!(NavigationMode { New, Back, Forward, Refresh }, { namespace: "FerroUI.Navigation" });
511ferro_markup_enum!(NavigationCacheMode { Disabled, Required, Enabled }, { namespace: "FerroUI.Navigation" });
512ferro_markup_type!(class FrameNavigationOptions {
513    namespace: "FerroUI.Navigation",
514    handles: [FrameNavigationOptions, Option<FrameNavigationOptions>],
515    constructors: [() => FrameNavigationOptions::new],
516    properties: [
517        IsNavigationStackEnabled: bool { get: FrameNavigationOptions::is_navigation_stack_enabled, set: FrameNavigationOptions::set_is_navigation_stack_enabled },
518    ],
519});
520ferro_markup_type!(class NavigatingCancelEventArgs {
521    namespace: "FerroUI.Navigation",
522    handles: [NavigatingCancelEventArgs],
523    properties: [
524        Cancel: bool { get: NavigatingCancelEventArgs::cancel, set: NavigatingCancelEventArgs::set_cancel },
525        NavigationMode: NavigationMode { get: NavigatingCancelEventArgs::navigation_mode },
526        SourcePageType: &'static TypeInfo { get: NavigatingCancelEventArgs::source_page_type },
527        Parameter: Option<BoxedValue> { get: NavigatingCancelEventArgs::parameter },
528    ],
529});
530ferro_markup_type!(class NavigationEventArgs {
531    namespace: "FerroUI.Navigation",
532    handles: [NavigationEventArgs],
533    properties: [
534        Content: Option<Ref<FramePage>> { get: NavigationEventArgs::content },
535        NavigationMode: NavigationMode { get: NavigationEventArgs::navigation_mode },
536        SourcePageType: &'static TypeInfo { get: NavigationEventArgs::source_page_type },
537        Parameter: Option<BoxedValue> { get: NavigationEventArgs::parameter },
538    ],
539});
540ferro_markup_type!(class NavigationFailedEventArgs {
541    namespace: "FerroUI.Navigation",
542    handles: [NavigationFailedEventArgs],
543    properties: [
544        Handled: bool { get: NavigationFailedEventArgs::handled, set: NavigationFailedEventArgs::set_handled },
545        SourcePageType: &'static TypeInfo { get: NavigationFailedEventArgs::source_page_type },
546        Exception: String { get: |args: &NavigationFailedEventArgs| args.exception().to_string() },
547    ],
548});
549ferro_markup_type!(class NavigationStack {
550    namespace: "FerroUI.Navigation",
551    handles: [NavigationStack, Option<NavigationStack>],
552    properties: [Count: i32 { get: |stack: &NavigationStack| i32::try_from(stack.count()).unwrap_or(i32::MAX) }],
553    methods: [
554        try fn Add(Ref<PageStackEntry>) => NavigationStack::add,
555        try fn Clear() => NavigationStack::clear,
556        try fn RemoveAt(usize) => NavigationStack::remove_at,
557    ],
558    indexers: [
559        (i32) -> Ref<PageStackEntry> { try_get: |stack: &NavigationStack, index: i32| stack_entry(stack, index) },
560    ],
561});
562
563/// The markup types of this module, listed with the value types of the crate
564/// (`markup_types/values.rs`).
565pub(crate) const MARKUP_TYPES: &[&MarkupType] = &[
566    <NavigationMode as MarkupTyped>::MARKUP,
567    <NavigationCacheMode as MarkupTyped>::MARKUP,
568    <FrameNavigationOptions as MarkupTyped>::MARKUP,
569    <NavigatingCancelEventArgs as MarkupTyped>::MARKUP,
570    <NavigationEventArgs as MarkupTyped>::MARKUP,
571    <NavigationFailedEventArgs as MarkupTyped>::MARKUP,
572    <NavigationStack as MarkupTyped>::MARKUP,
573];
574
575/// Registers the nullable forms of the value types of this module, and the journal as an items
576/// source and a notifying collection of bindings.
577pub(crate) fn register_value_types() {
578    ValueTypes::register_nullable::<NavigationMode>();
579    ValueTypes::register_nullable::<NavigationCacheMode>();
580    ValueTypes::register_nullable::<NavigationStack>();
581    ValueTypes::register_nullable::<FrameNavigationOptions>();
582    ItemsSource::register_binding_conversion::<NavigationStack>();
583    ferroui_base::data::model::ModelTypes::register_notifying_collection::<NavigationStack>();
584}