Skip to main content

ferroui_controls/
transitioning_content_control.rs

1use crate::presenters::ContentPresenter;
2use crate::primitives::TemplatedControlImpl;
3use crate::{
4    ContentControl, ContentControlImpl, ContentControlImplExt, ControlImpl, TransitionCompletedEventArgs,
5};
6use ferroui_base::animation::{CrossFade, IPageTransition, TimeSpan};
7use ferroui_base::input::InputElementImpl;
8use ferroui_base::interactivity::{
9    Interactive, InteractiveImpl, RoutedEvent, RoutedEventHandlerToken, RoutingStrategies,
10};
11use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt};
12use ferroui_base::threading::{
13    CancellationToken, CancellationTokenSource, Dispatcher, DispatcherPriority, DispatcherTask,
14    FerroSynchronizationContext,
15};
16use ferroui_base::{
17    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, instantiate,
18    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, Size, StyledElementImpl,
19    StyledProperty, Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
20};
21use std::cell::{Cell, RefCell};
22use std::future::Future;
23use std::pin::Pin;
24use std::rc::Rc;
25use std::task::{Context, Poll};
26
27/// Displays the content according to a data template, using a page
28/// transition to move between the old and new content.
29#[repr(C)]
30pub struct TransitioningContentControl {
31    base: ContentControl,
32    current_transition: RefCell<Option<CancellationTokenSource>>,
33    last_presenter: RefCell<Option<Ref<ContentPresenter>>>,
34    presenter2: RefCell<Option<Ref<ContentPresenter>>>,
35    is_first_full: Cell<bool>,
36    should_animate: Cell<bool>,
37}
38
39ferro_class!(TransitioningContentControl: ContentControl);
40ferro_class_info!(TransitioningContentControl { new: TransitioningContentControl::new });
41ferro_impl_classes!(
42    TransitioningContentControl: StyledElementImpl,
43    InteractiveImpl,
44    InputElementImpl,
45    ControlImpl,
46    TemplatedControlImpl
47);
48
49impl FerroObjectImpl for TransitioningContentControl {
50    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
51        if change.property() == ContentControl::content_property().as_property() {
52            this.update_content(true);
53            return;
54        }
55
56        Self::parent_on_property_changed(this, change);
57    }
58}
59
60impl VisualImpl for TransitioningContentControl {
61    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
62        Self::parent_on_attached_to_visual_tree(this, e);
63        this.update_content(false);
64    }
65}
66
67impl LayoutableImpl for TransitioningContentControl {
68    fn arrange_override(this: &Self, final_size: Size) -> Size {
69        let result = Self::parent_arrange_override(this, final_size);
70
71        if this.should_animate.get() {
72            let current_transition = this.current_transition.borrow().clone();
73            if let Some(current_transition) = current_transition {
74                current_transition.cancel();
75            }
76
77            let presenter2 = this.presenter2.borrow().clone();
78            if let (Some(presenter2), Some(presenter), Some(transition)) =
79                (presenter2, this.presenter(), this.page_transition())
80            {
81                this.should_animate.set(false);
82
83                let cancel = CancellationTokenSource::new();
84                *this.current_transition.borrow_mut() = Some(cancel.clone());
85
86                let (from, to) =
87                    if this.is_first_full.get() { (presenter2, presenter) } else { (presenter, presenter2) };
88                let from_content = from.content();
89                let to_content = to.content();
90
91                let from: Ref<Visual> = from.upcast();
92                let to: Ref<Visual> = to.upcast();
93                let task = transition.start(Some(&from), Some(&to), !this.is_transition_reversed(), cancel.token());
94
95                // The continuation always runs as a job of the
96                // synchronization context, never inside this arrange pass.
97                let this = this.to_ref();
98                let context = FerroSynchronizationContext::current().unwrap_or_else(|| {
99                    FerroSynchronizationContext::with_dispatcher(
100                        &Dispatcher::current_dispatcher(),
101                        DispatcherPriority::NORMAL,
102                    )
103                });
104                drop(context.to_task_scheduler().start_local(async move {
105                    if task.is_completed() {
106                        YieldOnce(false).await;
107                    }
108                    let ran_to_completion = TransitionEnd { task }.await;
109
110                    this.on_transition_completed(&TransitionCompletedEventArgs::new(
111                        from_content,
112                        to_content,
113                        ran_to_completion && !cancel.is_cancellation_requested(),
114                    ));
115
116                    if !cancel.is_cancellation_requested() {
117                        this.hide_old_presenter();
118                    }
119                }));
120            }
121
122            this.should_animate.set(false);
123        }
124
125        result
126    }
127}
128
129impl ContentControlImpl for TransitioningContentControl {
130    fn register_content_presenter(this: &Self, presenter: &ContentPresenter) -> bool {
131        if Self::parent_register_content_presenter(this, presenter) {
132            return true;
133        }
134
135        if presenter.name().as_deref() == Some("PART_ContentPresenter2") {
136            *this.presenter2.borrow_mut() = Some(presenter.to_ref());
137            presenter.set_is_visible(false);
138            this.update_content(false);
139            return true;
140        }
141
142        false
143    }
144}
145
146/// Waits for a page transition to end; the result tells whether it ran to
147/// completion. The failure of a transition is not re-raised.
148struct TransitionEnd {
149    task: DispatcherTask<()>,
150}
151
152impl Future for TransitionEnd {
153    type Output = bool;
154
155    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<bool> {
156        if self.task.is_faulted() {
157            return Poll::Ready(false);
158        }
159
160        match Pin::new(&mut self.task).poll(cx) {
161            Poll::Ready(result) => Poll::Ready(result.is_ok()),
162            Poll::Pending => Poll::Pending,
163        }
164    }
165}
166
167/// Suspends once: the rest of the asynchronous method runs from a job of its
168/// scheduler.
169struct YieldOnce(bool);
170
171impl Future for YieldOnce {
172    type Output = ();
173
174    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
175        if self.0 {
176            return Poll::Ready(());
177        }
178
179        self.0 = true;
180        cx.waker().wake_by_ref();
181        Poll::Pending
182    }
183}
184
185/// The default page transition: a cross-fade whose settings cannot be
186/// changed.
187struct ImmutableCrossFade {
188    inner: CrossFade,
189}
190
191impl ImmutableCrossFade {
192    fn new(duration: TimeSpan) -> Self {
193        Self { inner: CrossFade::with_duration(duration) }
194    }
195}
196
197impl IPageTransition for ImmutableCrossFade {
198    fn start(
199        &self,
200        from: Option<&Ref<Visual>>,
201        to: Option<&Ref<Visual>>,
202        _forward: bool,
203        cancellation_token: CancellationToken,
204    ) -> DispatcherTask<()> {
205        self.inner.start_cross_fade(from, to, cancellation_token)
206    }
207}
208
209ferro_properties! {
210    impl TransitioningContentControl {
211        /// Defines the `PageTransition` property.
212        pub fn page_transition_property() -> StyledProperty<Option<Rc<dyn IPageTransition>>> {
213            let default: Rc<dyn IPageTransition> = Rc::new(ImmutableCrossFade::new(TimeSpan::from_milliseconds(125.0)));
214            FerroProperty::register::<TransitioningContentControl, _>("PageTransition", Some(default))
215        }
216
217        /// Defines the `IsTransitionReversed` property.
218        pub fn is_transition_reversed_property() -> StyledProperty<bool> {
219            FerroProperty::register::<TransitioningContentControl, _>("IsTransitionReversed", false)
220        }
221    }
222}
223
224impl TransitioningContentControl {
225    ferro_routed_event!(
226        /// Defines the `TransitionCompleted` routed event.
227        pub fn transition_completed_event() -> RoutedEvent<TransitionCompletedEventArgs> {
228            RoutedEvent::register::<TransitioningContentControl, _>("TransitionCompleted", RoutingStrategies::DIRECT)
229        }
230    );
231
232    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
233    pub fn construct() -> Self {
234        Self {
235            base: ContentControl::construct(),
236            current_transition: RefCell::new(None),
237            last_presenter: RefCell::new(None),
238            presenter2: RefCell::new(None),
239            is_first_full: Cell::new(false),
240            should_animate: Cell::new(false),
241        }
242    }
243
244    pub fn new() -> Ref<Self> {
245        instantiate(Self::construct())
246    }
247
248    /// The animation played when content appears and disappears.
249    pub fn page_transition(&self) -> Option<Rc<dyn IPageTransition>> {
250        self.get_value(Self::page_transition_property())
251    }
252
253    pub fn set_page_transition(&self, value: Option<Rc<dyn IPageTransition>>) {
254        self.set_value(Self::page_transition_property(), value)
255    }
256
257    /// Whether the control is animated in the reverse direction.
258    ///
259    /// May not apply to all transitions.
260    pub fn is_transition_reversed(&self) -> bool {
261        self.get_value(Self::is_transition_reversed_property())
262    }
263
264    pub fn set_is_transition_reversed(&self, value: bool) {
265        self.set_value(Self::is_transition_reversed_property(), value)
266    }
267
268    /// Raised when the old content isn't needed anymore by the control,
269    /// because the transition has completed.
270    pub fn transition_completed(
271        &self,
272        handler: impl Fn(&Interactive, &TransitionCompletedEventArgs) + 'static,
273    ) -> RoutedEventHandlerToken {
274        self.add_handler(Self::transition_completed_event(), handler)
275    }
276
277    fn update_content(&self, with_transition: bool) {
278        let presenter2 = self.presenter2.borrow().clone();
279        let (Some(presenter2), Some(presenter)) = (presenter2, self.presenter()) else {
280            return;
281        };
282        if self.visual_root().is_none() {
283            return;
284        }
285
286        let current_presenter = if self.is_first_full.get() { presenter2 } else { presenter };
287        let last_presenter = self.last_presenter.borrow().clone();
288        let from_content = last_presenter.as_ref().and_then(|presenter| presenter.content());
289        let to_content = self.content();
290
291        if let Some(last_presenter) = &last_presenter {
292            if !last_presenter.ptr_eq(&current_presenter)
293                && crate::reference_equals(&last_presenter.content(), &to_content) {
294                last_presenter.set_content(None);
295            }
296        }
297
298        current_presenter.set_content(to_content.clone());
299        current_presenter.set_is_visible(true);
300        *self.last_presenter.borrow_mut() = Some(current_presenter);
301
302        self.is_first_full.set(!self.is_first_full.get());
303
304        if self.page_transition().is_some() && with_transition {
305            self.should_animate.set(true);
306            self.invalidate_arrange();
307        } else {
308            self.hide_old_presenter();
309            self.on_transition_completed(&TransitionCompletedEventArgs::new(from_content, to_content, false));
310        }
311    }
312
313    fn hide_old_presenter(&self) {
314        let old_presenter = if self.is_first_full.get() { self.presenter2.borrow().clone() } else { self.presenter() };
315        if let Some(old_presenter) = old_presenter {
316            old_presenter.set_content(None);
317            old_presenter.set_is_visible(false);
318        }
319    }
320
321    fn on_transition_completed(&self, e: &TransitionCompletedEventArgs) {
322        self.raise_event(e);
323    }
324}