Skip to main content

ferroui_controls/page/
page.rs

1use super::{INavigation, NavigatedFromEventArgs, NavigatedToEventArgs, NavigatingFromEventArgs};
2use crate::automation::AutomationProperties;
3use crate::i_headered::{register_headered, IHeadered};
4use crate::primitives::{TemplatedControl, TemplatedControlImpl};
5use crate::templates::IDataTemplate;
6use crate::{ControlImpl, ControlImplExt};
7use ferroui_base::input::InputElementImpl;
8use ferroui_base::interactivity::{
9    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
10};
11use ferroui_base::layout::{Layoutable, 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, ferro_routed_event, AnyValue, BoxedValue,
16    DirectProperty, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Nullable, Ref,
17    StyledElementImpl, StyledProperty, StyledPropertyOptions, Thickness, VisualImpl,
18};
19use std::cell::RefCell;
20use std::future::Future;
21use std::pin::Pin;
22use std::rc::Rc;
23
24/// What a handler of the `Navigating` event of a page returns: the rest of
25/// its (possibly asynchronous) work. The navigation awaits it before the
26/// next handler runs.
27pub type NavigatingTask = Pin<Box<dyn Future<Output = ()>>>;
28
29type NavigatingHandler = dyn Fn(&NavigatingFromEventArgs) -> NavigatingTask;
30
31/// Abstract base class for all page types.
32#[repr(C)]
33pub struct Page {
34    base: TemplatedControl,
35    navigation: RefCell<Option<Rc<dyn INavigation>>>,
36    navigated_to: HandlerList<dyn Fn(&NavigatedToEventArgs)>,
37    navigating: HandlerList<NavigatingHandler>,
38    navigated_from: HandlerList<dyn Fn(&NavigatedFromEventArgs)>,
39}
40
41ferro_class! {
42    Page: TemplatedControl, virtuals PageImpl: TemplatedControlImpl {
43        /// Called when the page has been navigated to.
44        fn on_navigated_to(this, args: &NavigatedToEventArgs);
45        /// Called when the page is about to be navigated from.
46        ///
47        /// Setting the cancel flag of the args here prevents the
48        /// `Navigating` asynchronous handlers from running and aborts the
49        /// navigation. This method is called before the `Navigating` event.
50        fn on_navigating_from(this, args: &NavigatingFromEventArgs);
51        /// Called when the page has been navigated from.
52        fn on_navigated_from(this, args: &NavigatedFromEventArgs);
53        /// Called when the system back button is pressed. Returns true if
54        /// the back press was handled.
55        fn on_system_back_button_pressed(this) -> bool;
56        /// Called when the safe-area padding changes.
57        fn update_content_safe_area_padding(this);
58        /// Called when the active child page changes.
59        fn update_active_page(this);
60    }
61}
62
63ferro_class_info!(Page {});
64
65ferro_impl_classes!(
66    Page: StyledElementImpl,
67    VisualImpl,
68    LayoutableImpl,
69    InteractiveImpl,
70    InputElementImpl,
71    TemplatedControlImpl
72);
73
74impl FerroObjectImpl for Page {
75    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
76        Self::parent_on_property_changed(this, change);
77
78        if change.property() == Self::safe_area_padding_property().as_property()
79            || change.property() == TemplatedControl::padding_property().as_property()
80        {
81            this.update_content_safe_area_padding();
82        }
83
84        if change.property() == Self::header_property().as_property() {
85            let header = change.get_new_value::<Option<BoxedValue>>();
86            let name = header.as_ref().and_then(|header| {
87                let header: &dyn AnyValue = &**header;
88                header.downcast_ref::<String>().cloned()
89            });
90            AutomationProperties::set_name(this, Some(name.as_deref().unwrap_or("")));
91        }
92    }
93}
94
95impl ControlImpl for Page {
96    fn on_loaded(this: &Self, e: &RoutedEventArgs) {
97        Self::parent_on_loaded(this, e);
98        this.update_content_safe_area_padding();
99    }
100}
101
102impl PageImpl for Page {
103    fn on_navigated_to(this: &Self, args: &NavigatedToEventArgs) {
104        for (_, handler) in this.navigated_to.snapshot().iter() {
105            handler(args);
106        }
107    }
108
109    fn on_navigating_from(_this: &Self, _args: &NavigatingFromEventArgs) {}
110
111    fn on_navigated_from(this: &Self, args: &NavigatedFromEventArgs) {
112        for (_, handler) in this.navigated_from.snapshot().iter() {
113            handler(args);
114        }
115    }
116
117    fn on_system_back_button_pressed(_this: &Self) -> bool {
118        false
119    }
120
121    fn update_content_safe_area_padding(_this: &Self) {}
122
123    fn update_active_page(_this: &Self) {}
124}
125
126impl IHeadered for Page {
127    fn header(&self) -> Option<BoxedValue> {
128        Page::header(self)
129    }
130
131    fn set_header(&self, value: Option<BoxedValue>) {
132        Page::set_header(self, value)
133    }
134}
135
136ferro_properties! {
137    impl Page {
138        /// Defines the `SafeAreaPadding` property.
139        pub fn safe_area_padding_property() -> StyledProperty<Thickness> {
140            FerroProperty::register_with::<Page, _>(
141                "SafeAreaPadding",
142                StyledPropertyOptions::new(Thickness::default()).validate(|value| {
143                    TemplatedControl::padding_property().validate_value().is_none_or(|validate| validate(value))
144                }),
145            )
146        }
147
148        /// Defines the `Header` property.
149        pub fn header_property() -> StyledProperty<Option<BoxedValue>> {
150            FerroProperty::register::<Page, _>("Header", None)
151        }
152
153        /// Defines the `HeaderTemplate` property.
154        pub fn header_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
155            FerroProperty::register::<Page, _>("HeaderTemplate", None)
156        }
157
158        /// Defines the `Icon` property.
159        pub fn icon_property() -> StyledProperty<Option<BoxedValue>> {
160            FerroProperty::register::<Page, _>("Icon", None)
161        }
162
163        /// Defines the `IconTemplate` property.
164        pub fn icon_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
165            FerroProperty::register::<Page, _>("IconTemplate", None)
166        }
167
168        /// Defines the `CurrentPage` property.
169        pub fn current_page_property() -> StyledProperty<Option<Ref<Page>>> {
170            FerroProperty::register::<Page, _>("CurrentPage", None)
171        }
172
173        /// Defines the `IsInNavigationPage` property.
174        pub fn is_in_navigation_page_property() -> StyledProperty<bool> {
175            FerroProperty::register::<Page, _>("IsInNavigationPage", false)
176        }
177
178        /// Defines the `Navigation` property.
179        pub fn navigation_property() -> DirectProperty<Page, Option<Rc<dyn INavigation>>> {
180            FerroProperty::register_direct::<Page, _>(
181                "Navigation",
182                |o| o.navigation(),
183                Some(|o, v| o.set_navigation(v)),
184                None,
185            )
186        }
187    }
188}
189
190impl Page {
191    ferro_routed_event!(
192        /// Defines the routed event raised when the system back button is
193        /// pressed.
194        pub fn page_navigation_system_back_button_pressed_event() -> RoutedEvent<RoutedEventArgs> {
195            RoutedEvent::register::<Page, _>("PageNavigationSystemBackButtonPressed", RoutingStrategies::DIRECT)
196        }
197    );
198
199    fn static_constructor() {
200        register_headered::<Page>();
201
202        Layoutable::affects_measure::<Page>(&[Self::safe_area_padding_property().as_property()]);
203        Self::page_navigation_system_back_button_pressed_event().add_class_handler::<Page>(|sender, event_args| {
204            if event_args.handled() {
205                return;
206            }
207
208            if sender.on_system_back_button_pressed() {
209                event_args.set_handled(true);
210            }
211        });
212    }
213
214    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
215    pub fn construct() -> Self {
216        Self {
217            base: TemplatedControl::construct(),
218            navigation: RefCell::new(None),
219            navigated_to: HandlerList::new(),
220            navigating: HandlerList::new(),
221            navigated_from: HandlerList::new(),
222        }
223    }
224
225    /// The header content displayed in the navigation bar or tab strip.
226    pub fn header(&self) -> Option<BoxedValue> {
227        self.get_value(Self::header_property())
228    }
229
230    pub fn set_header(&self, value: Option<BoxedValue>) {
231        self.set_value(Self::header_property(), value)
232    }
233
234    /// The data template used to display the header.
235    pub fn header_template(&self) -> Option<Rc<dyn IDataTemplate>> {
236        self.get_value(Self::header_template_property())
237    }
238
239    pub fn set_header_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
240        self.set_value(Self::header_template_property(), value)
241    }
242
243    /// The icon displayed alongside the page header.
244    pub fn icon(&self) -> Option<BoxedValue> {
245        self.get_value(Self::icon_property())
246    }
247
248    pub fn set_icon(&self, value: Option<BoxedValue>) {
249        self.set_value(Self::icon_property(), value)
250    }
251
252    /// The data template used to display the icon.
253    pub fn icon_template(&self) -> Option<Rc<dyn IDataTemplate>> {
254        self.get_value(Self::icon_template_property())
255    }
256
257    pub fn set_icon_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
258        self.set_value(Self::icon_template_property(), value)
259    }
260
261    /// The safe-area padding applied to this page's content.
262    pub fn safe_area_padding(&self) -> Thickness {
263        self.get_value(Self::safe_area_padding_property())
264    }
265
266    pub fn set_safe_area_padding(&self, value: Thickness) {
267        self.set_value(Self::safe_area_padding_property(), value)
268    }
269
270    /// The currently active child page.
271    pub fn current_page(&self) -> Option<Ref<Page>> {
272        self.get_value(Self::current_page_property())
273    }
274
275    pub fn set_current_page(&self, value: impl Into<Nullable<Page>>) {
276        self.set_value(Self::current_page_property(), value.into().0)
277    }
278
279    /// The navigation service provided by the parent navigation page.
280    pub fn navigation(&self) -> Option<Rc<dyn INavigation>> {
281        self.navigation.borrow().clone()
282    }
283
284    pub fn set_navigation(&self, value: Option<Rc<dyn INavigation>>) {
285        self.set_and_raise(Self::navigation_property(), &self.navigation, value);
286    }
287
288    /// Whether this page is currently hosted inside a navigation page.
289    pub fn is_in_navigation_page(&self) -> bool {
290        self.get_value(Self::is_in_navigation_page_property())
291    }
292
293    pub fn set_is_in_navigation_page(&self, value: bool) {
294        self.set_value(Self::is_in_navigation_page_property(), value)
295    }
296
297    /// Raised when the system back button is pressed while this page is
298    /// active.
299    pub fn page_navigation_system_back_button_pressed(
300        &self,
301        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
302    ) -> RoutedEventHandlerToken {
303        self.add_handler(Self::page_navigation_system_back_button_pressed_event(), handler)
304    }
305
306    fn subscribe<F: ?Sized + 'static>(
307        &self,
308        select: fn(&Page) -> &HandlerList<F>,
309        handler: Rc<F>,
310    ) -> Rc<dyn IDisposable> {
311        let token = select(self).add(handler);
312        let weak = self.to_ref().downgrade();
313        Disposable::create(move || {
314            if let Some(this) = weak.upgrade() {
315                select(&this).remove(token);
316            }
317        })
318    }
319
320    /// Occurs when the page has been navigated to. Disposing the returned
321    /// handle unsubscribes.
322    pub fn navigated_to(&self, handler: impl Fn(&NavigatedToEventArgs) + 'static) -> Rc<dyn IDisposable> {
323        self.subscribe::<dyn Fn(&NavigatedToEventArgs)>(|page| &page.navigated_to, Rc::new(handler))
324    }
325
326    /// Occurs when the page is about to be navigated from. Disposing the
327    /// returned handle unsubscribes.
328    ///
329    /// Each subscriber is awaited in turn. Set the cancel flag of the args
330    /// to abort the navigation; remaining subscribers are not invoked once
331    /// cancellation is requested. If a subscriber panics, the panic
332    /// propagates to the task of the calling navigation method (such as the
333    /// push of a navigation page) and the navigation is aborted.
334    pub fn navigating(
335        &self,
336        handler: impl Fn(&NavigatingFromEventArgs) -> NavigatingTask + 'static,
337    ) -> Rc<dyn IDisposable> {
338        self.subscribe::<NavigatingHandler>(|page| &page.navigating, Rc::new(handler))
339    }
340
341    /// Occurs when the page has been navigated from. Disposing the returned
342    /// handle unsubscribes.
343    pub fn navigated_from(&self, handler: impl Fn(&NavigatedFromEventArgs) + 'static) -> Rc<dyn IDisposable> {
344        self.subscribe::<dyn Fn(&NavigatedFromEventArgs)>(|page| &page.navigated_from, Rc::new(handler))
345    }
346
347    pub(crate) fn send_navigated_to(&self, args: &NavigatedToEventArgs) {
348        self.on_navigated_to(args)
349    }
350
351    /// Asks the page whether it may be navigated from: the virtual first,
352    /// then each `Navigating` handler in turn, stopping at the first one
353    /// that cancels.
354    pub(crate) async fn send_navigating_async(this: Ref<Page>, args: NavigatingFromEventArgs) {
355        this.on_navigating_from(&args);
356
357        if args.cancel() {
358            return;
359        }
360
361        let navigating = this.navigating.snapshot();
362        for (_, handler) in navigating.iter() {
363            handler(&args).await;
364            if args.cancel() {
365                return;
366            }
367        }
368    }
369
370    pub(crate) fn send_navigated_from(&self, args: &NavigatedFromEventArgs) {
371        self.on_navigated_from(args)
372    }
373
374    pub(crate) fn set_in_navigation_page(&self, value: bool) {
375        self.set_current_value(Self::is_in_navigation_page_property(), value)
376    }
377}