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#[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 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
146struct 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
167struct 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
185struct 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 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 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 pub fn transition_completed_event() -> RoutedEvent<TransitionCompletedEventArgs> {
228 RoutedEvent::register::<TransitioningContentControl, _>("TransitionCompleted", RoutingStrategies::DIRECT)
229 }
230 );
231
232 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 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 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 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(¤t_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}