Skip to main content

ferroui_controls/page/
carousel_page.rs

1use super::selecting_multi_page::same_page;
2use super::{
3    MultiPage, MultiPageImpl, NavigationType, Page, PageImpl, PageList, SelectingMultiPage, SelectingMultiPageImpl,
4};
5use crate::automation::AutomationProperties;
6use crate::presenters::ContentPresenter;
7use crate::primitives::{SelectingItemsControl, TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt};
8use crate::templates::{FuncTemplate, IDataTemplate, ITemplateOf};
9use crate::{
10    Carousel, ContainerPreparedEventArgs, ContentControl, Control, ControlImpl, ItemsControl, ItemsSource, Panel,
11    VirtualizingCarouselPanel,
12};
13use ferroui_base::animation::IPageTransition;
14use ferroui_base::data::core::ValueTypes;
15use ferroui_base::input::{
16    InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, PointerWheelEventArgs,
17};
18use ferroui_base::interactivity::{InteractiveImpl, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies};
19use ferroui_base::layout::LayoutableImpl;
20use ferroui_base::media::FlowDirection;
21use ferroui_base::reactive::IDisposable;
22use ferroui_base::threading::{Dispatcher, DispatcherPriority};
23use ferroui_base::{
24    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, FerroObjectImpl,
25    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StaticType, StyledElementImpl,
26    StyledProperty, TypeInfo, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
27};
28use std::cell::{Cell, RefCell};
29use std::rc::Rc;
30
31/// The carousel of the template and the handlers added to it.
32struct CarouselPart {
33    carousel: Ref<Carousel>,
34    selection_changed: Option<RoutedEventHandlerToken>,
35    container_prepared: Rc<dyn IDisposable>,
36}
37
38/// A page that displays its child pages in a horizontally scrollable
39/// carousel, with optional animated page transitions.
40#[repr(C)]
41pub struct CarouselPage {
42    base: SelectingMultiPage,
43    carousel: RefCell<Option<CarouselPart>>,
44    pointer_wheel_handler: Cell<Option<RoutedEventHandlerToken>>,
45}
46
47ferro_class!(CarouselPage: SelectingMultiPage);
48
49ferro_class_info!(CarouselPage {
50    new: CarouselPage::new,
51    markup: {
52        attributes: [
53            TemplatePart("PART_Carousel", type(Ref<Carousel>)),
54        ],
55    },
56});
57
58ferro_impl_classes!(CarouselPage: LayoutableImpl, InteractiveImpl, PageImpl);
59
60impl FerroObjectImpl for CarouselPage {
61    fn constructed(this: &Self) {
62        Self::parent_constructed(this);
63
64        this.set_current_value(MultiPage::pages_property(), Some(PageList::new()));
65    }
66
67    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
68        Self::parent_on_property_changed(this, change);
69
70        let Some(carousel) = this.carousel() else {
71            return;
72        };
73
74        if change.property() == Self::page_transition_property().as_property() {
75            carousel.set_page_transition(change.get_new_value::<Option<Rc<dyn IPageTransition>>>());
76        } else if change.property() == Self::items_panel_property().as_property() {
77            carousel.set_items_panel(change.get_new_value::<Rc<dyn ITemplateOf<Option<Ref<Panel>>>>>());
78        } else if change.property() == MultiPage::page_template_property().as_property() {
79            carousel.set_item_template(change.get_new_value::<Option<Rc<dyn IDataTemplate>>>());
80            if this.items_source().is_some() {
81                this.update_active_page();
82            }
83        } else if change.property() == Self::is_gesture_enabled_property().as_property() {
84            carousel.set_is_swipe_enabled(change.get_new_value::<bool>());
85        } else if change.property() == MultiPage::items_source_property().as_property() {
86            carousel
87                .set_items_source(change.get_new_value::<Option<ItemsSource>>().or_else(|| this.pages_items_source()));
88            this.update_active_page();
89        } else if change.property() == MultiPage::pages_property().as_property() && this.items_source().is_none() {
90            carousel.set_items_source(
91                change.get_new_value::<Option<PageList>>().as_ref().map(MultiPage::items_source_of),
92            );
93        }
94    }
95}
96
97impl StyledElementImpl for CarouselPage {
98    fn style_key_override(_this: &Self) -> &'static TypeInfo {
99        <CarouselPage as StaticType>::TYPE
100    }
101}
102
103impl VisualImpl for CarouselPage {
104    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
105        Self::parent_on_attached_to_visual_tree(this, e);
106
107        let weak = this.to_ref().downgrade();
108        let token = this.add_handler_with(
109            InputElement::pointer_wheel_changed_event(),
110            move |_, e: &PointerWheelEventArgs| {
111                if let Some(this) = weak.upgrade() {
112                    this.on_pointer_wheel_changed(e);
113                }
114            },
115            RoutingStrategies::BUBBLE,
116            false,
117        );
118        if let Some(previous) = this.pointer_wheel_handler.replace(Some(token)) {
119            this.remove_handler(InputElement::pointer_wheel_changed_event(), previous);
120        }
121    }
122
123    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
124        Self::parent_on_detached_from_visual_tree(this, e);
125
126        if let Some(token) = this.pointer_wheel_handler.take() {
127            this.remove_handler(InputElement::pointer_wheel_changed_event(), token);
128        }
129    }
130}
131
132impl InputElementImpl for CarouselPage {
133    fn on_key_down(this: &Self, e: &KeyEventArgs) {
134        Self::parent_on_key_down(this, e);
135        if e.handled() || !this.is_keyboard_navigation_enabled() {
136            return;
137        }
138
139        let is_rtl = this.is_right_to_left();
140        let next = if is_rtl { e.key == Key::Left } else { e.key == Key::Right };
141        let prev = if is_rtl { e.key == Key::Right } else { e.key == Key::Left };
142
143        let page_count = this.get_page_count();
144        if next || e.key == Key::Down {
145            if this.selected_index() < page_count - 1 {
146                this.apply_selected_index(this.selected_index() + 1);
147                e.set_handled(true);
148            }
149        } else if prev || e.key == Key::Up {
150            if this.selected_index() > 0 {
151                this.apply_selected_index(this.selected_index() - 1);
152                e.set_handled(true);
153            }
154        } else if e.key == Key::Home {
155            if page_count > 0 && this.selected_index() != 0 {
156                this.apply_selected_index(0);
157                e.set_handled(true);
158            }
159        } else if e.key == Key::End && page_count > 0 && this.selected_index() != page_count - 1 {
160            this.apply_selected_index(page_count - 1);
161            e.set_handled(true);
162        }
163    }
164}
165
166impl TemplatedControlImpl for CarouselPage {
167    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
168        Self::parent_on_apply_template(this, e);
169
170        let requested_index = this.selected_index();
171
172        let previous = this.carousel.borrow_mut().take();
173        if let Some(previous) = previous {
174            if let Some(token) = previous.selection_changed {
175                previous.carousel.remove_handler(SelectingItemsControl::selection_changed_event(), token);
176            }
177            previous.container_prepared.dispose();
178        }
179
180        let carousel = e.name_scope().find_as::<Carousel>("PART_Carousel");
181
182        if let Some(carousel) = carousel {
183            let weak = this.to_ref().downgrade();
184            let container_prepared = carousel.container_prepared(move |e| {
185                if let Some(this) = weak.upgrade() {
186                    this.on_carousel_container_prepared(e);
187                }
188            });
189            *this.carousel.borrow_mut() =
190                Some(CarouselPart { carousel: carousel.clone(), selection_changed: None, container_prepared });
191            carousel.set_page_transition(this.page_transition());
192            carousel.set_items_panel(this.items_panel());
193            carousel.set_item_template(this.page_template());
194            carousel.set_is_swipe_enabled(this.is_gesture_enabled());
195            carousel.set_items_source(this.items_source().or_else(|| this.pages_items_source()));
196
197            if requested_index >= 0 {
198                carousel.set_selected_index(requested_index);
199            }
200
201            let weak = this.to_ref().downgrade();
202            let token = carousel.selection_changed(move |_, _| {
203                if let Some(this) = weak.upgrade() {
204                    this.on_carousel_selection_changed();
205                }
206            });
207            if let Some(part) = this.carousel.borrow_mut().as_mut() {
208                part.selection_changed = Some(token);
209            }
210
211            this.update_active_page();
212        }
213    }
214}
215
216impl MultiPageImpl for CarouselPage {
217    fn update_active_page_with(this: &Self, navigation_type: NavigationType) {
218        if let Some(carousel) = this.carousel() {
219            let index = carousel.selected_index();
220            if index >= 0 {
221                this.update_selection(index, navigation_type);
222            } else if this.get_page_count() > 0 {
223                this.apply_selected_index(0);
224            }
225        } else if this.get_page_count() > 0 {
226            let index = this.coerce_pre_template_selected_index(this.selected_index());
227            if this.items_source().is_some() {
228                this.store_selected_index(index);
229            } else {
230                let page = this.resolve_page_at_index(index);
231
232                if index != this.selected_index() || !same_page(&this.selected_page(), &page) {
233                    this.commit_selection(index, page, navigation_type);
234                }
235            }
236        }
237    }
238}
239
240impl SelectingMultiPageImpl for CarouselPage {
241    fn apply_selected_index(this: &Self, index: i32) {
242        if let Some(carousel) = this.carousel() {
243            carousel.set_selected_index(index);
244        } else {
245            let page_count = this.get_page_count();
246
247            if page_count > 0 {
248                let coerced_index = this.coerce_pre_template_selected_index(index);
249                if this.items_source().is_some() {
250                    this.store_selected_index(coerced_index);
251                } else {
252                    let new_page = this.resolve_page_at_index(coerced_index);
253                    this.commit_selection(coerced_index, new_page, NavigationType::Replace);
254                }
255            } else {
256                // Preserve preselection until pages exist.
257                this.store_selected_index(index);
258            }
259        }
260    }
261}
262
263impl ControlImpl for CarouselPage {
264    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
265        crate::automation::peers::CarouselPageAutomationPeer::new(this).upcast()
266    }
267}
268
269ferro_properties! {
270    impl CarouselPage {
271        /// Defines the `ItemsPanel` property.
272        pub fn items_panel_property() -> StyledProperty<Rc<dyn ITemplateOf<Option<Ref<Panel>>>>> {
273            ItemsControl::items_panel_property().add_owner::<CarouselPage>()
274        }
275
276        /// Defines the `PageTransition` property.
277        pub fn page_transition_property() -> StyledProperty<Option<Rc<dyn IPageTransition>>> {
278            FerroProperty::register::<CarouselPage, _>("PageTransition", None)
279        }
280
281        /// Defines the `IsGestureEnabled` property.
282        pub fn is_gesture_enabled_property() -> StyledProperty<bool> {
283            FerroProperty::register::<CarouselPage, _>("IsGestureEnabled", true)
284        }
285
286        /// Defines the `IsKeyboardNavigationEnabled` property.
287        pub fn is_keyboard_navigation_enabled_property() -> StyledProperty<bool> {
288            FerroProperty::register::<CarouselPage, _>("IsKeyboardNavigationEnabled", true)
289        }
290    }
291}
292
293impl CarouselPage {
294    fn static_constructor() {
295        let default_panel: Rc<dyn ITemplateOf<Option<Ref<Panel>>>> =
296            FuncTemplate::new(|| Some(VirtualizingCarouselPanel::new().upcast::<Panel>()));
297
298        Self::items_panel_property().override_default_value::<CarouselPage>(default_panel);
299        InputElement::focusable_property().override_default_value::<CarouselPage>(true);
300        Page::page_navigation_system_back_button_pressed_event().add_class_handler::<CarouselPage>(
301            |sender, event_args| {
302                if event_args.handled() {
303                    return;
304                }
305
306                let page_event = RoutedEventArgs::with_event(Page::page_navigation_system_back_button_pressed_event());
307                if let Some(current_page) = sender.current_page() {
308                    current_page.raise_event(&page_event);
309                }
310
311                if page_event.handled() {
312                    event_args.set_handled(true);
313                }
314            },
315        );
316    }
317
318    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
319    pub fn construct() -> Self {
320        Self {
321            base: SelectingMultiPage::construct(),
322            carousel: RefCell::new(None),
323            pointer_wheel_handler: Cell::new(None),
324        }
325    }
326
327    /// Initializes a new instance of the [`CarouselPage`] class.
328    pub fn new() -> Ref<Self> {
329        instantiate(Self::construct())
330    }
331
332    /// Gets or sets the items panel template used to arrange page items.
333    pub fn items_panel(&self) -> Rc<dyn ITemplateOf<Option<Ref<Panel>>>> {
334        self.get_value(Self::items_panel_property())
335    }
336
337    pub fn set_items_panel(&self, value: Rc<dyn ITemplateOf<Option<Ref<Panel>>>>) {
338        self.set_value(Self::items_panel_property(), value)
339    }
340
341    /// Gets or sets the animated page transition used when the selected
342    /// page changes.
343    pub fn page_transition(&self) -> Option<Rc<dyn IPageTransition>> {
344        self.get_value(Self::page_transition_property())
345    }
346
347    pub fn set_page_transition(&self, value: Option<Rc<dyn IPageTransition>>) {
348        self.set_value(Self::page_transition_property(), value)
349    }
350
351    /// Gets or sets whether swipe and scroll gestures can be used to
352    /// navigate between pages.
353    pub fn is_gesture_enabled(&self) -> bool {
354        self.get_value(Self::is_gesture_enabled_property())
355    }
356
357    pub fn set_is_gesture_enabled(&self, value: bool) {
358        self.set_value(Self::is_gesture_enabled_property(), value)
359    }
360
361    /// Gets or sets whether keyboard shortcuts (arrow keys, Home/End) can
362    /// be used to navigate between pages.
363    pub fn is_keyboard_navigation_enabled(&self) -> bool {
364        self.get_value(Self::is_keyboard_navigation_enabled_property())
365    }
366
367    pub fn set_is_keyboard_navigation_enabled(&self, value: bool) {
368        self.set_value(Self::is_keyboard_navigation_enabled_property(), value)
369    }
370
371    fn carousel(&self) -> Option<Ref<Carousel>> {
372        self.carousel.borrow().as_ref().map(|part| part.carousel.clone())
373    }
374
375    fn on_carousel_selection_changed(&self) {
376        let Some(carousel) = self.carousel() else {
377            return;
378        };
379
380        let new_index = carousel.selected_index();
381        let new_page = self.resolve_displayed_page_at_index(new_index);
382        if new_index == self.selected_index() && same_page(&new_page, &self.selected_page()) {
383            return;
384        }
385
386        self.update_selection(new_index, NavigationType::Replace);
387    }
388
389    fn on_carousel_container_prepared(&self, e: &ContainerPreparedEventArgs) {
390        let Some(carousel) = self.carousel() else {
391            return;
392        };
393        if e.index() != carousel.selected_index() {
394            return;
395        }
396
397        let weak = self.to_ref().downgrade();
398        let index = e.index();
399        Dispatcher::ui_thread().post_local(
400            move || {
401                let Some(this) = weak.upgrade() else {
402                    return;
403                };
404                if this.carousel().is_some_and(|carousel| carousel.selected_index() == index) {
405                    this.update_selection(index, NavigationType::Replace);
406                }
407            },
408            DispatcherPriority::LOADED,
409        );
410    }
411
412    fn on_pointer_wheel_changed(&self, e: &PointerWheelEventArgs) {
413        if !self.is_gesture_enabled() {
414            return;
415        }
416
417        let is_rtl = self.is_right_to_left();
418        let page_count = self.get_page_count();
419        let delta = e.delta();
420        let go_next = delta.y < 0.0 || if is_rtl { delta.x < 0.0 } else { delta.x > 0.0 };
421        let go_prev = delta.y > 0.0 || if is_rtl { delta.x > 0.0 } else { delta.x < 0.0 };
422
423        if go_next && self.selected_index() < page_count - 1 {
424            self.apply_selected_index(self.selected_index() + 1);
425            e.set_handled(true);
426        } else if go_prev && self.selected_index() > 0 {
427            self.apply_selected_index(self.selected_index() - 1);
428            e.set_handled(true);
429        }
430    }
431
432    fn update_accessibility_name(&self, index: i32, page_count: i32, page: Option<&Ref<Page>>) {
433        let header =
434            page.and_then(|page| page.header()).map(|header| ValueTypes::to_display_string(Some(&header)));
435        let position = if page_count > 0 { format!("Page {} of {}", index + 1, page_count) } else { String::new() };
436        let name = match header {
437            None => position,
438            Some(header) if header.is_empty() => position,
439            Some(header) if position.is_empty() => header,
440            Some(header) => format!("{position}: {header}"),
441        };
442        // CarouselPageAutomationPeer::get_name_core reads this via the base implementation, which returns
443        // the automation name when set. Position and header are encoded here rather than in the
444        // peer so that the name stays current without requiring the peer to re-query the carousel state.
445        AutomationProperties::set_name(self, Some(&name));
446    }
447
448    fn is_right_to_left(&self) -> bool {
449        self.flow_direction() == FlowDirection::RightToLeft
450    }
451
452    fn get_page_count(&self) -> i32 {
453        if let Some(carousel) = self.carousel() {
454            return carousel.item_count();
455        }
456
457        if let Some(source) = self.items_source() {
458            return source.count() as i32;
459        }
460
461        self.pages().map_or(0, |pages| pages.count() as i32)
462    }
463
464    fn coerce_pre_template_selected_index(&self, index: i32) -> i32 {
465        let page_count = self.get_page_count();
466        if page_count <= 0 {
467            return index;
468        }
469
470        if index >= 0 && index < page_count {
471            index
472        } else {
473            0
474        }
475    }
476
477    fn update_selection(&self, index: i32, navigation_type: NavigationType) {
478        let page = self.resolve_displayed_page_at_index(index);
479        let page_count = self.get_page_count();
480
481        if page.is_none() && self.items_source().is_some() {
482            self.store_selected_index(index);
483            self.update_accessibility_name(index, page_count, None);
484            return;
485        }
486
487        self.commit_selection(index, page.clone(), navigation_type);
488        self.update_accessibility_name(index, page_count, page.as_ref());
489    }
490
491    fn resolve_displayed_page_at_index(&self, index: i32) -> Option<Ref<Page>> {
492        if index < 0 {
493            return None;
494        }
495
496        if let Some(container) = self.carousel().and_then(|carousel| carousel.container_from_index(index)) {
497            return Self::try_get_page_from_container(&container);
498        }
499
500        if self.items_source().is_none() {
501            self.resolve_page_at_index(index)
502        } else {
503            None
504        }
505    }
506
507    fn try_get_page_from_container(container: &Ref<Control>) -> Option<Ref<Page>> {
508        if let Some(page) = container.clone().cast::<Page>() {
509            return Some(page);
510        }
511
512        if let Some(presenter) = container.clone().cast::<ContentPresenter>() {
513            if presenter.child().is_none() {
514                presenter.update_child();
515            }
516
517            return presenter.child().and_then(|child| child.cast::<Page>());
518        }
519
520        if let Some(content_control) = container.clone().cast::<ContentControl>() {
521            return content_control
522                .content()
523                .as_ref()
524                .and_then(Control::from_boxed)
525                .and_then(|content| content.cast::<Page>());
526        }
527
528        None
529    }
530}