Skip to main content

ferroui_controls/page/
page_navigation_host.rs

1use super::{DefaultPageDataTemplate, NavigatedFromEventArgs, NavigatedToEventArgs, NavigationType, Page};
2use crate::platform::{IInsetsManager, SafeAreaChangedArgs};
3use crate::presenters::ContentPresenter;
4use crate::primitives::{TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt};
5use crate::templates::IDataTemplate;
6use crate::{ContentControl, ContentControlImpl, Control, ControlImpl, TopLevel};
7use ferroui_base::input::InputElementImpl;
8use ferroui_base::interactivity::{InteractiveImpl, RoutedEventArgs, RoutedEventHandlerToken};
9use ferroui_base::layout::LayoutableImpl;
10use ferroui_base::reactive::IDisposable;
11use ferroui_base::{
12    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, FerroObjectImpl,
13    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Nullable, Ref, StaticType, StyledElementImpl,
14    StyledProperty, Thickness, TypeInfo, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
15};
16use std::cell::RefCell;
17use std::rc::Rc;
18
19/// The subscriptions made on the top-level the host is attached to.
20struct TopLevelSubscriptions {
21    top_level: Ref<TopLevel>,
22    back_requested: RoutedEventHandlerToken,
23    scaling_changed: Rc<dyn IDisposable>,
24}
25
26/// The subscription made on the insets manager of the top-level.
27struct InsetsSubscription {
28    insets_manager: Rc<dyn IInsetsManager>,
29    safe_area_changed: Rc<dyn IDisposable>,
30}
31
32/// Hosts a page as the root of a top-level: forwards the safe-area padding
33/// and the system back navigation of the top-level to the page.
34#[repr(C)]
35pub struct PageNavigationHost {
36    base: ContentControl,
37    top_level: RefCell<Option<TopLevelSubscriptions>>,
38    inset_manager: RefCell<Option<InsetsSubscription>>,
39    content_presenter: RefCell<Option<Ref<ContentPresenter>>>,
40    /// The handler added to the property-changed event of the content
41    /// presenter. At most one is ever subscribed: every path that adds one
42    /// removes the previous one first.
43    content_presenter_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
44}
45
46ferro_class!(PageNavigationHost: ContentControl);
47ferro_class_info!(PageNavigationHost { new: PageNavigationHost::new });
48ferro_impl_classes!(
49    PageNavigationHost: LayoutableImpl,
50    InteractiveImpl,
51    InputElementImpl,
52    ControlImpl,
53    ContentControlImpl
54);
55
56impl FerroObjectImpl for PageNavigationHost {
57    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
58        Self::parent_on_property_changed(this, change);
59
60        if change.property() == Self::page_property().as_property() {
61            let (old_page, new_page) = change.get_old_and_new_value::<Option<Ref<Page>>>();
62
63            this.set_current_value(
64                ContentControl::content_property(),
65                new_page.as_ref().map(|page| Control::boxed(page.clone())),
66            );
67
68            if let (Some(inset_manager), Some(new_page)) = (this.inset_manager(), &new_page) {
69                new_page.set_safe_area_padding(inset_manager.safe_area_padding());
70            }
71
72            if let Some(old_page) = &old_page {
73                old_page.send_navigated_from(&NavigatedFromEventArgs::new(new_page.clone(), NavigationType::Replace));
74            }
75            if let Some(new_page) = &new_page {
76                new_page.send_navigated_to(&NavigatedToEventArgs::new(old_page.clone(), NavigationType::Replace));
77            }
78        }
79    }
80}
81
82impl StyledElementImpl for PageNavigationHost {
83    fn style_key_override(_this: &Self) -> &'static TypeInfo {
84        <ContentControl as StaticType>::TYPE
85    }
86}
87
88impl VisualImpl for PageNavigationHost {
89    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
90        Self::parent_on_attached_to_visual_tree(this, e);
91
92        this.clean_up_subscriptions();
93
94        let top_level = TopLevel::get_top_level(Some(this));
95        let inset_manager = top_level.as_ref().and_then(|top_level| top_level.insets_manager());
96
97        if let Some(inset_manager) = inset_manager {
98            let weak = this.to_ref().downgrade();
99            let safe_area_changed = inset_manager.safe_area_changed(Rc::new(move |e| {
100                if let Some(this) = weak.upgrade() {
101                    this.inset_manager_safe_area_changed(e);
102                }
103            }));
104            inset_manager.set_display_edge_to_edge_preference(true);
105            *this.inset_manager.borrow_mut() = Some(InsetsSubscription { insets_manager: inset_manager, safe_area_changed });
106        }
107
108        if let Some(top_level) = top_level {
109            let weak = this.to_ref().downgrade();
110            let back_requested = top_level.back_requested(move |_, e| {
111                if let Some(this) = weak.upgrade() {
112                    this.top_level_back_requested(e);
113                }
114            });
115            let weak = this.to_ref().downgrade();
116            let scaling_changed = top_level.scaling_changed(move || {
117                if let Some(this) = weak.upgrade() {
118                    this.top_level_scaling_changed();
119                }
120            });
121            *this.top_level.borrow_mut() = Some(TopLevelSubscriptions { top_level, back_requested, scaling_changed });
122        }
123
124        this.attach_content_presenter();
125    }
126
127    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
128        Self::parent_on_detached_from_visual_tree(this, e);
129
130        this.clean_up_subscriptions();
131
132        if this.content_presenter.borrow().is_some() {
133            this.remove_content_presenter_property_changed();
134        }
135    }
136}
137
138impl TemplatedControlImpl for PageNavigationHost {
139    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
140        Self::parent_on_apply_template(this, e);
141
142        if this.content_presenter.borrow().is_some() {
143            this.remove_content_presenter_property_changed();
144        }
145
146        *this.content_presenter.borrow_mut() = Some(e.name_scope().get_as::<ContentPresenter>("PART_ContentPresenter"));
147
148        this.attach_content_presenter();
149    }
150}
151
152ferro_properties! {
153    impl PageNavigationHost {
154        /// Defines the `Page` property.
155        pub fn page_property() -> StyledProperty<Option<Ref<Page>>> {
156            FerroProperty::register::<PageNavigationHost, _>("Page", None)
157        }
158    }
159}
160
161impl PageNavigationHost {
162    fn static_constructor() {
163        let template: Rc<dyn IDataTemplate> = DefaultPageDataTemplate::new();
164        ContentControl::content_template_property().override_default_value::<PageNavigationHost>(Some(template));
165        TopLevel::auto_safe_area_padding_property().override_default_value::<PageNavigationHost>(false);
166    }
167
168    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
169    pub fn construct() -> Self {
170        Self {
171            base: ContentControl::construct(),
172            top_level: RefCell::new(None),
173            inset_manager: RefCell::new(None),
174            content_presenter: RefCell::new(None),
175            content_presenter_subscription: RefCell::new(None),
176        }
177    }
178
179    pub fn new() -> Ref<Self> {
180        instantiate(Self::construct())
181    }
182
183    /// The page hosted by the control.
184    pub fn page(&self) -> Option<Ref<Page>> {
185        self.get_value(Self::page_property())
186    }
187
188    pub fn set_page(&self, value: impl Into<Nullable<Page>>) {
189        self.set_value(Self::page_property(), value.into().0)
190    }
191
192    fn inset_manager(&self) -> Option<Rc<dyn IInsetsManager>> {
193        self.inset_manager.borrow().as_ref().map(|subscription| subscription.insets_manager.clone())
194    }
195
196    /// The page that the presenter of the control displays, if its child is
197    /// a page.
198    fn presented_page(presenter: Option<Ref<ContentPresenter>>) -> Option<Ref<Page>> {
199        presenter.and_then(|presenter| presenter.child()).and_then(|child| child.cast::<Page>())
200    }
201
202    fn top_level_scaling_changed(&self) {
203        let content_presenter = self.content_presenter.borrow().clone();
204        if let (Some(inset_manager), Some(page)) = (self.inset_manager(), Self::presented_page(content_presenter)) {
205            page.set_safe_area_padding(inset_manager.safe_area_padding());
206        }
207    }
208
209    fn clean_up_subscriptions(&self) {
210        let inset_manager = self.inset_manager.borrow_mut().take();
211        if let Some(inset_manager) = inset_manager {
212            inset_manager.safe_area_changed.dispose();
213        }
214
215        let top_level = self.top_level.borrow_mut().take();
216        if let Some(top_level) = top_level {
217            top_level.top_level.remove_handler(TopLevel::back_requested_event(), top_level.back_requested);
218            top_level.scaling_changed.dispose();
219        }
220    }
221
222    /// Removes the handler from the property-changed event of the content
223    /// presenter.
224    fn remove_content_presenter_property_changed(&self) {
225        let subscription = self.content_presenter_subscription.borrow_mut().take();
226        if let Some(subscription) = subscription {
227            subscription.dispose();
228        }
229    }
230
231    fn attach_content_presenter(&self) {
232        let Some(content_presenter) = self.content_presenter.borrow().clone() else {
233            return;
234        };
235
236        if let (Some(inset_manager), Some(page)) =
237            (self.inset_manager(), Self::presented_page(Some(content_presenter.clone())))
238        {
239            page.set_safe_area_padding(inset_manager.safe_area_padding());
240        }
241
242        if self.is_attached_to_visual_tree() {
243            let weak = self.to_ref().downgrade();
244            let subscription = content_presenter.property_changed(move |e| {
245                if let Some(this) = weak.upgrade() {
246                    this.content_presenter_property_changed(e);
247                }
248            });
249            let previous = self.content_presenter_subscription.borrow_mut().replace(subscription);
250            if let Some(previous) = previous {
251                previous.dispose();
252            }
253        }
254    }
255
256    fn content_presenter_property_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
257        if e.property() != ContentPresenter::child_property().as_property() {
258            return;
259        }
260
261        let (old_value, new_value) = e.get_old_and_new_value::<Option<Ref<Control>>>();
262
263        if let Some(old_page) = old_value.and_then(|old| old.cast::<Page>()) {
264            old_page.set_safe_area_padding(Thickness::default());
265        }
266
267        if let (Some(new_page), Some(inset_manager)) =
268            (new_value.and_then(|new| new.cast::<Page>()), self.inset_manager())
269        {
270            new_page.set_safe_area_padding(inset_manager.safe_area_padding());
271        }
272    }
273
274    fn top_level_back_requested(&self, e: &RoutedEventArgs) {
275        if e.handled() {
276            return;
277        }
278
279        if let Some(page) = Self::presented_page(self.presenter()) {
280            let forwarded = RoutedEventArgs::with_event(Page::page_navigation_system_back_button_pressed_event());
281            page.raise_event(&forwarded);
282            e.set_handled(forwarded.handled());
283        }
284    }
285
286    fn inset_manager_safe_area_changed(&self, e: &SafeAreaChangedArgs) {
287        if self.content().is_some() {
288            if let Some(page) = Self::presented_page(self.presenter()) {
289                page.set_safe_area_padding(e.safe_area_padding());
290            }
291        }
292    }
293}