Skip to main content

ferroui_controls/page/
tabbed_page.rs

1use super::{
2    MultiPage, MultiPageImpl, NavigationType, Page, PageImpl, PageImplExt, SelectingMultiPage, SelectingMultiPageImpl,
3    TabPlacement,
4};
5use crate::primitives::{SelectingItemsControl, TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt};
6use crate::templates::IDataTemplate;
7use crate::{
8    ContainerClearingEventArgs, ContainerIndexChangedEventArgs, ContainerPreparedEventArgs, Control, ControlImpl, Dock,
9    ItemsSource, TabControl, TabItem,
10};
11use ferroui_base::animation::IPageTransition;
12use ferroui_base::input::gesture_recognizers::SwipeGestureRecognizer;
13use ferroui_base::input::{
14    InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, KeyModifiers, NavigationMethod,
15    SwipeDirection, SwipeGestureEventArgs,
16};
17use ferroui_base::interactivity::{InteractiveImpl, RoutedEventArgs, RoutedEventHandlerToken};
18use ferroui_base::layout::LayoutableImpl;
19use ferroui_base::media::FlowDirection;
20use ferroui_base::reactive::IDisposable;
21use ferroui_base::threading::{Dispatcher, DispatcherPriority};
22use ferroui_base::{
23    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, AttachedProperty, BoxedValue,
24    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StaticType, StyledElement,
25    StyledElementImpl, StyledProperty, Thickness, TypeInfo, VisualImpl, VisualImplExt,
26    VisualTreeAttachmentEventArgs,
27};
28use std::cell::{Cell, RefCell};
29use std::collections::HashMap;
30use std::rc::Rc;
31
32const IS_MOBILE_PLATFORM: bool = cfg!(target_os = "android") || cfg!(target_os = "ios");
33
34/// The page shown by a tab container, and the subscription to the property
35/// changes of the page.
36struct ContainerPage {
37    tab_item: Ref<TabItem>,
38    page: Ref<Page>,
39    page_property_changed: Rc<dyn IDisposable>,
40}
41
42/// The tab control of the template and the handlers added to it.
43struct TabControlPart {
44    tab_control: Ref<TabControl>,
45    selection_changed: Option<RoutedEventHandlerToken>,
46    container_prepared: Rc<dyn IDisposable>,
47    container_index_changed: Rc<dyn IDisposable>,
48    container_clearing: Rc<dyn IDisposable>,
49}
50
51fn tab_item_key(tab_item: &TabItem) -> usize {
52    tab_item as *const TabItem as usize
53}
54
55fn page_key(page: &Page) -> usize {
56    page as *const Page as usize
57}
58
59/// A page that displays its child pages through a tab strip.
60#[repr(C)]
61pub struct TabbedPage {
62    base: SelectingMultiPage,
63    tab_control: RefCell<Option<TabControlPart>>,
64    ignoring_disabled_selection: Cell<bool>,
65    /// The pages of the tab containers, by the identity of the container.
66    container_page_map: RefCell<HashMap<usize, ContainerPage>>,
67    /// The tab containers of the pages, by the identity of the page.
68    page_container_map: RefCell<HashMap<usize, Ref<TabItem>>>,
69    /// The pages created by the page template, by their identity.
70    template_created_pages: RefCell<HashMap<usize, Ref<Page>>>,
71    last_swipe_gesture_id: Cell<i32>,
72    swipe_recognizer: Ref<SwipeGestureRecognizer>,
73    swipe_gesture_handler: Cell<Option<RoutedEventHandlerToken>>,
74}
75
76ferro_class!(TabbedPage: SelectingMultiPage);
77
78ferro_class_info!(TabbedPage {
79    new: TabbedPage::new,
80    markup: {
81        attributes: [
82            TemplatePart("PART_TabControl", type(Ref<TabControl>)),
83        ],
84    },
85});
86
87ferro_impl_classes!(TabbedPage: LayoutableImpl, InteractiveImpl);
88
89impl FerroObjectImpl for TabbedPage {
90    fn constructed(this: &Self) {
91        Self::parent_constructed(this);
92
93        this.set_current_value(MultiPage::pages_property(), Some(super::PageList::new()));
94        this.gesture_recognizers().add(this.swipe_recognizer.clone());
95        this.update_swipe_recognizer_axes();
96    }
97
98    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
99        Self::parent_on_property_changed(this, change);
100
101        let tab_control = this.tab_control();
102        if change.property() == Self::tab_placement_property().as_property() {
103            this.apply_tab_placement();
104            this.update_swipe_recognizer_axes();
105        } else if change.property() == Self::page_transition_property().as_property() && tab_control.is_some() {
106            if let Some(tab_control) = tab_control {
107                tab_control.set_page_transition(change.get_new_value::<Option<Rc<dyn IPageTransition>>>());
108            }
109        } else if change.property() == Self::indicator_template_property().as_property() {
110            this.apply_indicator_template();
111        } else if change.property() == Self::is_gesture_enabled_property().as_property() {
112            this.swipe_recognizer.set_is_enabled(change.get_new_value::<bool>());
113        } else if change.property() == MultiPage::items_source_property().as_property() && tab_control.is_some() {
114            if let Some(tab_control) = tab_control {
115                tab_control.set_items_source(
116                    change.get_new_value::<Option<ItemsSource>>().or_else(|| this.pages_items_source()),
117                );
118            }
119        } else if change.property() == MultiPage::pages_property().as_property()
120            && this.items_source().is_none()
121            && tab_control.is_some()
122        {
123            if let Some(tab_control) = tab_control {
124                tab_control.set_items_source(
125                    change.get_new_value::<Option<super::PageList>>().as_ref().map(MultiPage::items_source_of),
126                );
127            }
128        } else if change.property() == MultiPage::page_template_property().as_property()
129            && this.items_source().is_some()
130            && tab_control.is_some()
131        {
132            this.rebuild_template_created_pages();
133        }
134    }
135}
136
137impl StyledElementImpl for TabbedPage {
138    fn style_key_override(_this: &Self) -> &'static TypeInfo {
139        <TabbedPage as StaticType>::TYPE
140    }
141}
142
143impl VisualImpl for TabbedPage {
144    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
145        Self::parent_on_attached_to_visual_tree(this, e);
146
147        let weak = this.to_ref().downgrade();
148        let token = this.add_handler(InputElement::swipe_gesture_event(), move |_, e: &SwipeGestureEventArgs| {
149            if let Some(this) = weak.upgrade() {
150                this.on_swipe_gesture(e);
151            }
152        });
153        if let Some(previous) = this.swipe_gesture_handler.replace(Some(token)) {
154            this.remove_handler(InputElement::swipe_gesture_event(), previous);
155        }
156    }
157
158    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
159        Self::parent_on_detached_from_visual_tree(this, e);
160
161        if let Some(token) = this.swipe_gesture_handler.take() {
162            this.remove_handler(InputElement::swipe_gesture_event(), token);
163        }
164    }
165}
166
167impl InputElementImpl for TabbedPage {
168    fn on_key_down(this: &Self, e: &KeyEventArgs) {
169        Self::parent_on_key_down(this, e);
170
171        if !this.is_keyboard_navigation_enabled() {
172            return;
173        }
174        let Some(tab_control) = this.tab_control() else {
175            return;
176        };
177
178        let resolved = this.resolve_tab_placement();
179        let is_horizontal = resolved == TabPlacement::Top || resolved == TabPlacement::Bottom;
180        let is_rtl = this.flow_direction() == FlowDirection::RightToLeft;
181
182        let mut next = if is_horizontal {
183            if is_rtl {
184                e.key == Key::Left
185            } else {
186                e.key == Key::Right
187            }
188        } else {
189            e.key == Key::Down
190        };
191        let mut prev = if is_horizontal {
192            if is_rtl {
193                e.key == Key::Right
194            } else {
195                e.key == Key::Left
196            }
197        } else {
198            e.key == Key::Up
199        };
200
201        if e.key_modifiers.contains(KeyModifiers::CONTROL) && e.key == Key::Tab {
202            next = !e.key_modifiers.contains(KeyModifiers::SHIFT);
203            prev = e.key_modifiers.contains(KeyModifiers::SHIFT);
204        }
205
206        if next {
207            let target = this.find_next_enabled_tab(tab_control.selected_index() + 1, 1);
208            if target >= 0 {
209                tab_control.set_selected_index(target);
210                this.focus_selected_tab_item();
211                e.set_handled(true);
212            }
213        } else if prev {
214            let target = this.find_next_enabled_tab(tab_control.selected_index() - 1, -1);
215            if target >= 0 {
216                tab_control.set_selected_index(target);
217                this.focus_selected_tab_item();
218                e.set_handled(true);
219            }
220        }
221    }
222}
223
224impl TemplatedControlImpl for TabbedPage {
225    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
226        Self::parent_on_apply_template(this, e);
227
228        let requested_index = this.selected_index();
229
230        let previous = this.tab_control.borrow_mut().take();
231        if let Some(previous) = previous {
232            if let Some(token) = previous.selection_changed {
233                previous.tab_control.remove_handler(SelectingItemsControl::selection_changed_event(), token);
234            }
235            previous.container_prepared.dispose();
236            previous.container_index_changed.dispose();
237            previous.container_clearing.dispose();
238        }
239
240        this.clear_container_pages();
241
242        let tab_control = e.name_scope().find_as::<TabControl>("PART_TabControl");
243
244        if let Some(tab_control) = tab_control {
245            let weak = this.to_ref().downgrade();
246            let container_prepared = tab_control.container_prepared(move |e| {
247                if let Some(this) = weak.upgrade() {
248                    this.on_container_prepared(e);
249                }
250            });
251            let weak = this.to_ref().downgrade();
252            let container_index_changed = tab_control.container_index_changed(move |e| {
253                if let Some(this) = weak.upgrade() {
254                    this.on_container_index_changed(e);
255                }
256            });
257            let weak = this.to_ref().downgrade();
258            let container_clearing = tab_control.container_clearing(move |e| {
259                if let Some(this) = weak.upgrade() {
260                    this.on_container_clearing(e);
261                }
262            });
263            *this.tab_control.borrow_mut() = Some(TabControlPart {
264                tab_control: tab_control.clone(),
265                selection_changed: None,
266                container_prepared,
267                container_index_changed,
268                container_clearing,
269            });
270            tab_control.set_items_source(this.items_source().or_else(|| this.pages_items_source()));
271
272            if requested_index >= 0 {
273                tab_control.set_selected_index(requested_index);
274            }
275
276            let weak = this.to_ref().downgrade();
277            let token = tab_control.selection_changed(move |_, _| {
278                if let Some(this) = weak.upgrade() {
279                    this.tab_control_selection_changed();
280                }
281            });
282            if let Some(part) = this.tab_control.borrow_mut().as_mut() {
283                part.selection_changed = Some(token);
284            }
285
286            if let Some(page_transition) = this.page_transition() {
287                tab_control.set_page_transition(Some(page_transition));
288            }
289
290            this.apply_tab_placement();
291            this.apply_indicator_template();
292            this.update_active_page();
293
294            let weak = this.to_ref().downgrade();
295            let captured_tab_control = tab_control;
296            Dispatcher::ui_thread().post_local(
297                move || {
298                    if let Some(this) = weak.upgrade() {
299                        this.sync_all_tab_enabled_states(&captured_tab_control);
300                    }
301                },
302                DispatcherPriority::LOADED,
303            );
304        }
305    }
306}
307
308impl PageImpl for TabbedPage {
309    fn update_content_safe_area_padding(this: &Self) {
310        Self::parent_update_content_safe_area_padding(this);
311
312        if let Some(tab_control) = this.tab_control() {
313            let sa = this.safe_area_padding();
314
315            let (bar_margin, content_safe_area) = match this.resolve_tab_placement() {
316                TabPlacement::Bottom => {
317                    (Thickness::new(sa.left, 0.0, sa.right, sa.bottom), Thickness::new(sa.left, sa.top, sa.right, 0.0))
318                }
319                TabPlacement::Left => {
320                    (Thickness::new(sa.left, sa.top, 0.0, sa.bottom), Thickness::new(0.0, sa.top, sa.right, sa.bottom))
321                }
322                TabPlacement::Right => {
323                    (Thickness::new(0.0, sa.top, sa.right, sa.bottom), Thickness::new(sa.left, sa.top, 0.0, sa.bottom))
324                }
325                _ => (Thickness::new(sa.left, sa.top, sa.right, 0.0), Thickness::new(sa.left, 0.0, sa.right, sa.bottom)),
326            };
327
328            tab_control.set_margin(bar_margin);
329
330            if let Some(current_page) = this.current_page() {
331                current_page.set_safe_area_padding(content_safe_area);
332            }
333        }
334    }
335}
336
337impl MultiPageImpl for TabbedPage {
338    fn update_active_page_with(this: &Self, navigation_type: NavigationType) {
339        if let Some(tab_control) = this.tab_control() {
340            let index = tab_control.selected_index();
341            this.commit_selection_if_resolved(index, navigation_type);
342            this.update_content_safe_area_padding();
343        }
344    }
345}
346
347impl SelectingMultiPageImpl for TabbedPage {
348    fn apply_selected_index(this: &Self, index: i32) {
349        if let Some(tab_control) = this.tab_control() {
350            tab_control.set_selected_index(index);
351        } else {
352            this.store_selected_index(index);
353        }
354    }
355}
356
357impl ControlImpl for TabbedPage {
358    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
359        crate::automation::peers::TabbedPageAutomationPeer::new(this).upcast()
360    }
361}
362
363ferro_properties! {
364    impl TabbedPage {
365        /// Defines the `TabPlacement` property.
366        pub fn tab_placement_property() -> StyledProperty<TabPlacement> {
367            FerroProperty::register::<TabbedPage, _>("TabPlacement", TabPlacement::Auto)
368        }
369
370        /// Defines the `IsKeyboardNavigationEnabled` property.
371        pub fn is_keyboard_navigation_enabled_property() -> StyledProperty<bool> {
372            FerroProperty::register::<TabbedPage, _>("IsKeyboardNavigationEnabled", true)
373        }
374
375        /// Defines the `IsGestureEnabled` property.
376        pub fn is_gesture_enabled_property() -> StyledProperty<bool> {
377            FerroProperty::register::<TabbedPage, _>("IsGestureEnabled", false)
378        }
379
380        /// Defines the `PageTransition` property.
381        pub fn page_transition_property() -> StyledProperty<Option<Rc<dyn IPageTransition>>> {
382            FerroProperty::register::<TabbedPage, _>("PageTransition", None)
383        }
384
385        /// Defines the `IndicatorTemplate` property.
386        pub fn indicator_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
387            FerroProperty::register::<TabbedPage, _>("IndicatorTemplate", None)
388        }
389
390        /// Defines the `IsTabEnabled` attached property.
391        pub fn is_tab_enabled_property() -> AttachedProperty<bool> {
392            FerroProperty::register_attached::<TabbedPage, Page, _>("IsTabEnabled", true)
393        }
394    }
395}
396
397impl TabbedPage {
398    fn static_constructor() {
399        InputElement::focusable_property().override_default_value::<TabbedPage>(true);
400        Page::page_navigation_system_back_button_pressed_event().add_class_handler::<TabbedPage>(
401            |sender, event_args| {
402                if event_args.handled() {
403                    return;
404                }
405
406                let forwarded = RoutedEventArgs::with_event(Page::page_navigation_system_back_button_pressed_event());
407                if let Some(current_page) = sender.current_page() {
408                    current_page.raise_event(&forwarded);
409                }
410
411                if forwarded.handled() {
412                    event_args.set_handled(true);
413                }
414            },
415        );
416    }
417
418    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
419    pub fn construct() -> Self {
420        let swipe_recognizer = SwipeGestureRecognizer::new();
421        swipe_recognizer.set_is_enabled(false);
422        Self {
423            base: SelectingMultiPage::construct(),
424            tab_control: RefCell::new(None),
425            ignoring_disabled_selection: Cell::new(false),
426            container_page_map: RefCell::new(HashMap::new()),
427            page_container_map: RefCell::new(HashMap::new()),
428            template_created_pages: RefCell::new(HashMap::new()),
429            last_swipe_gesture_id: Cell::new(0),
430            swipe_recognizer,
431            swipe_gesture_handler: Cell::new(None),
432        }
433    }
434
435    /// Initializes a new instance of the [`TabbedPage`] class.
436    pub fn new() -> Ref<Self> {
437        instantiate(Self::construct())
438    }
439
440    /// Gets the value of the `IsTabEnabled` attached property for a page:
441    /// true if the tab is enabled.
442    pub fn get_is_tab_enabled(page: &Page) -> bool {
443        page.get_value(Self::is_tab_enabled_property())
444    }
445
446    /// Sets the value of the `IsTabEnabled` attached property for a page.
447    /// Disabled tabs are skipped during keyboard and swipe navigation.
448    pub fn set_is_tab_enabled(page: &Page, value: bool) {
449        page.set_value(Self::is_tab_enabled_property(), value)
450    }
451
452    /// Gets or sets the tab bar placement.
453    pub fn tab_placement(&self) -> TabPlacement {
454        self.get_value(Self::tab_placement_property())
455    }
456
457    pub fn set_tab_placement(&self, value: TabPlacement) {
458        self.set_value(Self::tab_placement_property(), value)
459    }
460
461    /// Gets or sets whether keyboard navigation can switch tabs.
462    pub fn is_keyboard_navigation_enabled(&self) -> bool {
463        self.get_value(Self::is_keyboard_navigation_enabled_property())
464    }
465
466    pub fn set_is_keyboard_navigation_enabled(&self, value: bool) {
467        self.set_value(Self::is_keyboard_navigation_enabled_property(), value)
468    }
469
470    /// Gets or sets whether swipe gestures can be used to navigate between
471    /// tabs.
472    ///
473    /// Defaults to false because tab strips do not respond to swipe
474    /// gestures on most platforms (iOS, desktop). Enable this only when the
475    /// host platform and the content inside each tab page do not conflict
476    /// with horizontal swipe input.
477    pub fn is_gesture_enabled(&self) -> bool {
478        self.get_value(Self::is_gesture_enabled_property())
479    }
480
481    pub fn set_is_gesture_enabled(&self, value: bool) {
482        self.set_value(Self::is_gesture_enabled_property(), value)
483    }
484
485    /// Gets or sets the page transition to use when switching tabs.
486    pub fn page_transition(&self) -> Option<Rc<dyn IPageTransition>> {
487        self.get_value(Self::page_transition_property())
488    }
489
490    pub fn set_page_transition(&self, value: Option<Rc<dyn IPageTransition>>) {
491        self.set_value(Self::page_transition_property(), value)
492    }
493
494    /// Gets or sets the data template used to render the selection
495    /// indicator on each tab.
496    pub fn indicator_template(&self) -> Option<Rc<dyn IDataTemplate>> {
497        self.get_value(Self::indicator_template_property())
498    }
499
500    pub fn set_indicator_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
501        self.set_value(Self::indicator_template_property(), value)
502    }
503
504    fn tab_control(&self) -> Option<Ref<TabControl>> {
505        self.tab_control.borrow().as_ref().map(|part| part.tab_control.clone())
506    }
507
508    fn tab_item_from_index(tab_control: &TabControl, index: i32) -> Option<Ref<TabItem>> {
509        tab_control.container_from_index(index).and_then(|container| container.cast::<TabItem>())
510    }
511
512    fn container_page(&self, tab_item: &TabItem) -> Option<Ref<Page>> {
513        self.container_page_map.borrow().get(&tab_item_key(tab_item)).map(|entry| entry.page.clone())
514    }
515
516    fn page_container(&self, page: &Page) -> Option<Ref<TabItem>> {
517        self.page_container_map.borrow().get(&page_key(page)).cloned()
518    }
519
520    fn resolve_tab_placement(&self) -> TabPlacement {
521        if self.tab_placement() != TabPlacement::Auto {
522            return self.tab_placement();
523        }
524
525        if IS_MOBILE_PLATFORM {
526            TabPlacement::Bottom
527        } else {
528            TabPlacement::Top
529        }
530    }
531
532    fn apply_tab_placement(&self) {
533        let Some(tab_control) = self.tab_control() else {
534            return;
535        };
536
537        tab_control.set_tab_strip_placement(match self.resolve_tab_placement() {
538            TabPlacement::Bottom => Dock::Bottom,
539            TabPlacement::Left => Dock::Left,
540            TabPlacement::Right => Dock::Right,
541            _ => Dock::Top,
542        });
543    }
544
545    fn update_swipe_recognizer_axes(&self) {
546        let placement = self.resolve_tab_placement();
547        let is_horizontal = placement == TabPlacement::Top || placement == TabPlacement::Bottom;
548        self.swipe_recognizer.set_can_horizontally_swipe(is_horizontal);
549        self.swipe_recognizer.set_can_vertically_swipe(!is_horizontal);
550    }
551
552    fn apply_indicator_template(&self) {
553        let Some(tab_control) = self.tab_control() else {
554            return;
555        };
556
557        tab_control.set_indicator_template(self.indicator_template());
558    }
559
560    fn tab_control_selection_changed(&self) {
561        if self.ignoring_disabled_selection.get() {
562            return;
563        }
564
565        let Some(tab_control) = self.tab_control() else {
566            return;
567        };
568        let new_index = tab_control.selected_index();
569        let new_page = self.resolve_tab_page_at_index(new_index);
570
571        if let Some(new_page) = new_page {
572            if !Self::get_is_tab_enabled(&new_page) {
573                let target = self.find_nearest_enabled_tab(new_index);
574                if target < 0 {
575                    self.select_ignoring_disabled_selection(&tab_control, self.selected_index());
576                    return;
577                }
578
579                self.select_ignoring_disabled_selection(&tab_control, target);
580
581                if target == self.selected_index() {
582                    return;
583                }
584
585                self.commit_selection_if_resolved(target, NavigationType::Replace);
586                self.update_content_safe_area_padding();
587                return;
588            }
589        }
590
591        self.commit_selection_if_resolved(new_index, NavigationType::Replace);
592        self.update_content_safe_area_padding();
593    }
594
595    /// Selects `index` in the tab control without reacting to the selection
596    /// change it raises.
597    fn select_ignoring_disabled_selection(&self, tab_control: &TabControl, index: i32) {
598        struct Reset<'a>(&'a Cell<bool>);
599        impl Drop for Reset<'_> {
600            fn drop(&mut self) {
601                self.0.set(false);
602            }
603        }
604
605        self.ignoring_disabled_selection.set(true);
606        let _reset = Reset(&self.ignoring_disabled_selection);
607        tab_control.set_selected_index(index);
608    }
609
610    fn sync_tab_item_with_page(tab_item: &TabItem, page: &Page) {
611        tab_item.set_is_enabled(Self::get_is_tab_enabled(page));
612        tab_item.set_header(page.header());
613        tab_item.set_icon(page.icon());
614        tab_item.set_icon_template(page.icon_template());
615    }
616
617    fn on_container_prepared(&self, e: &ContainerPreparedEventArgs) {
618        let Some(tab_item) = e.container().clone().cast::<TabItem>() else {
619            return;
620        };
621
622        let Some(page) = self.prepare_page_for_container(&tab_item, e.index()) else {
623            return;
624        };
625
626        Self::sync_tab_item_with_page(&tab_item, &page);
627
628        if e.index() == self.tab_control().map_or(-1, |tab_control| tab_control.selected_index()) {
629            self.update_active_page();
630        }
631    }
632
633    /// Selects the page of the tab that took the place of the selected tab
634    /// when the selected page left the tab control.
635    ///
636    /// Deviation (DEVIATIONS.md, Selection and pages): the tab control
637    /// moves its selection while the container of the page that left is
638    /// still the one at its index, so the selection change it raises
639    /// resolves to that page again; the containers are updated afterwards.
640    /// The reference does not handle this event: its selected page stays
641    /// the page that left, which here keeps that page alive.
642    fn on_container_index_changed(&self, e: &ContainerIndexChangedEventArgs) {
643        let Some(tab_control) = self.tab_control() else {
644            return;
645        };
646        if e.new_index() != tab_control.selected_index() {
647            return;
648        }
649        let Some(selected_page) = self.selected_page() else {
650            return;
651        };
652        if self.page_container(&selected_page).is_some() {
653            return;
654        }
655        let Some(page) = e.container().clone().cast::<TabItem>().and_then(|tab_item| self.container_page(&tab_item))
656        else {
657            return;
658        };
659
660        self.commit_selection(e.new_index(), Some(page), NavigationType::Remove);
661        self.update_content_safe_area_padding();
662    }
663
664    fn on_container_clearing(&self, e: &ContainerClearingEventArgs) {
665        if let Some(tab_item) = e.container().clone().cast::<TabItem>() {
666            self.clear_container_page(&tab_item);
667        }
668    }
669
670    fn rebuild_template_created_pages(&self) {
671        let Some(tab_control) = self.tab_control() else {
672            return;
673        };
674        if self.items_source().is_none() {
675            return;
676        }
677
678        for i in 0..tab_control.item_count() {
679            let Some(tab_item) = Self::tab_item_from_index(&tab_control, i) else {
680                continue;
681            };
682
683            let Some(page) = self.prepare_page_for_container(&tab_item, i) else {
684                continue;
685            };
686
687            Self::sync_tab_item_with_page(&tab_item, &page);
688        }
689
690        self.update_active_page();
691    }
692
693    fn on_page_property_changed(&self, page: &Ref<Page>, e: &FerroPropertyChangedEventArgs<'_>) {
694        if self.tab_control().is_none() {
695            return;
696        }
697
698        if e.property() == Page::icon_property().as_property() {
699            if let Some(tab_item) = self.page_container(page) {
700                tab_item.set_icon(page.icon());
701            }
702        } else if e.property() == Page::icon_template_property().as_property() {
703            if let Some(tab_item) = self.page_container(page) {
704                tab_item.set_icon_template(page.icon_template());
705            }
706        } else if e.property() == Page::header_property().as_property() {
707            if let Some(tab_item) = self.page_container(page) {
708                tab_item.set_header(page.header());
709            }
710        } else if e.property() == Self::is_tab_enabled_property().as_property() {
711            self.sync_tab_enabled_state(page);
712        }
713    }
714
715    fn find_nearest_enabled_tab(&self, disabled_index: i32) -> i32 {
716        let count = self.get_tab_count();
717        for dist in 1..count {
718            let p = disabled_index - dist;
719            if p >= 0 && self.is_tab_enabled_at_index(p) {
720                return p;
721            }
722            let n = disabled_index + dist;
723            if n < count && self.is_tab_enabled_at_index(n) {
724                return n;
725            }
726        }
727
728        -1
729    }
730
731    pub(crate) fn find_next_enabled_tab(&self, start: i32, direction: i32) -> i32 {
732        let count = self.get_tab_count();
733        let mut i = start;
734        while i >= 0 && i < count {
735            if self.is_tab_enabled_at_index(i) {
736                return i;
737            }
738            i += direction;
739        }
740
741        -1
742    }
743
744    pub(crate) fn get_tab_count(&self) -> i32 {
745        if let Some(tab_control) = self.tab_control() {
746            return tab_control.item_count();
747        }
748
749        if let Some(source) = self.items_source() {
750            return source.count() as i32;
751        }
752
753        self.pages().map_or(0, |pages| pages.count() as i32)
754    }
755
756    fn is_tab_enabled_at_index(&self, index: i32) -> bool {
757        if let Some(page) = self
758            .tab_control()
759            .and_then(|tab_control| Self::tab_item_from_index(&tab_control, index))
760            .and_then(|tab_item| self.container_page(&tab_item))
761        {
762            return Self::get_is_tab_enabled(&page);
763        }
764        if let Some(page) = self.resolve_tab_page_at_index(index) {
765            return Self::get_is_tab_enabled(&page);
766        }
767        true
768    }
769
770    /// The page of the tab at `index`: the page of its container, else the
771    /// item of the tab control if it is a page, else the page at the index
772    /// of the `Pages` collection. (The reference hides the member of the
773    /// base class of the same name with this one.)
774    fn resolve_tab_page_at_index(&self, index: i32) -> Option<Ref<Page>> {
775        let tab_control = self.tab_control();
776        let tab_item = tab_control.as_ref().and_then(|tab_control| Self::tab_item_from_index(tab_control, index));
777
778        if let Some(page) = tab_item.as_ref().and_then(|tab_item| self.container_page(tab_item)) {
779            return Some(page);
780        }
781
782        if let Some(tab_control) = &tab_control {
783            let items_view = tab_control.items_view();
784            if index >= 0 && (index as usize) < items_view.count() {
785                return items_view
786                    .get_at(index as usize)
787                    .as_ref()
788                    .and_then(Control::from_boxed)
789                    .and_then(|control| control.cast::<Page>());
790            }
791        }
792
793        if let Some(page) = tab_item
794            .and_then(|tab_item| tab_item.content())
795            .as_ref()
796            .and_then(Control::from_boxed)
797            .and_then(|control| control.cast::<Page>())
798        {
799            return Some(page);
800        }
801
802        self.resolve_page_at_index(index)
803    }
804
805    fn sync_tab_enabled_state(&self, page: &Page) {
806        let Some(tab_control) = self.tab_control() else {
807            return;
808        };
809        let Some(tab_item) = self.page_container(page) else {
810            return;
811        };
812
813        tab_item.set_is_enabled(Self::get_is_tab_enabled(page));
814
815        let selected_container = Self::tab_item_from_index(&tab_control, tab_control.selected_index());
816        if !Self::get_is_tab_enabled(page) && selected_container.is_some_and(|selected| selected.ptr_eq(&tab_item)) {
817            let i = tab_control.selected_index();
818            let target = self.find_nearest_enabled_tab(i);
819            if target >= 0 && target != i {
820                tab_control.set_selected_index(target);
821            }
822        }
823    }
824
825    fn sync_all_tab_enabled_states(&self, tab_control: &TabControl) {
826        for i in 0..tab_control.item_count() {
827            if let Some(tab_item) = Self::tab_item_from_index(tab_control, i) {
828                if let Some(page) = self.container_page(&tab_item) {
829                    tab_item.set_is_enabled(Self::get_is_tab_enabled(&page));
830                }
831            }
832        }
833    }
834
835    fn prepare_page_for_container(&self, tab_item: &Ref<TabItem>, index: i32) -> Option<Ref<Page>> {
836        self.clear_container_page(tab_item);
837
838        let page;
839
840        if self.items_source().is_some() {
841            let item = self.try_get_item_at_index(index)?;
842
843            page = self
844                .page_template()
845                .and_then(|page_template| page_template.build(&item))
846                .and_then(|control| control.cast::<Page>());
847            tab_item.set_content(page.as_ref().map(|page| Control::boxed(page.clone())));
848
849            let page = page?;
850
851            self.template_created_pages.borrow_mut().insert(page_key(&page), page.clone());
852            StyledElement::logical_children(self).add(page.clone().upcast());
853
854            self.map_container_to_page(tab_item, &page);
855            return Some(page);
856        }
857
858        let item = self.try_get_item_at_index(index)?;
859
860        page = item.as_ref().and_then(Control::from_boxed).and_then(|control| control.cast::<Page>());
861        let page = page?;
862
863        tab_item.set_content(Some(Control::boxed(page.clone())));
864
865        self.map_container_to_page(tab_item, &page);
866        Some(page)
867    }
868
869    fn map_container_to_page(&self, tab_item: &Ref<TabItem>, page: &Ref<Page>) {
870        let weak = self.to_ref().downgrade();
871        let weak_page = page.downgrade();
872        let page_property_changed = page.property_changed(move |e| {
873            if let (Some(this), Some(page)) = (weak.upgrade(), weak_page.upgrade()) {
874                this.on_page_property_changed(&page, e);
875            }
876        });
877
878        let replaced = self.container_page_map.borrow_mut().insert(
879            tab_item_key(tab_item),
880            ContainerPage { tab_item: tab_item.clone(), page: page.clone(), page_property_changed },
881        );
882        if let Some(replaced) = replaced {
883            replaced.page_property_changed.dispose();
884        }
885        self.page_container_map.borrow_mut().insert(page_key(page), tab_item.clone());
886    }
887
888    fn clear_container_pages(&self) {
889        let entries: Vec<ContainerPage> = self.container_page_map.borrow_mut().drain().map(|(_, entry)| entry).collect();
890        for entry in &entries {
891            entry.page_property_changed.dispose();
892        }
893
894        let template_created_pages: Vec<Ref<Page>> =
895            self.template_created_pages.borrow_mut().drain().map(|(_, page)| page).collect();
896        let logical_children = StyledElement::logical_children(self);
897        for page in template_created_pages {
898            logical_children.remove(&page.upcast());
899        }
900
901        self.page_container_map.borrow_mut().clear();
902    }
903
904    fn clear_container_page(&self, tab_item: &TabItem) {
905        let entry = self.container_page_map.borrow_mut().remove(&tab_item_key(tab_item));
906        let Some(entry) = entry else {
907            return;
908        };
909        debug_assert!(std::ptr::eq::<TabItem>(&*entry.tab_item, tab_item));
910
911        self.page_container_map.borrow_mut().remove(&page_key(&entry.page));
912        entry.page_property_changed.dispose();
913
914        let template_created = self.template_created_pages.borrow_mut().remove(&page_key(&entry.page));
915        if let Some(page) = template_created {
916            StyledElement::logical_children(self).remove(&page.upcast());
917        }
918    }
919
920    /// The item at `index` of the items of the tab control, else of the
921    /// items source or the pages. `None` when there is no such item.
922    fn try_get_item_at_index(&self, index: i32) -> Option<Option<BoxedValue>> {
923        if index < 0 {
924            return None;
925        }
926        let index = index as usize;
927
928        if let Some(tab_control) = self.tab_control() {
929            let items_view = tab_control.items_view();
930            if index < items_view.count() {
931                return Some(items_view.get_at(index));
932            }
933        }
934
935        if let Some(source) = self.items_source() {
936            return (index < source.count()).then(|| source.get_at(index));
937        }
938
939        self.pages().and_then(|pages| pages.try_get(index)).map(|page| Some(Control::boxed(page)))
940    }
941
942    fn commit_selection_if_resolved(&self, index: i32, navigation_type: NavigationType) {
943        let page = self.resolve_tab_page_at_index(index);
944
945        if page.is_none()
946            && self.items_source().is_some()
947            && index >= 0
948            && self.tab_control().and_then(|tab_control| tab_control.container_from_index(index)).is_none()
949        {
950            self.store_selected_index(index);
951            return;
952        }
953
954        self.commit_selection(index, page, navigation_type);
955    }
956
957    fn on_swipe_gesture(&self, e: &SwipeGestureEventArgs) {
958        if !self.is_gesture_enabled() {
959            return;
960        }
961        let Some(tab_control) = self.tab_control() else {
962            return;
963        };
964        if e.id() == self.last_swipe_gesture_id.get() {
965            return;
966        }
967
968        let placement = self.resolve_tab_placement();
969        let is_horizontal = placement == TabPlacement::Top || placement == TabPlacement::Bottom;
970        let is_rtl = self.flow_direction() == FlowDirection::RightToLeft;
971
972        let delta = match (e.swipe_direction(), is_horizontal, is_rtl) {
973            (SwipeDirection::Left, true, false) => 1,
974            (SwipeDirection::Right, true, false) => -1,
975            (SwipeDirection::Left, true, true) => -1,
976            (SwipeDirection::Right, true, true) => 1,
977            (SwipeDirection::Up, false, _) => -1,
978            (SwipeDirection::Down, false, _) => 1,
979            _ => 0,
980        };
981
982        if delta == 0 {
983            return;
984        }
985
986        let next = self.find_next_enabled_tab(tab_control.selected_index() + delta, delta);
987        if next >= 0 {
988            tab_control.set_selected_index(next);
989            e.set_handled(true);
990            self.last_swipe_gesture_id.set(e.id());
991        }
992    }
993
994    fn focus_selected_tab_item(&self) {
995        let Some(tab_control) = self.tab_control() else {
996            return;
997        };
998
999        if let Some(container) = tab_control.container_from_index(tab_control.selected_index()) {
1000            container.focus_with(NavigationMethod::Directional, KeyModifiers::NONE);
1001        }
1002    }
1003}