Skip to main content

ferroui_controls/page/
content_page.rs

1use super::safe_area_padding_extensions::SafeAreaPaddingExtensions;
2use super::{Page, PageImpl};
3use crate::presenters::ContentPresenter;
4use crate::primitives::{
5    TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt,
6};
7use crate::templates::IDataTemplate;
8use crate::{ContentControl, Control, ControlImpl};
9use ferroui_base::input::InputElementImpl;
10use ferroui_base::interactivity::InteractiveImpl;
11use ferroui_base::layout::{HorizontalAlignment, LayoutableImpl, VerticalAlignment};
12use ferroui_base::{
13    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, BoxedValue, FerroObject,
14    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StaticType, StyledElement,
15    StyledElementImpl, StyledProperty, StyledPropertyMetadata, TypeInfo, VisualImpl,
16};
17use std::cell::RefCell;
18use std::rc::Rc;
19
20/// A page that displays a single piece of content with optional top and
21/// bottom command bars.
22#[repr(C)]
23pub struct ContentPage {
24    base: Page,
25    content_presenter: RefCell<Option<Ref<ContentPresenter>>>,
26    top_command_bar_presenter: RefCell<Option<Ref<ContentPresenter>>>,
27    bottom_command_bar_presenter: RefCell<Option<Ref<ContentPresenter>>>,
28}
29
30ferro_class!(ContentPage: Page);
31
32ferro_class_info!(ContentPage {
33    new: ContentPage::new,
34    markup: {
35        // XAML-SEAM: the `Content` property depends on `ContentTemplate` (markup assignment order).
36        content: Content,
37        property_attributes: [Content: [DependsOn("ContentTemplate")]],
38        attributes: [
39            TemplatePart("PART_ContentPresenter", type(Ref<ContentPresenter>)),
40            TemplatePart("PART_TopCommandBar", type(Ref<ContentPresenter>)),
41            TemplatePart("PART_BottomCommandBar", type(Ref<ContentPresenter>)),
42        ],
43    },
44});
45
46ferro_impl_classes!(ContentPage: VisualImpl, LayoutableImpl, InteractiveImpl, InputElementImpl);
47
48impl FerroObjectImpl for ContentPage {
49    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
50        Self::parent_on_property_changed(this, change);
51
52        if change.property() == Self::automatically_apply_safe_area_padding_property().as_property()
53            || change.property() == Self::content_property().as_property()
54        {
55            this.update_content_safe_area_padding();
56        } else if change.property() == Self::top_command_bar_property().as_property()
57            || change.property() == Self::bottom_command_bar_property().as_property()
58        {
59            this.update_command_bars();
60        }
61    }
62}
63
64impl StyledElementImpl for ContentPage {
65    fn style_key_override(_this: &Self) -> &'static TypeInfo {
66        <ContentPage as StaticType>::TYPE
67    }
68}
69
70impl TemplatedControlImpl for ContentPage {
71    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
72        Self::parent_on_apply_template(this, e);
73
74        *this.content_presenter.borrow_mut() = Some(e.name_scope().get_as::<ContentPresenter>("PART_ContentPresenter"));
75        *this.top_command_bar_presenter.borrow_mut() = e.name_scope().find_as::<ContentPresenter>("PART_TopCommandBar");
76        *this.bottom_command_bar_presenter.borrow_mut() =
77            e.name_scope().find_as::<ContentPresenter>("PART_BottomCommandBar");
78
79        this.update_command_bars();
80        this.update_content_safe_area_padding();
81    }
82}
83
84impl PageImpl for ContentPage {
85    fn update_content_safe_area_padding(this: &Self) {
86        let content_presenter = this.content_presenter.borrow().clone();
87        if let Some(content_presenter) = content_presenter {
88            content_presenter.set_padding(if this.automatically_apply_safe_area_padding() {
89                this.padding().apply_safe_area_padding(this.safe_area_padding())
90            } else {
91                this.padding()
92            });
93            content_presenter.invalidate_measure();
94        }
95    }
96}
97
98impl ControlImpl for ContentPage {
99    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
100        crate::automation::peers::ContentPageAutomationPeer::new(this).upcast()
101    }
102}
103
104ferro_properties! {
105    impl ContentPage {
106        /// Defines the `Content` property.
107        pub fn content_property() -> StyledProperty<Option<BoxedValue>> {
108            ContentControl::content_property()
109                .add_owner_with::<ContentPage>(StyledPropertyMetadata::new(None).with_coerce(ContentPage::coerce_content))
110        }
111
112        /// Defines the `ContentTemplate` property.
113        pub fn content_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
114            ContentControl::content_template_property().add_owner::<ContentPage>()
115        }
116
117        /// Defines the `AutomaticallyApplySafeAreaPadding` property.
118        pub fn automatically_apply_safe_area_padding_property() -> StyledProperty<bool> {
119            FerroProperty::register::<ContentPage, _>("AutomaticallyApplySafeAreaPadding", true)
120        }
121
122        /// Defines the `TopCommandBar` property.
123        pub fn top_command_bar_property() -> StyledProperty<Option<BoxedValue>> {
124            FerroProperty::register::<ContentPage, _>("TopCommandBar", None)
125        }
126
127        /// Defines the `BottomCommandBar` property.
128        pub fn bottom_command_bar_property() -> StyledProperty<Option<BoxedValue>> {
129            FerroProperty::register::<ContentPage, _>("BottomCommandBar", None)
130        }
131
132        /// Defines the `HorizontalContentAlignment` property.
133        pub fn horizontal_content_alignment_property() -> StyledProperty<HorizontalAlignment> {
134            ContentControl::horizontal_content_alignment_property().add_owner::<ContentPage>()
135        }
136
137        /// Defines the `VerticalContentAlignment` property.
138        pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
139            ContentControl::vertical_content_alignment_property().add_owner::<ContentPage>()
140        }
141    }
142}
143
144impl ContentPage {
145    fn static_constructor() {
146        Self::content_property().changed().add_class_handler::<ContentPage>(|x, e| x.content_changed(e));
147    }
148
149    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
150    pub fn construct() -> Self {
151        Self {
152            base: Page::construct(),
153            content_presenter: RefCell::new(None),
154            top_command_bar_presenter: RefCell::new(None),
155            bottom_command_bar_presenter: RefCell::new(None),
156        }
157    }
158
159    pub fn new() -> Ref<Self> {
160        instantiate(Self::construct())
161    }
162
163    /// A page cannot be the content of a content page.
164    fn coerce_content(_instance: &FerroObject, value: Option<BoxedValue>) -> Option<BoxedValue> {
165        let is_page =
166            value.as_ref().and_then(Control::from_boxed).is_some_and(|control| control.is::<Page>());
167        if is_page {
168            panic!(
169                "A Page cannot be used as the content of a ContentPage. Use a MultiPage subclass such as NavigationPage, TabbedPage, DrawerPage, or CarouselPage to host child pages."
170            );
171        }
172        value
173    }
174
175    /// The horizontal alignment of the content within the page.
176    pub fn horizontal_content_alignment(&self) -> HorizontalAlignment {
177        self.get_value(Self::horizontal_content_alignment_property())
178    }
179
180    pub fn set_horizontal_content_alignment(&self, value: HorizontalAlignment) {
181        self.set_value(Self::horizontal_content_alignment_property(), value)
182    }
183
184    /// The vertical alignment of the content within the page.
185    pub fn vertical_content_alignment(&self) -> VerticalAlignment {
186        self.get_value(Self::vertical_content_alignment_property())
187    }
188
189    pub fn set_vertical_content_alignment(&self, value: VerticalAlignment) {
190        self.set_value(Self::vertical_content_alignment_property(), value)
191    }
192
193    /// The page content.
194    ///
195    /// The content must not be another page. Use a page that hosts child
196    /// pages (a navigation page, tabbed page, drawer page or carousel page)
197    /// for that.
198    pub fn content(&self) -> Option<BoxedValue> {
199        self.get_value(Self::content_property())
200    }
201
202    /// Sets the page content.
203    ///
204    /// # Panics
205    /// Panics when the assigned value is a page.
206    pub fn set_content(&self, value: Option<BoxedValue>) {
207        self.set_value(Self::content_property(), value)
208    }
209
210    /// The data template used to display the content.
211    pub fn content_template(&self) -> Option<Rc<dyn IDataTemplate>> {
212        self.get_value(Self::content_template_property())
213    }
214
215    pub fn set_content_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
216        self.set_value(Self::content_template_property(), value)
217    }
218
219    /// Whether safe-area padding is automatically applied to the content
220    /// presenter.
221    pub fn automatically_apply_safe_area_padding(&self) -> bool {
222        self.get_value(Self::automatically_apply_safe_area_padding_property())
223    }
224
225    pub fn set_automatically_apply_safe_area_padding(&self, value: bool) {
226        self.set_value(Self::automatically_apply_safe_area_padding_property(), value)
227    }
228
229    /// The content displayed in the top command bar area.
230    pub fn top_command_bar(&self) -> Option<BoxedValue> {
231        self.get_value(Self::top_command_bar_property())
232    }
233
234    pub fn set_top_command_bar(&self, value: Option<BoxedValue>) {
235        self.set_value(Self::top_command_bar_property(), value)
236    }
237
238    /// The content displayed in the bottom command bar area.
239    pub fn bottom_command_bar(&self) -> Option<BoxedValue> {
240        self.get_value(Self::bottom_command_bar_property())
241    }
242
243    pub fn set_bottom_command_bar(&self, value: Option<BoxedValue>) {
244        self.set_value(Self::bottom_command_bar_property(), value)
245    }
246
247    fn content_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
248        let (old_value, new_value) = e.get_old_and_new_value::<Option<BoxedValue>>();
249
250        if let Some(old_child) = old_value.as_ref().and_then(Control::logical_from_boxed) {
251            StyledElement::logical_children(self).remove(&old_child);
252        }
253
254        if let Some(new_child) = new_value.as_ref().and_then(Control::logical_from_boxed) {
255            StyledElement::logical_children(self).add(new_child);
256        }
257    }
258
259    fn update_command_bars(&self) {
260        let top_command_bar_presenter = self.top_command_bar_presenter.borrow().clone();
261        if let Some(presenter) = top_command_bar_presenter {
262            let top_command_bar = self.top_command_bar();
263            presenter.set_content(top_command_bar.clone());
264            presenter.set_is_visible(top_command_bar.is_some());
265        }
266
267        let bottom_command_bar_presenter = self.bottom_command_bar_presenter.borrow().clone();
268        if let Some(presenter) = bottom_command_bar_presenter {
269            let bottom_command_bar = self.bottom_command_bar();
270            presenter.set_content(bottom_command_bar.clone());
271            presenter.set_is_visible(bottom_command_bar.is_some());
272        }
273    }
274}