1mod 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
30#[repr(i32)]
31pub enum NavigationMode {
32 #[default]
34 New = 0,
35 Back = 1,
37 Forward = 2,
39 Refresh = 3,
41}
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
46#[repr(i32)]
47pub enum NavigationCacheMode {
48 #[default]
50 Disabled = 0,
51 Required = 1,
53 Enabled = 2,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum NavigationError {
61 MissingSourcePageType,
63 NotAPage(String),
65 MissingConstructor(String),
67 FactoryFailed(String),
69 FactoryTypeMismatch {
71 expected: String,
73 actual: String,
75 },
76 NavigationPending,
78 EntryOwnedByAnotherFrame,
80 IndexOutOfRange {
82 index: usize,
84 count: usize,
86 },
87 NoBackEntry,
89 NoForwardEntry,
91 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#[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 pub fn new() -> Self {
133 Self::default()
134 }
135
136 pub fn is_navigation_stack_enabled(&self) -> bool {
138 self.is_navigation_stack_enabled.get()
139 }
140
141 pub fn set_is_navigation_stack_enabled(&self, value: bool) {
143 self.is_navigation_stack_enabled.set(value);
144 }
145}
146
147#[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 pub fn cancel(&self) -> bool {
176 self.cancel.get()
177 }
178
179 pub fn set_cancel(&self, value: bool) {
181 self.cancel.set(value);
182 }
183
184 pub fn navigation_mode(&self) -> NavigationMode {
186 self.navigation_mode
187 }
188
189 pub fn source_page_type(&self) -> &'static TypeInfo {
191 self.source_page_type
192 }
193
194 pub fn parameter(&self) -> Option<BoxedValue> {
196 self.parameter.clone()
197 }
198}
199
200#[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 pub fn content(&self) -> Option<Ref<FramePage>> {
232 self.content.clone()
233 }
234
235 pub fn navigation_mode(&self) -> NavigationMode {
237 self.navigation_mode
238 }
239
240 pub fn source_page_type(&self) -> &'static TypeInfo {
242 self.source_page_type
243 }
244
245 pub fn parameter(&self) -> Option<BoxedValue> {
247 self.parameter.clone()
248 }
249}
250
251#[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 pub fn handled(&self) -> bool {
273 self.handled.get()
274 }
275
276 pub fn set_handled(&self, value: bool) {
278 self.handled.set(value);
279 }
280
281 pub fn source_page_type(&self) -> &'static TypeInfo {
283 self.source_page_type
284 }
285
286 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#[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 pub fn count(&self) -> usize {
361 self.0.items.count()
362 }
363
364 pub fn is_empty(&self) -> bool {
366 self.0.items.is_empty()
367 }
368
369 pub fn get(&self, index: usize) -> Option<Ref<PageStackEntry>> {
371 self.0.items.try_get(index)
372 }
373
374 pub fn snapshot(&self) -> Rc<Vec<Ref<PageStackEntry>>> {
376 self.0.items.snapshot()
377 }
378
379 pub fn add_collection_changed(&self, handler: Rc<CollectionChangedHandler<Ref<PageStackEntry>>>) -> u64 {
381 self.0.items.add_collection_changed(handler)
382 }
383
384 pub fn remove_collection_changed(&self, token: u64) -> bool {
386 self.0.items.remove_collection_changed(token)
387 }
388
389 pub fn add(&self, entry: Ref<PageStackEntry>) -> Result<(), NavigationError> {
391 self.insert(self.count(), entry)
392 }
393
394 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 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 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 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
501fn 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
563pub(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
575pub(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}