Skip to main content

ferroui_controls/page/
multi_page.rs

1use super::{DefaultPageDataTemplate, NavigationType, Page, PageImpl};
2use crate::primitives::TemplatedControlImpl;
3use crate::templates::IDataTemplate;
4use crate::{
5    Control, ControlImpl, IItemsList, ItemsChangedEventArgs, ItemsChangedHandler, ItemsSource, ItemsView,
6};
7use ferroui_base::collections::{FerroList, NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs};
8use ferroui_base::data::core::ValueTypes;
9use ferroui_base::input::InputElementImpl;
10use ferroui_base::interactivity::InteractiveImpl;
11use ferroui_base::layout::LayoutableImpl;
12use ferroui_base::reactive::{Disposable, IDisposable};
13use ferroui_base::utilities::HandlerList;
14use ferroui_base::{
15    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, BoxedValue, FerroObjectImpl,
16    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElement, StyledElementImpl,
17    StyledProperty, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
18};
19use std::any::Any;
20use std::cell::{Cell, RefCell};
21use std::rc::{Rc, Weak};
22
23/// The collection of child pages of a multi-page: a shared handle to an
24/// observable list of pages.
25pub type PageList = FerroList<Ref<Page>>;
26
27/// The signature of a handler of the `PagesChanged` event.
28pub type PagesChangedHandler = dyn for<'a> Fn(&NotifyCollectionChangedEventArgs<'a, Ref<Page>>);
29
30/// Abstract base class for pages that host a collection of child pages.
31#[repr(C)]
32pub struct MultiPage {
33    base: Page,
34    current_page_changed: HandlerList<dyn Fn()>,
35    pages_changed: HandlerList<PagesChangedHandler>,
36    /// The collection whose changes are observed and the token of the
37    /// subscription.
38    pages_subscription: RefCell<Option<(PageList, u64)>>,
39    /// Whether the value of the `Pages` property counts as a collection that
40    /// notifies of changes. See [`MultiPage::set_observes_pages`].
41    observes_pages: Cell<bool>,
42}
43
44ferro_class! {
45    MultiPage: Page, virtuals MultiPageImpl: PageImpl {
46        /// Called when the active child page changes.
47        ///
48        /// The base class calls this method in the following situations,
49        /// passing the corresponding navigation type:
50        ///
51        /// - [`NavigationType::Replace`]: the `Pages` collection was
52        ///   assigned, a new item was added, or the active page changed for
53        ///   any reason not covered below.
54        /// - [`NavigationType::Remove`]: the currently active page was
55        ///   removed from the `Pages` collection or the collection was reset
56        ///   and the active page is no longer present. Subclasses should
57        ///   select a replacement page.
58        ///
59        /// Subclasses may also call this method directly with any navigation
60        /// type appropriate for their own navigation model.
61        fn update_active_page_with(this, navigation_type: NavigationType);
62    }
63}
64
65ferro_class_info!(MultiPage {
66    markup: {
67        content: Pages,
68    },
69});
70
71ferro_impl_classes!(
72    MultiPage: StyledElementImpl,
73    LayoutableImpl,
74    InteractiveImpl,
75    InputElementImpl,
76    ControlImpl,
77    TemplatedControlImpl
78);
79
80impl FerroObjectImpl for MultiPage {
81    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
82        Self::parent_on_property_changed(this, change);
83
84        if change.property() == Self::pages_property().as_property() {
85            let (old_value, new_value) = change.get_old_and_new_value::<Option<PageList>>();
86            if old_value == new_value {
87                return;
88            }
89
90            if let Some(old_notify_collection) = &old_value {
91                this.unsubscribe_from_pages(old_notify_collection);
92            }
93
94            let logical_children = StyledElement::logical_children(this);
95            logical_children.clear();
96
97            if let Some(new_items) = &new_value {
98                for page in new_items.snapshot().iter() {
99                    logical_children.add(page.clone().upcast());
100                }
101            }
102
103            if let Some(new_notify_collection) = &new_value {
104                this.subscribe_to_pages(new_notify_collection);
105            }
106
107            if new_value.is_some() {
108                this.update_active_page();
109            } else {
110                this.set_current_value(Page::current_page_property(), None);
111            }
112        } else if change.property() == Page::current_page_property().as_property() {
113            for (_, handler) in this.current_page_changed.snapshot().iter() {
114                handler();
115            }
116        }
117    }
118}
119
120impl Drop for MultiPage {
121    fn drop(&mut self) {
122        // A page that is dropped while it observes its pages (it never left the visual tree, or was
123        // never in it) releases the subscription: the collection may outlive the page.
124        if let Some((collection, token)) = self.pages_subscription.get_mut().take() {
125            collection.remove_collection_changed(token);
126        }
127    }
128}
129
130impl VisualImpl for MultiPage {
131    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
132        Self::parent_on_attached_to_visual_tree(this, e);
133
134        if let Some(collection) = this.pages() {
135            this.unsubscribe_from_pages(&collection);
136            this.subscribe_to_pages(&collection);
137        }
138    }
139
140    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
141        Self::parent_on_detached_from_visual_tree(this, e);
142
143        if let Some(collection) = this.pages() {
144            this.unsubscribe_from_pages(&collection);
145        }
146    }
147}
148
149impl PageImpl for MultiPage {
150    /// Routes no-argument calls to
151    /// [`update_active_page_with`](MultiPageImpl::update_active_page_with)
152    /// using [`NavigationType::Replace`] as the default. Subclasses must
153    /// override the typed member, not this one.
154    fn update_active_page(this: &Self) {
155        this.update_active_page_with(NavigationType::Replace)
156    }
157}
158
159impl MultiPageImpl for MultiPage {
160    fn update_active_page_with(_this: &Self, _navigation_type: NavigationType) {}
161}
162
163ferro_properties! {
164    impl MultiPage {
165        /// Defines the `Pages` property.
166        pub fn pages_property() -> StyledProperty<Option<PageList>> {
167            FerroProperty::register::<MultiPage, _>("Pages", None)
168        }
169
170        /// Defines the `ItemsSource` property.
171        pub fn items_source_property() -> StyledProperty<Option<ItemsSource>> {
172            FerroProperty::register::<MultiPage, _>("ItemsSource", None)
173        }
174
175        /// Defines the `PageTemplate` property.
176        pub fn page_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
177            let default_template: Rc<dyn IDataTemplate> = DefaultPageDataTemplate::new();
178            FerroProperty::register::<MultiPage, _>("PageTemplate", Some(default_template))
179        }
180    }
181}
182
183impl MultiPage {
184    fn static_constructor() {
185        // Lets a binding deliver the value of the `Pages` property to the
186        // items source of the inner tab or carousel control.
187        ValueTypes::register_conversion::<PageList, ItemsSource>(|pages| Some(Self::items_source_of(pages)));
188        ValueTypes::register_conversion::<PageList, Option<ItemsSource>>(|pages| Some(Some(Self::items_source_of(pages))));
189        ValueTypes::register_conversion::<Option<PageList>, Option<ItemsSource>>(|pages| {
190            Some(pages.as_ref().map(Self::items_source_of))
191        });
192    }
193
194    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
195    pub fn construct() -> Self {
196        Self {
197            base: Page::construct(),
198            current_page_changed: HandlerList::new(),
199            pages_changed: HandlerList::new(),
200            pages_subscription: RefCell::new(None),
201            observes_pages: Cell::new(true),
202        }
203    }
204
205    /// Gets or sets the collection of child pages.
206    ///
207    /// This control subscribes to the changes of the assigned collection.
208    /// The subscription is released when a new collection is assigned or
209    /// when the control is detached from the visual tree.
210    pub fn pages(&self) -> Option<PageList> {
211        self.get_value(Self::pages_property())
212    }
213
214    pub fn set_pages(&self, value: Option<PageList>) {
215        self.set_value(Self::pages_property(), value)
216    }
217
218    /// Gets or sets a view-model collection to bind to. Use together with
219    /// the page template to convert each item into a page. When set, takes
220    /// precedence over the `Pages` property as the item source for the
221    /// inner tab or carousel control.
222    pub fn items_source(&self) -> Option<ItemsSource> {
223        self.get_value(Self::items_source_property())
224    }
225
226    pub fn set_items_source(&self, value: Option<ItemsSource>) {
227        self.set_value(Self::items_source_property(), value)
228    }
229
230    /// Gets or sets the data template used to create pages from view-model
231    /// items.
232    pub fn page_template(&self) -> Option<Rc<dyn IDataTemplate>> {
233        self.get_value(Self::page_template_property())
234    }
235
236    pub fn set_page_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
237        self.set_value(Self::page_template_property(), value)
238    }
239
240    /// Occurs when the `CurrentPage` property changes. Disposing the
241    /// returned handle unsubscribes.
242    pub fn current_page_changed(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
243        let token = self.current_page_changed.add(Rc::new(handler));
244        let weak = self.to_ref().downgrade();
245        Disposable::create(move || {
246            if let Some(this) = weak.upgrade() {
247                this.current_page_changed.remove(token);
248            }
249        })
250    }
251
252    /// Occurs when the `Pages` collection changes. Disposing the returned
253    /// handle unsubscribes.
254    pub fn pages_changed(
255        &self,
256        handler: impl for<'a> Fn(&NotifyCollectionChangedEventArgs<'a, Ref<Page>>) + 'static,
257    ) -> Rc<dyn IDisposable> {
258        let token = self.pages_changed.add(Rc::new(handler));
259        let weak = self.to_ref().downgrade();
260        Disposable::create(move || {
261            if let Some(this) = weak.upgrade() {
262                this.pages_changed.remove(token);
263            }
264        })
265    }
266
267    /// The untyped items list of a collection of pages, for the items source
268    /// of an items control: each page is an item boxed as a control. The
269    /// same handle is returned for the same collection for as long as a
270    /// handle is alive, so that assigning it twice is not a change.
271    pub fn items_source_of(pages: &PageList) -> ItemsSource {
272        ferroui_base::ferro_thread_local! {
273            static SOURCES: RefCell<Vec<Weak<PageItemsList>>> = const { RefCell::new(Vec::new()) };
274        }
275
276        SOURCES.with(|sources| {
277            let mut sources = sources.borrow_mut();
278            sources.retain(|source| source.strong_count() > 0);
279            if let Some(existing) = sources.iter().filter_map(Weak::upgrade).find(|source| source.0.ptr_eq(pages)) {
280                return ItemsSource::new(existing);
281            }
282            let list = Rc::new(PageItemsList(pages.clone()));
283            sources.push(Rc::downgrade(&list));
284            ItemsSource::new(list)
285        })
286    }
287
288    /// The value of the `Pages` property as the items source of an items
289    /// control. See [`items_source_of`](Self::items_source_of).
290    pub fn pages_items_source(&self) -> Option<ItemsSource> {
291        self.pages().as_ref().map(Self::items_source_of)
292    }
293
294    /// States whether the value of the `Pages` property is a collection
295    /// that notifies of changes (the default). The reference implementation
296    /// observes the collection only if it does; a subclass that exposes a
297    /// collection it mutates itself (the navigation stack of a navigation
298    /// page) opts out before assigning it.
299    pub(crate) fn set_observes_pages(&self, value: bool) {
300        self.observes_pages.set(value);
301    }
302
303    fn subscribe_to_pages(&self, collection: &PageList) {
304        if !self.observes_pages.get() {
305            return;
306        }
307
308        let weak = self.to_ref().downgrade();
309        let token = collection.add_collection_changed(Rc::new(
310            move |e: &NotifyCollectionChangedEventArgs<'_, Ref<Page>>| {
311                if let Some(this) = weak.upgrade() {
312                    this.notify_collection_collection_changed(e);
313                }
314            },
315        ));
316        let previous = self.pages_subscription.borrow_mut().replace((collection.clone(), token));
317        if let Some((previous, token)) = previous {
318            previous.remove_collection_changed(token);
319        }
320    }
321
322    fn unsubscribe_from_pages(&self, collection: &PageList) {
323        let subscription = {
324            let mut subscription = self.pages_subscription.borrow_mut();
325            if subscription.as_ref().is_some_and(|(subscribed, _)| subscribed.ptr_eq(collection)) {
326                subscription.take()
327            } else {
328                None
329            }
330        };
331        if let Some((subscribed, token)) = subscription {
332            subscribed.remove_collection_changed(token);
333        }
334    }
335
336    fn notify_collection_collection_changed(&self, e: &NotifyCollectionChangedEventArgs<'_, Ref<Page>>) {
337        let logical_children = StyledElement::logical_children(self);
338        match e.action {
339            NotifyCollectionChangedAction::Reset => {
340                logical_children.clear();
341                if let Some(pages) = self.pages() {
342                    for page in pages.snapshot().iter() {
343                        logical_children.add(page.clone().upcast());
344                    }
345                }
346            }
347            _ => {
348                for old in e.old_items {
349                    logical_children.remove(&old.clone().upcast());
350                }
351
352                if !e.new_items.is_empty() {
353                    let mut insert_idx =
354                        if e.new_starting_index >= 0 { e.new_starting_index as usize } else { logical_children.count() };
355                    for new_item in e.new_items {
356                        logical_children.insert(insert_idx, new_item.clone().upcast());
357                        insert_idx += 1;
358                    }
359                }
360            }
361        }
362
363        for (_, handler) in self.pages_changed.snapshot().iter() {
364            handler(e);
365        }
366
367        let mut nav_type = NavigationType::Replace;
368        if let Some(current) = self.current_page() {
369            let mut current_removed = false;
370            if e.action == NotifyCollectionChangedAction::Remove || e.action == NotifyCollectionChangedAction::Replace
371            {
372                current_removed = e.old_items.iter().any(|old| old.ptr_eq(&current));
373            } else if e.action == NotifyCollectionChangedAction::Reset {
374                current_removed = true;
375                if let Some(pages) = self.pages() {
376                    if pages.snapshot().iter().any(|page| page.ptr_eq(&current)) {
377                        current_removed = false;
378                    }
379                }
380            }
381
382            if current_removed {
383                nav_type = NavigationType::Remove;
384            }
385        }
386
387        self.update_active_page_with(nav_type);
388    }
389}
390
391/// A collection of pages as an untyped items list: each page is an item
392/// boxed as a control.
393struct PageItemsList(PageList);
394
395fn box_pages(pages: &[Ref<Page>]) -> Vec<Option<BoxedValue>> {
396    pages.iter().map(|page| Some(Control::boxed(page.clone()))).collect()
397}
398
399impl IItemsList for PageItemsList {
400    fn count(&self) -> usize {
401        self.0.count()
402    }
403
404    fn get_at(&self, index: usize) -> Option<BoxedValue> {
405        Some(Control::boxed(self.0.get(index)))
406    }
407
408    fn index_of(&self, item: &Option<BoxedValue>) -> i32 {
409        let Some(control) = item.as_ref().and_then(Control::from_boxed) else {
410            return -1;
411        };
412        let control: &Control = &control;
413        self.0
414            .snapshot()
415            .iter()
416            .position(|page| {
417                let page: &Control = page;
418                std::ptr::eq(page, control)
419            })
420            .map_or(-1, |index| index as i32)
421    }
422
423    fn is_notifying(&self) -> bool {
424        true
425    }
426
427    fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
428        Some(self.0.add_collection_changed(Rc::new(move |e: &NotifyCollectionChangedEventArgs<'_, Ref<Page>>| {
429            let new_items = box_pages(e.new_items);
430            let old_items = box_pages(e.old_items);
431            handler(&ItemsChangedEventArgs {
432                action: e.action,
433                new_items: ItemsView::from_slice(&new_items),
434                old_items: ItemsView::from_slice(&old_items),
435                new_starting_index: e.new_starting_index,
436                old_starting_index: e.old_starting_index,
437            });
438        })))
439    }
440
441    fn remove_collection_changed(&self, token: u64) {
442        self.0.remove_collection_changed(token);
443    }
444
445    fn as_any(&self) -> &dyn Any {
446        self
447    }
448}