Skip to main content

ferroui_controls/
tab_control.rs

1use crate::generators::RecycleKey;
2use crate::metadata::TemplatePartAttribute;
3use crate::presenters::{register_content_presenter_host, ContentPresenter, IContentPresenterHost, ItemsPresenter};
4use crate::primitives::{
5    SelectingItemsControl, SelectingItemsControlImpl, SelectingItemsControlImplExt, TemplateAppliedEventArgs,
6    TemplatedControlImpl,
7};
8use crate::templates::{FuncTemplate, IDataTemplate, ITemplateOf};
9use crate::{
10    ContentControl, Control, ControlImpl, Dock, ItemsControl, ItemsControlImpl, ItemsControlImplExt, Panel,
11    SelectionMode, TabItem, WrapPanel,
12};
13use ferroui_base::animation::IPageTransition;
14use ferroui_base::collections::FerroList;
15use ferroui_base::input::{
16    FocusChangedEventArgs, InputElement, InputElementImpl, KeyboardNavigation, KeyboardNavigationMode,
17    NavigationMethod, PointerEventArgs, PointerUpdateKind,
18};
19use ferroui_base::interactivity::{IRoutedEventArgs, InteractiveImpl};
20use ferroui_base::layout::{HorizontalAlignment, Layoutable, LayoutableImpl, LayoutableImplExt, VerticalAlignment};
21use ferroui_base::logging::{LogArea, LogEventLevel, Logger};
22use ferroui_base::reactive::{CompositeDisposable, IDisposable, ObservableExt};
23use ferroui_base::threading::{
24    CancellationTokenSource, Dispatcher, DispatcherPriority, DispatcherTask, FerroSynchronizationContext,
25};
26use ferroui_base::{
27    ferro_class, ferro_impl_classes, ferro_property, instantiate, BoxedValue, DirectProperty, FerroObject,
28    FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref,
29    Size, StaticType, StyledElement, StyledElementImpl, StyledProperty, Visual, VisualImpl,
30    VisualImplExt, VisualTreeAttachmentEventArgs,
31};
32use std::any::Any;
33use std::cell::{Cell, RefCell};
34use std::future::Future;
35use std::pin::Pin;
36use std::rc::Rc;
37use std::task::{Context, Poll};
38
39/// A tab control that displays a tab strip along with the content of the
40/// selected tab.
41#[repr(C)]
42pub struct TabControl {
43    base: SelectingItemsControl,
44    selected_content: RefCell<Option<BoxedValue>>,
45    selected_content_template: RefCell<Option<Rc<dyn IDataTemplate>>>,
46    selected_item_subscriptions: RefCell<Option<CompositeDisposable>>,
47    items_presenter_part: RefCell<Option<Ref<ItemsPresenter>>>,
48    content_part: RefCell<Option<Ref<ContentPresenter>>>,
49    content_presenter2: RefCell<Option<Ref<ContentPresenter>>>,
50    previous_selected_index: Cell<i32>,
51    current_transition: RefCell<Option<Rc<CancellationTokenSource>>>,
52    should_animate: Cell<bool>,
53    pending_forward: Cell<bool>,
54}
55
56ferro_class! {
57    TabControl: SelectingItemsControl, virtuals TabControlImpl: SelectingItemsControlImpl {
58        /// Called when a [`ContentPresenter`] is registered with the
59        /// control.
60        fn register_content_presenter(this, presenter: &ContentPresenter) -> bool;
61    }
62}
63ferroui_base::ferro_class_info!(TabControl { new: TabControl::new });
64
65ferro_impl_classes!(TabControl: StyledElementImpl, InteractiveImpl, InputElementImpl, ControlImpl);
66
67impl FerroObjectImpl for TabControl {
68    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
69        Self::parent_on_property_changed(this, change);
70
71        if change.property() == Self::tab_strip_placement_property().as_property() {
72            this.refresh_containers();
73        } else if change.property() == Self::indicator_template_property().as_property() {
74            this.update_indicator_template();
75        } else if change.property() == Self::content_template_property().as_property() {
76            let new_template = change.get_new_value::<Option<Rc<dyn IDataTemplate>>>();
77            if this.selected_content_template() != new_template {
78                if let Some(container) = this.container_from_index(this.selected_index()) {
79                    if container.get_value(ContentControl::content_template_property()).is_none() {
80                        this.set_selected_content_template(new_template.clone());
81                        if !this.should_animate.get() {
82                            if let Some(content_part) = this.content_part() {
83                                content_part.set_content_template(new_template);
84                            }
85                        }
86                    }
87                }
88            }
89        } else if change.property() == KeyboardNavigation::tab_once_active_element_property().as_property() {
90            if let Some(panel) = this.items_presenter_part().and_then(|presenter| presenter.panel()) {
91                KeyboardNavigation::set_tab_once_active_element(
92                    &panel,
93                    change.get_new_value::<Option<Ref<InputElement>>>(),
94                );
95            }
96        }
97    }
98}
99
100impl VisualImpl for TabControl {
101    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
102        Self::parent_on_detached_from_visual_tree(this, e);
103
104        if let Some(current_transition) = this.current_transition.take() {
105            current_transition.cancel();
106        }
107        this.should_animate.set(false);
108        Self::reset_transition_presenter(this.content_presenter2().as_ref());
109    }
110}
111
112impl LayoutableImpl for TabControl {
113    fn arrange_override(this: &Self, final_size: Size) -> Size {
114        let result = Self::parent_arrange_override(this, final_size);
115
116        if this.should_animate.get() {
117            this.should_animate.set(false);
118            if let Some(current_transition) = this.current_transition.take() {
119                current_transition.cancel();
120            }
121            let cancel = Rc::new(CancellationTokenSource::new());
122            *this.current_transition.borrow_mut() = Some(cancel.clone());
123            let from = this.content_part();
124            let to = this.content_presenter2();
125            if let (Some(from), Some(to), Some(page_transition)) = (from, to, this.page_transition()) {
126                this.run_transition_async(page_transition, from, to, this.pending_forward.get(), cancel);
127            }
128        }
129
130        result
131    }
132}
133
134impl TemplatedControlImpl for TabControl {
135    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
136        let items_presenter_part = e.name_scope().find_as::<ItemsPresenter>("PART_ItemsPresenter");
137        *this.items_presenter_part.borrow_mut() = items_presenter_part.clone();
138        if let Some(items_presenter_part) = &items_presenter_part {
139            items_presenter_part.apply_template();
140        }
141
142        this.update_tab_strip_placement();
143
144        if let Some(panel) = items_presenter_part.and_then(|presenter| presenter.panel()) {
145            if !panel.is_set(KeyboardNavigation::tab_navigation_property().as_property()) {
146                panel.set_current_value(KeyboardNavigation::tab_navigation_property(), KeyboardNavigationMode::Once);
147            }
148            KeyboardNavigation::set_tab_once_active_element(
149                &panel,
150                KeyboardNavigation::get_tab_once_active_element(this),
151            );
152        }
153    }
154}
155
156impl ItemsControlImpl for TabControl {
157    fn create_container_for_item_override(
158        _this: &Self,
159        _item: &Option<BoxedValue>,
160        _index: i32,
161        _recycle_key: Option<RecycleKey>,
162    ) -> Ref<Control> {
163        TabItem::new().upcast()
164    }
165
166    fn needs_container_override(this: &Self, item: &Option<BoxedValue>, _index: i32) -> (bool, Option<RecycleKey>) {
167        this.needs_container::<TabItem>(item)
168    }
169
170    fn prepare_container_for_item_override(
171        this: &Self,
172        element: &Ref<Control>,
173        item: &Option<BoxedValue>,
174        index: i32,
175    ) {
176        Self::parent_prepare_container_for_item_override(this, element, item, index);
177
178        if let Some(tab_item) = element.downcast_ref::<TabItem>() {
179            tab_item.set_tab_strip_placement(Some(this.tab_strip_placement()));
180
181            if let Some(tmpl) = this.indicator_template() {
182                if !tab_item.is_set(TabItem::indicator_template_property().as_property()) {
183                    tab_item.set_current_value(TabItem::indicator_template_property(), Some(tmpl));
184                }
185            }
186        }
187
188        if index == this.selected_index() {
189            this.update_selected_content(Some(element.clone()));
190        }
191    }
192
193    fn container_index_changed_override(this: &Self, container: &Ref<Control>, old_index: i32, new_index: i32) {
194        Self::parent_container_index_changed_override(this, container, old_index, new_index);
195
196        let selected_index = this.selected_index();
197
198        if selected_index == old_index || selected_index == new_index {
199            this.update_selected_content(None);
200        }
201    }
202
203    fn clear_container_for_item_override(this: &Self, element: &Ref<Control>) {
204        Self::parent_clear_container_for_item_override(this, element);
205        this.update_selected_content(None);
206    }
207}
208
209impl SelectingItemsControlImpl for TabControl {
210    fn should_trigger_selection(this: &Self, selectable: &Visual, event_args: &PointerEventArgs) -> bool {
211        matches!(
212            event_args.properties().pointer_update_kind,
213            PointerUpdateKind::LeftButtonPressed | PointerUpdateKind::LeftButtonReleased
214        ) && Self::parent_should_trigger_selection(this, selectable, event_args)
215    }
216
217    fn update_selection_from_event(this: &Self, container: &Ref<Control>, event_args: &dyn IRoutedEventArgs) -> bool {
218        if let Some(focus) = event_args.downcast_ref::<FocusChangedEventArgs>() {
219            if focus.navigation_method != NavigationMethod::Directional {
220                return false;
221            }
222        }
223
224        Self::parent_update_selection_from_event(this, container, event_args)
225    }
226}
227
228impl TabControlImpl for TabControl {
229    fn register_content_presenter(this: &Self, presenter: &ContentPresenter) -> bool {
230        let name = presenter.name();
231
232        if name.as_deref() == Some("PART_SelectedContentHost") {
233            *this.content_part.borrow_mut() = Some(presenter.to_ref());
234            presenter.set_content(this.selected_content());
235            presenter.set_content_template(this.selected_content_template());
236            return true;
237        }
238
239        if name.as_deref() == Some("PART_SelectedContentHost2") {
240            *this.content_presenter2.borrow_mut() = Some(presenter.to_ref());
241            presenter.set_is_visible(false);
242            return true;
243        }
244
245        false
246    }
247}
248
249impl IContentPresenterHost for TabControl {
250    fn logical_children(&self) -> &FerroList<Ref<StyledElement>> {
251        StyledElement::logical_children(self)
252    }
253
254    fn register_content_presenter(&self, presenter: &ContentPresenter) -> bool {
255        TabControl::register_content_presenter(self, presenter)
256    }
257}
258
259impl TabControl {
260    /// The named parts expected in the control template.
261    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] = &[
262        TemplatePartAttribute::new("PART_ItemsPresenter", <ItemsPresenter as StaticType>::TYPE),
263        TemplatePartAttribute::new("PART_SelectedContentHost", <ContentPresenter as StaticType>::TYPE),
264        TemplatePartAttribute::new("PART_SelectedContentHost2", <ContentPresenter as StaticType>::TYPE),
265    ];
266}
267
268ferroui_base::ferro_properties! { impl TabControl {
269    ferro_property!(
270        /// Defines the `TabStripPlacement` property.
271        pub fn tab_strip_placement_property() -> StyledProperty<Dock> {
272            FerroProperty::register::<TabControl, _>("TabStripPlacement", Dock::Top)
273        }
274    );
275
276    ferro_property!(
277        /// Defines the `HorizontalContentAlignment` property.
278        pub fn horizontal_content_alignment_property() -> StyledProperty<HorizontalAlignment> {
279            ContentControl::horizontal_content_alignment_property().add_owner::<TabControl>()
280        }
281    );
282
283    ferro_property!(
284        /// Defines the `VerticalContentAlignment` property.
285        pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
286            ContentControl::vertical_content_alignment_property().add_owner::<TabControl>()
287        }
288    );
289
290    ferro_property!(
291        /// Defines the `ContentTemplate` property.
292        pub fn content_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
293            ContentControl::content_template_property().add_owner::<TabControl>()
294        }
295    );
296
297    ferro_property!(
298        /// Defines the `SelectedContent` property.
299        pub fn selected_content_property() -> DirectProperty<TabControl, Option<BoxedValue>> {
300            FerroProperty::register_direct::<TabControl, _>("SelectedContent", |o| o.selected_content(), None, None)
301        }
302    );
303
304    ferro_property!(
305        /// Defines the `SelectedContentTemplate` property.
306        pub fn selected_content_template_property() -> DirectProperty<TabControl, Option<Rc<dyn IDataTemplate>>> {
307            FerroProperty::register_direct::<TabControl, _>(
308                "SelectedContentTemplate",
309                |o| o.selected_content_template(),
310                None,
311                None,
312            )
313        }
314    );
315
316    ferro_property!(
317        /// Defines the `PageTransition` property.
318        pub fn page_transition_property() -> StyledProperty<Option<Rc<dyn IPageTransition>>> {
319            FerroProperty::register::<TabControl, _>("PageTransition", None)
320        }
321    );
322
323    ferro_property!(
324        /// Defines the `IndicatorTemplate` property.
325        pub fn indicator_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
326            FerroProperty::register::<TabControl, _>("IndicatorTemplate", None)
327        }
328    );
329} }
330
331impl TabControl {
332    /// Initializes static members of the [`TabControl`] class.
333    fn static_constructor() {
334        register_content_presenter_host::<TabControl>();
335
336        // The default value for the `ItemsPanel` property.
337        let default_panel: Rc<dyn ITemplateOf<Option<Ref<Panel>>>> =
338            FuncTemplate::new(|| Some(WrapPanel::new().upcast::<Panel>()));
339
340        SelectingItemsControl::selection_mode_property()
341            .override_default_value::<TabControl>(SelectionMode::ALWAYS_SELECTED);
342        ItemsControl::items_panel_property().override_default_value::<TabControl>(default_panel);
343        Layoutable::affects_measure::<TabControl>(&[Self::tab_strip_placement_property().as_property()]);
344        SelectingItemsControl::selected_item_property()
345            .changed()
346            .add_class_handler::<TabControl>(|x, _| x.update_selected_content(None));
347        crate::automation::AutomationProperties::control_type_override_property()
348            .override_default_value::<TabControl>(Some(crate::automation::peers::AutomationControlType::Tab));
349    }
350
351    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
352    pub fn construct() -> Self {
353        Self {
354            base: SelectingItemsControl::construct(),
355            selected_content: RefCell::new(None),
356            selected_content_template: RefCell::new(None),
357            selected_item_subscriptions: RefCell::new(None),
358            items_presenter_part: RefCell::new(None),
359            content_part: RefCell::new(None),
360            content_presenter2: RefCell::new(None),
361            previous_selected_index: Cell::new(-1),
362            current_transition: RefCell::new(None),
363            should_animate: Cell::new(false),
364            pending_forward: Cell::new(false),
365        }
366    }
367
368    pub fn new() -> Ref<Self> {
369        instantiate(Self::construct())
370    }
371
372    /// Gets or sets the horizontal alignment of the content within the
373    /// control.
374    pub fn horizontal_content_alignment(&self) -> HorizontalAlignment {
375        self.get_value(Self::horizontal_content_alignment_property())
376    }
377
378    pub fn set_horizontal_content_alignment(&self, value: HorizontalAlignment) {
379        self.set_value(Self::horizontal_content_alignment_property(), value)
380    }
381
382    /// Gets or sets the vertical alignment of the content within the
383    /// control.
384    pub fn vertical_content_alignment(&self) -> VerticalAlignment {
385        self.get_value(Self::vertical_content_alignment_property())
386    }
387
388    pub fn set_vertical_content_alignment(&self, value: VerticalAlignment) {
389        self.set_value(Self::vertical_content_alignment_property(), value)
390    }
391
392    /// Gets or sets the tabstrip placement of the [`TabControl`].
393    pub fn tab_strip_placement(&self) -> Dock {
394        self.get_value(Self::tab_strip_placement_property())
395    }
396
397    pub fn set_tab_strip_placement(&self, value: Dock) {
398        self.set_value(Self::tab_strip_placement_property(), value)
399    }
400
401    /// Gets or sets the default data template used to display the content
402    /// of the selected tab.
403    pub fn content_template(&self) -> Option<Rc<dyn IDataTemplate>> {
404        self.get_value(Self::content_template_property())
405    }
406
407    pub fn set_content_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
408        self.set_value(Self::content_template_property(), value)
409    }
410
411    /// Gets the content of the selected tab.
412    pub fn selected_content(&self) -> Option<BoxedValue> {
413        self.selected_content.borrow().clone()
414    }
415
416    pub(crate) fn set_selected_content(&self, value: Option<BoxedValue>) {
417        self.set_and_raise(Self::selected_content_property(), &self.selected_content, value);
418    }
419
420    /// Gets the content template for the selected tab.
421    pub fn selected_content_template(&self) -> Option<Rc<dyn IDataTemplate>> {
422        self.selected_content_template.borrow().clone()
423    }
424
425    pub(crate) fn set_selected_content_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
426        self.set_and_raise(Self::selected_content_template_property(), &self.selected_content_template, value);
427    }
428
429    /// Gets or sets the page transition to use when switching tabs.
430    pub fn page_transition(&self) -> Option<Rc<dyn IPageTransition>> {
431        self.get_value(Self::page_transition_property())
432    }
433
434    pub fn set_page_transition(&self, value: Option<Rc<dyn IPageTransition>>) {
435        self.set_value(Self::page_transition_property(), value)
436    }
437
438    /// Gets or sets the data template used to render the selection
439    /// indicator on each tab item.
440    pub fn indicator_template(&self) -> Option<Rc<dyn IDataTemplate>> {
441        self.get_value(Self::indicator_template_property())
442    }
443
444    pub fn set_indicator_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
445        self.set_value(Self::indicator_template_property(), value)
446    }
447
448    pub(crate) fn items_presenter_part(&self) -> Option<Ref<ItemsPresenter>> {
449        self.items_presenter_part.borrow().clone()
450    }
451
452    pub(crate) fn content_part(&self) -> Option<Ref<ContentPresenter>> {
453        self.content_part.borrow().clone()
454    }
455
456    fn content_presenter2(&self) -> Option<Ref<ContentPresenter>> {
457        self.content_presenter2.borrow().clone()
458    }
459
460    /// Replaces the presenters that display the selected content. The
461    /// reference tests set the two private fields through reflection.
462    #[cfg(test)]
463    pub(crate) fn set_content_presenters_for_test(
464        &self,
465        content_part: Option<Ref<ContentPresenter>>,
466        content_presenter2: Option<Ref<ContentPresenter>>,
467    ) {
468        *self.content_part.borrow_mut() = content_part;
469        *self.content_presenter2.borrow_mut() = content_presenter2;
470    }
471
472    fn select_content_template(
473        &self,
474        container_template: Option<Rc<dyn IDataTemplate>>,
475    ) -> Option<Rc<dyn IDataTemplate>> {
476        container_template.or_else(|| self.content_template())
477    }
478
479    fn update_selected_content(&self, container: Option<Ref<Control>>) {
480        if let Some(subscriptions) = self.selected_item_subscriptions.take() {
481            subscriptions.dispose();
482        }
483
484        if let Some(current_transition) = self.current_transition.take() {
485            current_transition.cancel();
486        }
487        self.should_animate.set(false);
488
489        Self::reset_transition_presenter(self.content_presenter2().as_ref());
490
491        let old_index = self.previous_selected_index.get();
492        self.previous_selected_index.set(self.selected_index());
493        let forward = self.selected_index() >= old_index || old_index < 0;
494
495        if self.selected_index() == -1 {
496            self.set_selected_content_template(None);
497            self.set_selected_content(None);
498            if let Some(content_part) = self.content_part() {
499                content_part.set_content(None);
500                content_part.set_content_template(None);
501                content_part.set_data_context(None);
502            }
503            return;
504        }
505
506        let container = container.or_else(|| self.container_from_index(self.selected_index()));
507        let Some(container) = container else { return };
508
509        if self.selected_content_template()
510            != self.select_content_template(container.get_value(ContentControl::content_template_property()))
511        {
512            self.set_selected_content_template(None);
513            if let Some(content_part) = self.content_part() {
514                content_part.set_content_template(None);
515            }
516        }
517
518        let should_transition = self.page_transition().is_some()
519            && self.content_presenter2().is_some()
520            && self.visual_root().is_some()
521            && old_index >= 0
522            && old_index != self.selected_index();
523        let is_initial_fire = Rc::new(Cell::new(true));
524
525        // The handlers are held by the container: both the tab control and
526        // the container are referenced weakly.
527        let weak = self.to_ref().downgrade();
528        let weak_container = container.downgrade();
529        let container_object: &FerroObject = &container;
530
531        let content_subscription = {
532            let (weak, weak_container) = (weak.clone(), weak_container.clone());
533            let observable =
534                FerroObjectExtensions::get_observable(container_object, ContentControl::content_property());
535            observable.subscribe_fn(move |content| {
536                let (Some(this), Some(container)) = (weak.upgrade(), weak_container.upgrade()) else { return };
537
538                this.set_selected_content(content.clone());
539
540                let template =
541                    this.select_content_template(container.get_value(ContentControl::content_template_property()));
542                let control = match &template {
543                    None => content.as_ref().and_then(Control::from_boxed),
544                    Some(_) => None,
545                };
546
547                if is_initial_fire.get() && should_transition {
548                    this.set_selected_content_template(template.clone());
549
550                    let content_presenter2 =
551                        this.content_presenter2().expect("the transition presenter is registered");
552                    content_presenter2.set_content_template(template);
553                    content_presenter2.set_is_visible(true);
554
555                    match (&content, &control) {
556                        (Some(content), Some(control)) => {
557                            this.set_control_content(&content_presenter2, content, control, container.data_context())
558                        }
559                        _ => content_presenter2.set_content(content),
560                    }
561
562                    this.pending_forward.set(forward);
563                    this.should_animate.set(true);
564                    this.invalidate_arrange();
565                } else if let Some(content_part) = this.content_part() {
566                    match (&content, &control) {
567                        (Some(content), Some(control)) => {
568                            this.set_control_content(&content_part, content, control, container.data_context())
569                        }
570                        _ => content_part.set_content(content),
571                    }
572                }
573
574                is_initial_fire.set(false);
575            })
576        };
577
578        let content_template_subscription = {
579            let weak = weak.clone();
580            let observable =
581                FerroObjectExtensions::get_observable(container_object, ContentControl::content_template_property());
582            observable.subscribe_fn(move |v| {
583                let Some(this) = weak.upgrade() else { return };
584
585                this.set_selected_content_template(this.select_content_template(v));
586                if !this.should_animate.get() {
587                    if let Some(content_part) = this.content_part() {
588                        content_part.set_content_template(this.selected_content_template());
589                    }
590                }
591            })
592        };
593
594        let data_context_subscription = {
595            let observable =
596                FerroObjectExtensions::get_observable(container_object, StyledElement::data_context_property());
597            observable.subscribe_fn(move |dc| {
598                let Some(this) = weak.upgrade() else { return };
599
600                // During a transition, the content part holds the old tab's
601                // content and the second content presenter holds the new
602                // tab's content. Only update the presenter that is showing
603                // this container's content. Only override the data context
604                // when there's no content template; with a template, the
605                // presenter's data context should be the content itself (so
606                // the template can bind to it).
607                let has_template_less_control_content = |presenter: &ContentPresenter| {
608                    presenter.content().as_ref().and_then(Control::from_boxed).is_some()
609                        && presenter.content_template().is_none()
610                };
611
612                match this.content_presenter2().filter(|presenter| presenter.is_visible()) {
613                    Some(content_presenter2) => {
614                        if has_template_less_control_content(&content_presenter2) {
615                            content_presenter2.set_data_context(dc);
616                        }
617                    }
618                    None => {
619                        if let Some(content_part) = this.content_part() {
620                            if has_template_less_control_content(&content_part) {
621                                content_part.set_data_context(dc);
622                            }
623                        }
624                    }
625                }
626            })
627        };
628
629        *self.selected_item_subscriptions.borrow_mut() = Some(CompositeDisposable::from_disposables([
630            content_subscription,
631            content_template_subscription,
632            data_context_subscription,
633        ]));
634    }
635
636    fn run_transition_async(
637        &self,
638        transition: Rc<dyn IPageTransition>,
639        from: Ref<ContentPresenter>,
640        to: Ref<ContentPresenter>,
641        forward: bool,
642        cts: Rc<CancellationTokenSource>,
643    ) {
644        let this = self.to_ref().downgrade();
645        let context = FerroSynchronizationContext::current().unwrap_or_else(|| {
646            FerroSynchronizationContext::with_dispatcher(&Dispatcher::current_dispatcher(), DispatcherPriority::NORMAL)
647        });
648
649        // The transition is started before the first await, as part of the
650        // call.
651        let from_visual: Ref<Visual> = from.clone().upcast();
652        let to_visual: Ref<Visual> = to.upcast();
653        let task = transition.start(Some(&from_visual), Some(&to_visual), forward, cts.token());
654
655        let _ = context.to_task_scheduler().start_local(async move {
656            match (TransitionOutcomeFuture { task }).await {
657                TransitionOutcome::Canceled => return,
658                TransitionOutcome::Faulted => {
659                    if let Some(logger) = Logger::try_get(LogEventLevel::Error, LogArea::CONTROL) {
660                        let source = this.upgrade();
661                        logger.log(
662                            source.as_ref().map(|source| source as &dyn Any),
663                            "Tab transition threw an unhandled exception.",
664                        );
665                    }
666                }
667                TransitionOutcome::Completed => {}
668            }
669
670            if cts.is_cancellation_requested() {
671                return;
672            }
673
674            from.set_is_visible(false);
675            from.set_content(None);
676            from.set_content_template(None);
677            from.set_data_context(None);
678            from.set_render_transform(None);
679            from.set_opacity(1.0);
680
681            if let Some(this) = this.upgrade() {
682                let content_part = this.content_part.take();
683                let content_presenter2 = this.content_presenter2.replace(content_part);
684                *this.content_part.borrow_mut() = content_presenter2;
685            }
686        });
687    }
688
689    fn set_control_content(
690        &self,
691        presenter: &Ref<ContentPresenter>,
692        content: &BoxedValue,
693        control: &Ref<Control>,
694        data_context: Option<BoxedValue>,
695    ) {
696        self.clear_owning_content_presenter(presenter, control);
697        Self::clear_presenter_content_unless(self.content_part().as_ref(), presenter, control);
698        Self::clear_presenter_content_unless(self.content_presenter2().as_ref(), presenter, control);
699        presenter.set_content_with_data_context(Some(content.clone()), data_context);
700    }
701
702    fn clear_owning_content_presenter(&self, target_presenter: &Ref<ContentPresenter>, content: &Ref<Control>) {
703        let parent = content.visual_parent().and_then(|parent| parent.cast::<ContentPresenter>());
704        let Some(parent) = parent else { return };
705        let this: Ref<FerroObject> = self.to_ref().upcast();
706
707        if &parent != target_presenter && parent.host() == Some(this) && Self::has_content(&parent, content) {
708            Self::clear_presenter_content(Some(&parent), content);
709        }
710    }
711
712    fn clear_presenter_content_unless(
713        presenter: Option<&Ref<ContentPresenter>>,
714        target_presenter: &Ref<ContentPresenter>,
715        content: &Ref<Control>,
716    ) {
717        if presenter != Some(target_presenter) {
718            Self::clear_presenter_content(presenter, content);
719        }
720    }
721
722    fn clear_presenter_content(presenter: Option<&Ref<ContentPresenter>>, content: &Ref<Control>) {
723        if let Some(presenter) = presenter.filter(|presenter| Self::has_content(presenter, content)) {
724            presenter.set_content(None);
725            presenter.set_content_template(None);
726            presenter.set_data_context(None);
727            presenter.set_render_transform(None);
728            presenter.set_opacity(1.0);
729        }
730    }
731
732    /// Whether the content of the presenter is the given control.
733    fn has_content(presenter: &ContentPresenter, content: &Ref<Control>) -> bool {
734        presenter.content().as_ref().and_then(Control::from_boxed).as_ref() == Some(content)
735    }
736
737    fn reset_transition_presenter(presenter: Option<&Ref<ContentPresenter>>) {
738        let Some(presenter) = presenter else { return };
739
740        presenter.set_is_visible(false);
741        presenter.set_content(None);
742        presenter.set_content_template(None);
743        presenter.set_data_context(None);
744        presenter.set_render_transform(None);
745        presenter.set_opacity(1.0);
746    }
747
748    fn update_tab_strip_placement(&self) {
749        let Some(panel) = self.items_presenter_part().and_then(|presenter| presenter.panel()) else { return };
750
751        for control in panel.children().snapshot().iter() {
752            if let Some(tab_item) = control.downcast_ref::<TabItem>() {
753                tab_item.set_tab_strip_placement(Some(self.tab_strip_placement()));
754            }
755        }
756    }
757
758    fn update_indicator_template(&self) {
759        let Some(panel) = self.items_presenter_part().and_then(|presenter| presenter.panel()) else { return };
760
761        let template = self.indicator_template();
762        for control in panel.children().snapshot().iter() {
763            if let Some(tab_item) = control.downcast_ref::<TabItem>() {
764                if !tab_item.is_set(TabItem::indicator_template_property().as_property()) {
765                    match &template {
766                        Some(template) => tab_item
767                            .set_current_value(TabItem::indicator_template_property(), Some(template.clone())),
768                        None => tab_item.clear_value(TabItem::indicator_template_property()),
769                    }
770                }
771            }
772        }
773    }
774}
775
776/// How a page transition ended.
777enum TransitionOutcome {
778    Completed,
779    Canceled,
780    Faulted,
781}
782
783/// Waits for a page transition without re-raising its failure.
784struct TransitionOutcomeFuture {
785    task: DispatcherTask<()>,
786}
787
788impl Future for TransitionOutcomeFuture {
789    type Output = TransitionOutcome;
790
791    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<TransitionOutcome> {
792        if self.task.is_faulted() {
793            return Poll::Ready(TransitionOutcome::Faulted);
794        }
795
796        match Pin::new(&mut self.task).poll(cx) {
797            Poll::Ready(Ok(())) => Poll::Ready(TransitionOutcome::Completed),
798            Poll::Ready(Err(_)) => Poll::Ready(TransitionOutcome::Canceled),
799            Poll::Pending => Poll::Pending,
800        }
801    }
802}