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
31struct CarouselPart {
33 carousel: Ref<Carousel>,
34 selection_changed: Option<RoutedEventHandlerToken>,
35 container_prepared: Rc<dyn IDisposable>,
36}
37
38#[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 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 pub fn items_panel_property() -> StyledProperty<Rc<dyn ITemplateOf<Option<Ref<Panel>>>>> {
273 ItemsControl::items_panel_property().add_owner::<CarouselPage>()
274 }
275
276 pub fn page_transition_property() -> StyledProperty<Option<Rc<dyn IPageTransition>>> {
278 FerroProperty::register::<CarouselPage, _>("PageTransition", None)
279 }
280
281 pub fn is_gesture_enabled_property() -> StyledProperty<bool> {
283 FerroProperty::register::<CarouselPage, _>("IsGestureEnabled", true)
284 }
285
286 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 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 pub fn new() -> Ref<Self> {
329 instantiate(Self::construct())
330 }
331
332 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 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 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 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 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}