Skip to main content

ferroui_controls/
virtualizing_carousel_panel.rs

1use crate::generators::{ItemContainerGenerator, RecycleKey};
2use crate::items_source::ItemsChangedEventArgs;
3use crate::primitives::{register_logical_scrollable, ILogicalScrollable};
4use crate::{
5    Carousel, Control, ControlImpl, ItemsControl, ItemsSourceView, PanelImpl, VirtualizingPanel,
6    VirtualizingPanelImpl, VirtualizingPanelImplExt,
7};
8use ferroui_base::animation::easings::{Easing, QuadraticEaseOut, SineEaseOut};
9use ferroui_base::animation::{
10    Animation, Cue, FillMode, IPageTransition, KeyFrame, PageTransitionItem, SlideAxis, TimeSpan,
11};
12use ferroui_base::collections::NotifyCollectionChangedAction;
13use ferroui_base::input::gesture_recognizers::SwipeGestureRecognizer;
14use ferroui_base::input::{
15    IScrollable, InputElement, InputElementImpl, NavigationDirection, SwipeGestureEndedEventArgs,
16    SwipeGestureEventArgs,
17};
18use ferroui_base::interactivity::{InteractiveImpl, RoutedEventHandlerToken};
19use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt};
20use ferroui_base::reactive::{Disposable, IDisposable};
21use ferroui_base::styling::Setter;
22use ferroui_base::threading::{
23    CancellationTokenSource, Dispatcher, DispatcherPriority, DispatcherTask, DispatcherTaskScheduler,
24    FerroSynchronizationContext,
25};
26use ferroui_base::utilities::{HandlerList, MathUtilities};
27use ferroui_base::{
28    ferro_class, ferro_impl_classes, ferro_property, instantiate, AttachedProperty, BoxedValue, FerroObjectImpl, FerroObjectImplExt,
29    FerroProperty, FerroPropertyChangedEventArgs, Rect, Ref, Size, StyledElementImpl, StyledProperty, Vector,
30    Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs, WeakRef,
31};
32use std::cell::{Cell, RefCell};
33use std::collections::HashMap;
34use std::future::Future;
35use std::pin::Pin;
36use std::rc::Rc;
37use std::task::{Context, Poll};
38
39const SWIPE_COMMIT_THRESHOLD: f64 = 0.25;
40const VELOCITY_COMMIT_THRESHOLD: f64 = 800.0;
41const MIN_SWIPE_DISTANCE_FOR_VELOCITY_COMMIT: f64 = 0.05;
42const RUBBER_BAND_FACTOR: f64 = 0.3;
43const RUBBER_BAND_RETURN_DURATION: f64 = 0.16;
44const MAX_COMPLETION_DURATION: f64 = 0.35;
45const MIN_COMPLETION_DURATION: f64 = 0.12;
46
47/// A container realized for a slot of the fractional viewport.
48#[derive(Clone)]
49struct ViewportRealizedItem {
50    /// The position of the slot, in pages. With wrapping it can lie outside
51    /// the range of the items.
52    logical_index: i32,
53    item_index: i32,
54    control: Ref<Control>,
55}
56
57/// A slot of the fractional viewport: its position and the index of the
58/// item it shows.
59type ViewportSlot = (i32, i32);
60
61/// A panel used by [`Carousel`] to display the current item.
62#[repr(C)]
63pub struct VirtualizingCarouselPanel {
64    base: VirtualizingPanel,
65    extent: Cell<Size>,
66    offset: Cell<Vector>,
67    viewport: Cell<Size>,
68    recycle_pool: RefCell<HashMap<RecycleKey, Vec<Ref<Control>>>>,
69    /// The containers of the fractional viewport, in the order of their
70    /// slots.
71    viewport_realized: RefCell<Vec<ViewportRealizedItem>>,
72    realized: RefCell<Option<Ref<Control>>>,
73    realized_index: Cell<i32>,
74    transition_from: RefCell<Option<Ref<Control>>>,
75    transition_from_index: Cell<i32>,
76    transition: RefCell<Option<Rc<CancellationTokenSource>>>,
77    transition_task: RefCell<Option<DispatcherTask<()>>>,
78    scroll_invalidated: Rc<HandlerList<dyn Fn()>>,
79    can_horizontally_scroll: Cell<bool>,
80    can_vertically_scroll: Cell<bool>,
81
82    swipe_gesture_recognizer: RefCell<Option<Ref<SwipeGestureRecognizer>>>,
83    /// The handlers of the swipe gesture events, while they are added.
84    swipe_gesture_handler: Cell<Option<RoutedEventHandlerToken>>,
85    swipe_gesture_ended_handler: Cell<Option<RoutedEventHandlerToken>>,
86    swipe_gesture_id: Cell<i32>,
87    is_dragging: Cell<bool>,
88    total_delta: Cell<f64>,
89    is_forward: Cell<bool>,
90    swipe_target: RefCell<Option<Ref<Control>>>,
91    swipe_target_index: Cell<i32>,
92    swipe_axis: Cell<Option<SlideAxis>>,
93    locked_axis: Cell<SlideAxis>,
94
95    completion_cts: RefCell<Option<Rc<CancellationTokenSource>>>,
96    offset_animation_cts: RefCell<Option<Rc<CancellationTokenSource>>>,
97    completion_end_progress: Cell<f64>,
98    is_rubber_banding: Cell<bool>,
99    drag_start_offset: Cell<f64>,
100    progress_start_offset: Cell<f64>,
101    offset_animation_start: Cell<f64>,
102    offset_animation_target: Cell<f64>,
103    active_viewport_target_offset: Cell<f64>,
104    progress_from_index: Cell<i32>,
105    progress_to_index: Cell<i32>,
106}
107
108ferro_class!(VirtualizingCarouselPanel: VirtualizingPanel);
109ferroui_base::ferro_class_info!(VirtualizingCarouselPanel { new: VirtualizingCarouselPanel::new });
110ferro_impl_classes!(VirtualizingCarouselPanel: StyledElementImpl, InteractiveImpl, ControlImpl, PanelImpl);
111
112impl FerroObjectImpl for VirtualizingCarouselPanel {
113    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
114        Self::parent_on_property_changed(this, change);
115
116        if change.property() == Self::offset_animation_progress_property().as_property() {
117            if Self::is_active(&this.offset_animation_cts) {
118                let anim_progress = change.get_new_value::<f64>();
119                let primary_offset = this.offset_animation_start.get()
120                    + ((this.offset_animation_target.get() - this.offset_animation_start.get()) * anim_progress);
121                this.set_offset_core(this.with_primary_offset(this.offset.get(), primary_offset));
122
123                if this.uses_viewport_fraction_layout() {
124                    let transition = this.get_transition();
125                    if let Some(progressive) = transition.as_ref().and_then(|t| t.as_progress_page_transition()) {
126                        let transition_progress = this.get_fractional_transition_progress(primary_offset);
127                        progressive.update(
128                            transition_progress,
129                            Self::as_visual(this.find_viewport_control(this.progress_from_index.get())).as_ref(),
130                            Self::as_visual(this.find_viewport_control(this.progress_to_index.get())).as_ref(),
131                            this.is_forward.get(),
132                            this.get_viewport_item_extent(this.bounds().size()),
133                            &this.build_fractional_visible_items(primary_offset),
134                        );
135                    }
136                }
137            }
138        } else if change.property() == Self::completion_progress_property().as_property() {
139            let is_completion_animating = Self::is_active(&this.completion_cts);
140
141            if !this.is_dragging.get() && this.swipe_target().is_none() && !is_completion_animating {
142                return;
143            }
144
145            let progress = change.get_new_value::<f64>();
146            let transition = this.get_transition();
147            if let Some(progressive) = transition.as_ref().and_then(|t| t.as_progress_page_transition()) {
148                let realized = this.realized();
149                let swipe_target = this.swipe_target().filter(|target| realized.as_ref() != Some(target));
150                let size = this.locked_axis_size();
151                progressive.update(
152                    progress,
153                    Self::as_visual(realized).as_ref(),
154                    Self::as_visual(swipe_target).as_ref(),
155                    this.is_forward.get(),
156                    size,
157                    &[],
158                );
159            }
160        }
161    }
162}
163
164impl VisualImpl for VirtualizingCarouselPanel {
165    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
166        Self::parent_on_attached_to_visual_tree(this, e);
167        this.refresh_gesture_recognizer();
168    }
169
170    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
171        Self::parent_on_detached_from_visual_tree(this, e);
172        this.teardown_gesture_recognizer();
173    }
174}
175
176impl LayoutableImpl for VirtualizingCarouselPanel {
177    fn measure_override(this: &Self, available_size: Size) -> Size {
178        if this.uses_viewport_fraction_layout() {
179            return this.measure_viewport_fraction_override(available_size);
180        }
181
182        this.clear_viewport_realized();
183        this.cancel_offset_animation();
184
185        this.measure_single_page_override(available_size)
186    }
187
188    fn arrange_override(this: &Self, final_size: Size) -> Size {
189        if this.uses_viewport_fraction_layout() {
190            return this.arrange_viewport_fraction_override(final_size);
191        }
192
193        // The single page arrange.
194        let result = Self::parent_arrange_override(this, final_size);
195
196        if this.transition.borrow().is_none() {
197            let transition_from = this.transition_from();
198            let to = this.realized();
199            let transition = this.get_transition();
200
201            if let (Some(transition_from), Some(to), Some(transition)) = (transition_from, to, transition) {
202                let transition_cts = Rc::new(CancellationTokenSource::new());
203                *this.transition.borrow_mut() = Some(transition_cts.clone());
204
205                let forward = this.realized_index.get() > this.transition_from_index.get();
206
207                let task = this.run_transition_async(transition_cts, transition_from, to, forward, transition);
208                *this.transition_task.borrow_mut() = Some(task);
209            }
210        }
211
212        result
213    }
214}
215
216impl InputElementImpl for VirtualizingCarouselPanel {
217    fn as_scrollable(this: &Self) -> Option<&dyn IScrollable> {
218        Some(this)
219    }
220}
221
222impl VirtualizingPanelImpl for VirtualizingCarouselPanel {
223    fn on_items_control_changed(this: &Self, old_value: Option<&Ref<ItemsControl>>) {
224        Self::parent_on_items_control_changed(this, old_value);
225        // The items presenter attaches the panel to the visual tree before
226        // attaching it to the items control, so the attachment to the visual
227        // tree is notified with no items control; re-run setup here.
228        this.refresh_gesture_recognizer();
229    }
230
231    fn get_control_in_direction(
232        _this: &Self,
233        _direction: NavigationDirection,
234        _from: Option<&Ref<InputElement>>,
235        _wrap: bool,
236    ) -> Option<Ref<InputElement>> {
237        None
238    }
239
240    fn container_from_index(this: &Self, index: i32) -> Option<Ref<Control>> {
241        let items = this.items();
242
243        if index < 0 || index as usize >= items.count() {
244            return None;
245        }
246        if let Some(viewport_realized) = this.find_viewport_control(index) {
247            return Some(viewport_realized);
248        }
249        if index == this.realized_index.get() {
250            return this.realized();
251        }
252        if let Some(c) = items.get_at(index as usize).as_ref().and_then(Control::from_boxed) {
253            if c.get_value(Self::recycle_key_property()) == Some(Self::item_is_its_own_container()) {
254                return Some(c);
255            }
256        }
257        None
258    }
259
260    fn get_realized_containers(this: &Self) -> Option<Vec<Ref<Control>>> {
261        {
262            let viewport_realized = this.viewport_realized.borrow();
263            if !viewport_realized.is_empty() {
264                return Some(viewport_realized.iter().map(|x| x.control.clone()).collect());
265            }
266        }
267
268        this.realized().map(|realized| vec![realized])
269    }
270
271    fn index_from_container(this: &Self, container: &Ref<Control>) -> i32 {
272        {
273            let viewport_realized = this.viewport_realized.borrow();
274            if let Some(entry) = viewport_realized.iter().find(|entry| entry.control == *container) {
275                return entry.item_index;
276            }
277        }
278
279        if this.realized.borrow().as_ref() == Some(container) {
280            this.realized_index.get()
281        } else {
282            -1
283        }
284    }
285
286    fn scroll_into_view(_this: &Self, _index: i32) -> Option<Ref<Control>> {
287        None
288    }
289
290    fn on_items_changed(this: &Self, items: &Rc<ItemsSourceView>, e: &ItemsChangedEventArgs<'_>) {
291        Self::parent_on_items_changed(this, items, e);
292
293        if this.uses_viewport_fraction_layout() || !this.viewport_realized.borrow().is_empty() {
294            this.clear_viewport_realized();
295            this.invalidate_measure();
296            return;
297        }
298
299        // Returns whether the handling of the change is complete.
300        let add = |index: i32, count: i32| -> bool {
301            if this.realized.borrow().is_none() {
302                this.invalidate_measure();
303                return true;
304            }
305
306            if index <= this.realized_index.get() {
307                this.realized_index.set(this.realized_index.get() + count);
308            }
309            false
310        };
311
312        let remove = |index: i32, count: i32| {
313            let end = index + (count - 1);
314            let realized = this.realized();
315            let realized_index = this.realized_index.get();
316
317            match realized {
318                Some(realized) if index <= realized_index && end >= realized_index => {
319                    this.recycle_element(&realized);
320                    *this.realized.borrow_mut() = None;
321                    this.realized_index.set(-1);
322                }
323                _ => {
324                    if index < realized_index {
325                        this.realized_index.set(realized_index - count);
326                    }
327                }
328            }
329        };
330
331        let new_count = e.new_items.len() as i32;
332        let old_count = e.old_items.len() as i32;
333
334        let mut action = e.action;
335        if matches!(action, NotifyCollectionChangedAction::Replace | NotifyCollectionChangedAction::Move)
336            && e.old_starting_index < 0
337        {
338            action = NotifyCollectionChangedAction::Reset;
339        }
340
341        match action {
342            NotifyCollectionChangedAction::Add => {
343                if add(e.new_starting_index, new_count) {
344                    return;
345                }
346            }
347            NotifyCollectionChangedAction::Remove => {
348                remove(e.old_starting_index, old_count);
349            }
350            NotifyCollectionChangedAction::Replace => {
351                remove(e.old_starting_index, old_count);
352                if add(e.new_starting_index, new_count) {
353                    return;
354                }
355            }
356            NotifyCollectionChangedAction::Move => {
357                remove(e.old_starting_index, old_count);
358                let mut insert_index = e.new_starting_index;
359
360                if e.new_starting_index > e.old_starting_index {
361                    insert_index -= old_count - 1;
362                }
363
364                if add(insert_index, new_count) {
365                    return;
366                }
367            }
368            NotifyCollectionChangedAction::Reset => {
369                let realized = this.realized();
370                if let Some(realized) = realized {
371                    this.recycle_element(&realized);
372                    *this.realized.borrow_mut() = None;
373                    this.realized_index.set(-1);
374                }
375            }
376        }
377
378        this.invalidate_measure();
379    }
380}
381
382impl IScrollable for VirtualizingCarouselPanel {
383    fn extent(&self) -> Size {
384        self.extent.get()
385    }
386
387    fn offset(&self) -> Vector {
388        self.offset.get()
389    }
390
391    fn set_offset(&self, value: Vector) {
392        self.set_offset_core(value)
393    }
394
395    fn viewport(&self) -> Size {
396        self.viewport.get()
397    }
398
399    fn can_horizontally_scroll(&self) -> bool {
400        self.can_horizontally_scroll.get()
401    }
402
403    fn can_vertically_scroll(&self) -> bool {
404        self.can_vertically_scroll.get()
405    }
406}
407
408impl ILogicalScrollable for VirtualizingCarouselPanel {
409    fn set_can_horizontally_scroll(&self, value: bool) {
410        self.can_horizontally_scroll.set(value)
411    }
412
413    fn set_can_vertically_scroll(&self, value: bool) {
414        self.can_vertically_scroll.set(value)
415    }
416
417    fn is_logical_scroll_enabled(&self) -> bool {
418        true
419    }
420
421    fn scroll_size(&self) -> Size {
422        Size::new(1.0, 1.0)
423    }
424
425    fn page_scroll_size(&self) -> Size {
426        Size::new(1.0, 1.0)
427    }
428
429    fn scroll_invalidated(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
430        let token = self.scroll_invalidated.add(handler);
431        let handlers = Rc::downgrade(&self.scroll_invalidated);
432        Disposable::create(move || {
433            if let Some(handlers) = handlers.upgrade() {
434                handlers.remove(token);
435            }
436        })
437    }
438
439    fn bring_into_view(&self, _target: &Ref<Control>, _target_rect: Rect) -> bool {
440        false
441    }
442
443    fn get_control_in_direction(
444        &self,
445        _direction: NavigationDirection,
446        _from: Option<&Ref<Control>>,
447    ) -> Option<Ref<Control>> {
448        None
449    }
450
451    fn raise_scroll_invalidated(&self) {
452        self.invoke_scroll_invalidated();
453    }
454}
455
456/// Clears a cancellation source field when an asynchronous method is left,
457/// if the field still holds the source of that call.
458struct ClearSourceOnExit {
459    panel: WeakRef<VirtualizingCarouselPanel>,
460    field: fn(&VirtualizingCarouselPanel) -> &RefCell<Option<Rc<CancellationTokenSource>>>,
461    source: Rc<CancellationTokenSource>,
462}
463
464impl Drop for ClearSourceOnExit {
465    fn drop(&mut self) {
466        if let Some(panel) = self.panel.upgrade() {
467            let field = (self.field)(&panel);
468            let is_current = field.borrow().as_ref().is_some_and(|current| Rc::ptr_eq(current, &self.source));
469            if is_current {
470                *field.borrow_mut() = None;
471            }
472        }
473    }
474}
475
476/// Waits for a page transition to end. An interrupted or failed transition
477/// ends the wait like a completed one: an interruption is expected when a
478/// transition is superseded by a newer navigation action.
479struct TransitionEnd {
480    task: DispatcherTask<()>,
481}
482
483impl Future for TransitionEnd {
484    type Output = ();
485
486    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
487        // The failure of a transition is not re-raised.
488        if self.task.is_faulted() {
489            return Poll::Ready(());
490        }
491
492        match Pin::new(&mut self.task).poll(cx) {
493            Poll::Ready(_) => Poll::Ready(()),
494            Poll::Pending => Poll::Pending,
495        }
496    }
497}
498
499ferroui_base::ferro_properties! { impl VirtualizingCarouselPanel {
500    ferro_property!(
501        fn recycle_key_property() -> AttachedProperty<Option<RecycleKey>> {
502            FerroProperty::register_attached::<VirtualizingCarouselPanel, Control, _>("RecycleKey", None)
503        }
504    );
505
506    ferro_property!(
507        fn completion_progress_property() -> StyledProperty<f64> {
508            FerroProperty::register::<VirtualizingCarouselPanel, _>("CompletionProgress", 0.0)
509        }
510    );
511
512    ferro_property!(
513        fn offset_animation_progress_property() -> StyledProperty<f64> {
514            FerroProperty::register::<VirtualizingCarouselPanel, _>("OffsetAnimationProgress", 0.0)
515        }
516    );
517} }
518
519impl VirtualizingCarouselPanel {
520    /// The recycle key that marks an item which is its own container.
521    fn item_is_its_own_container() -> RecycleKey {
522        ferroui_base::ferro_thread_local! {
523            static KEY: RecycleKey = RecycleKey::new_unique();
524        }
525        KEY.with(|key| *key)
526    }
527
528    fn static_constructor() {
529        register_logical_scrollable::<VirtualizingCarouselPanel>();
530    }
531
532    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
533    pub fn construct() -> Self {
534        Self {
535            base: VirtualizingPanel::construct(),
536            extent: Cell::new(Size::default()),
537            offset: Cell::new(Vector::default()),
538            viewport: Cell::new(Size::default()),
539            recycle_pool: RefCell::new(HashMap::new()),
540            viewport_realized: RefCell::new(Vec::new()),
541            realized: RefCell::new(None),
542            realized_index: Cell::new(-1),
543            transition_from: RefCell::new(None),
544            transition_from_index: Cell::new(-1),
545            transition: RefCell::new(None),
546            transition_task: RefCell::new(None),
547            scroll_invalidated: Rc::new(HandlerList::new()),
548            can_horizontally_scroll: Cell::new(false),
549            can_vertically_scroll: Cell::new(false),
550            swipe_gesture_recognizer: RefCell::new(None),
551            swipe_gesture_handler: Cell::new(None),
552            swipe_gesture_ended_handler: Cell::new(None),
553            swipe_gesture_id: Cell::new(0),
554            is_dragging: Cell::new(false),
555            total_delta: Cell::new(0.0),
556            is_forward: Cell::new(false),
557            swipe_target: RefCell::new(None),
558            swipe_target_index: Cell::new(-1),
559            swipe_axis: Cell::new(None),
560            locked_axis: Cell::new(SlideAxis::Horizontal),
561            completion_cts: RefCell::new(None),
562            offset_animation_cts: RefCell::new(None),
563            completion_end_progress: Cell::new(0.0),
564            is_rubber_banding: Cell::new(false),
565            drag_start_offset: Cell::new(0.0),
566            progress_start_offset: Cell::new(0.0),
567            offset_animation_start: Cell::new(0.0),
568            offset_animation_target: Cell::new(0.0),
569            active_viewport_target_offset: Cell::new(0.0),
570            progress_from_index: Cell::new(-1),
571            progress_to_index: Cell::new(-1),
572        }
573    }
574
575    pub fn new() -> Ref<Self> {
576        instantiate(Self::construct())
577    }
578
579    pub(crate) fn is_managing_interaction_offset(&self) -> bool {
580        self.uses_viewport_fraction_layout() && (self.is_dragging.get() || Self::is_active(&self.offset_animation_cts))
581    }
582
583    /// Whether the field holds a cancellation source that has not been
584    /// cancelled.
585    #[inline]
586    fn is_active(source: &RefCell<Option<Rc<CancellationTokenSource>>>) -> bool {
587        source.borrow().as_ref().is_some_and(|source| !source.is_cancellation_requested())
588    }
589
590    /// The carousel that the panel is displaying items for.
591    #[inline]
592    fn carousel(&self) -> Option<Ref<Carousel>> {
593        self.items_control().and_then(|items_control| items_control.cast::<Carousel>())
594    }
595
596    #[inline]
597    fn realized(&self) -> Option<Ref<Control>> {
598        self.realized.borrow().clone()
599    }
600
601    #[inline]
602    fn transition_from(&self) -> Option<Ref<Control>> {
603        self.transition_from.borrow().clone()
604    }
605
606    #[inline]
607    fn swipe_target(&self) -> Option<Ref<Control>> {
608        self.swipe_target.borrow().clone()
609    }
610
611    #[inline]
612    fn as_visual(control: Option<Ref<Control>>) -> Option<Ref<Visual>> {
613        control.map(Ref::upcast)
614    }
615
616    /// The generator of the items control. Panics if the panel does not
617    /// belong to an items control.
618    #[inline]
619    fn generator(&self) -> Rc<ItemContainerGenerator> {
620        match self.item_container_generator() {
621            Some(generator) => generator,
622            None => panic!("The VirtualizingPanel does not belong to an ItemsControl."),
623        }
624    }
625
626    /// The number of items.
627    #[inline]
628    fn item_count(&self) -> i32 {
629        self.items().count() as i32
630    }
631
632    fn set_extent(&self, value: Size) {
633        if self.extent.get() != value {
634            self.extent.set(value);
635            self.invoke_scroll_invalidated();
636        }
637    }
638
639    fn set_viewport(&self, value: Size) {
640        if self.viewport.get() != value {
641            self.viewport.set(value);
642            self.invoke_scroll_invalidated();
643        }
644    }
645
646    fn invoke_scroll_invalidated(&self) {
647        if self.scroll_invalidated.is_empty() {
648            return;
649        }
650        for (_, handler) in self.scroll_invalidated.snapshot().iter() {
651            handler();
652        }
653    }
654
655    fn uses_viewport_fraction_layout(&self) -> bool {
656        self.carousel().is_some_and(|carousel| !MathUtilities::are_close(carousel.viewport_fraction(), 1.0))
657    }
658
659    fn get_layout_axis(&self) -> SlideAxis {
660        self.carousel().map_or(SlideAxis::Horizontal, |carousel| carousel.get_layout_axis())
661    }
662
663    fn get_viewport_fraction(&self) -> f64 {
664        self.carousel().map_or(1.0, |carousel| carousel.viewport_fraction())
665    }
666
667    fn get_viewport_units(&self) -> f64 {
668        1.0 / self.get_viewport_fraction()
669    }
670
671    fn get_primary_offset(&self, offset: Vector) -> f64 {
672        if self.get_layout_axis() == SlideAxis::Vertical {
673            offset.y
674        } else {
675            offset.x
676        }
677    }
678
679    fn get_primary_size(&self, size: Size) -> f64 {
680        if self.get_layout_axis() == SlideAxis::Vertical {
681            size.height
682        } else {
683            size.width
684        }
685    }
686
687    fn get_cross_size(&self, size: Size) -> f64 {
688        if self.get_layout_axis() == SlideAxis::Vertical {
689            size.width
690        } else {
691            size.height
692        }
693    }
694
695    fn create_logical_size(&self, primary: f64) -> Size {
696        if self.get_layout_axis() == SlideAxis::Vertical {
697            Size::new(1.0, primary)
698        } else {
699            Size::new(primary, 1.0)
700        }
701    }
702
703    fn create_item_size(&self, primary: f64, cross: f64) -> Size {
704        if self.get_layout_axis() == SlideAxis::Vertical {
705            Size::new(cross, primary)
706        } else {
707            Size::new(primary, cross)
708        }
709    }
710
711    fn create_item_rect(&self, primary_offset: f64, primary_size: f64, cross_size: f64) -> Rect {
712        if self.get_layout_axis() == SlideAxis::Vertical {
713            Rect::new(0.0, primary_offset, cross_size, primary_size)
714        } else {
715            Rect::new(primary_offset, 0.0, primary_size, cross_size)
716        }
717    }
718
719    fn with_primary_offset(&self, offset: Vector, primary: f64) -> Vector {
720        if self.get_layout_axis() == SlideAxis::Vertical {
721            Vector::new(offset.x, primary)
722        } else {
723            Vector::new(primary, offset.y)
724        }
725    }
726
727    /// The size of the panel along the axis that the swipe is locked to.
728    fn locked_axis_size(&self) -> f64 {
729        if self.locked_axis.get() == SlideAxis::Horizontal {
730            self.bounds().width
731        } else {
732            self.bounds().height
733        }
734    }
735
736    fn resolve_layout_size(&self, available_size: Size) -> Size {
737        let owner = self.items_control();
738
739        fn resolve_dimension(available: f64, bounds: f64, owner_bounds: f64, owner_explicit: f64) -> f64 {
740            if !available.is_infinite() && available > 0.0 {
741                return available;
742            }
743
744            if bounds > 0.0 {
745                return bounds;
746            }
747
748            if owner_bounds > 0.0 {
749                return owner_bounds;
750            }
751
752            if owner_explicit.is_nan() {
753                0.0
754            } else {
755                owner_explicit
756            }
757        }
758
759        let bounds = self.bounds();
760        let owner_bounds = owner.as_ref().map(|owner| owner.bounds());
761        let width = resolve_dimension(
762            available_size.width,
763            bounds.width,
764            owner_bounds.map_or(0.0, |bounds| bounds.width),
765            owner.as_ref().map_or(f64::NAN, |owner| owner.width()),
766        );
767        let height = resolve_dimension(
768            available_size.height,
769            bounds.height,
770            owner_bounds.map_or(0.0, |bounds| bounds.height),
771            owner.as_ref().map_or(f64::NAN, |owner| owner.height()),
772        );
773        Size::new(width, height)
774    }
775
776    fn get_viewport_item_extent(&self, size: Size) -> f64 {
777        let viewport_units = self.get_viewport_units();
778        if viewport_units <= 0.0 {
779            0.0
780        } else {
781            self.get_primary_size(size) / viewport_units
782        }
783    }
784
785    fn uses_viewport_wrap_layout(&self) -> bool {
786        self.uses_viewport_fraction_layout()
787            && self.carousel().is_some_and(|carousel| carousel.wrap_selection())
788            && self.item_count() > 1
789    }
790
791    fn normalize_index(index: i32, count: i32) -> i32 {
792        ((index % count) + count) % count
793    }
794
795    fn get_nearest_logical_offset(&self, item_index: i32, reference_offset: f64) -> f64 {
796        let count = self.item_count();
797
798        if !self.uses_viewport_wrap_layout() || count == 0 {
799            return item_index.clamp(0, (count - 1).max(0)) as f64;
800        }
801
802        let wrap_span = count as f64;
803        let wrap_multiplier = ((reference_offset - item_index as f64) / wrap_span).round_ties_even();
804        item_index as f64 + (wrap_multiplier * wrap_span)
805    }
806
807    fn is_preferred_viewport_slot(
808        &self,
809        candidate_logical_index: i32,
810        existing_logical_index: i32,
811        primary_offset: f64,
812    ) -> bool {
813        let candidate_distance = (candidate_logical_index as f64 - primary_offset).abs();
814        let existing_distance = (existing_logical_index as f64 - primary_offset).abs();
815
816        if !MathUtilities::are_close(candidate_distance, existing_distance) {
817            return candidate_distance < existing_distance;
818        }
819
820        let count = self.item_count();
821        let candidate_in_range = candidate_logical_index >= 0 && candidate_logical_index < count;
822        let existing_in_range = existing_logical_index >= 0 && existing_logical_index < count;
823
824        if candidate_in_range != existing_in_range {
825            return candidate_in_range;
826        }
827
828        if self.is_dragging.get() {
829            return if self.is_forward.get() {
830                candidate_logical_index > existing_logical_index
831            } else {
832                candidate_logical_index < existing_logical_index
833            };
834        }
835
836        candidate_logical_index < existing_logical_index
837    }
838
839    /// The slots that the viewport shows at the offset, ordered by position.
840    fn get_required_viewport_slots(&self, primary_offset: f64) -> Vec<ViewportSlot> {
841        let count = self.item_count();
842
843        if count == 0 {
844            return Vec::new();
845        }
846
847        let viewport_units = self.get_viewport_units();
848        let edge_inset = (viewport_units - 1.0) / 2.0;
849        let mut start = (primary_offset - edge_inset).floor() as i32;
850        let mut end = (primary_offset + viewport_units - edge_inset).ceil() as i32 - 1;
851
852        if !self.uses_viewport_wrap_layout() {
853            start = start.max(0);
854            end = end.min(count - 1);
855
856            if start > end {
857                return Vec::new();
858            }
859
860            return (start..=end).map(|index| (index, index)).collect();
861        }
862
863        // The best slot of each item.
864        let mut best_slots: Vec<ViewportSlot> = Vec::new();
865
866        for logical_index in start..=end {
867            let item_index = Self::normalize_index(logical_index, count);
868
869            match best_slots.iter().position(|slot| slot.1 == item_index) {
870                None => best_slots.push((logical_index, item_index)),
871                Some(existing) => {
872                    if self.is_preferred_viewport_slot(logical_index, best_slots[existing].0, primary_offset) {
873                        best_slots[existing].0 = logical_index;
874                    }
875                }
876            }
877        }
878
879        best_slots.sort_by_key(|slot| slot.0);
880        best_slots
881    }
882
883    fn viewport_slots_changed(&self, old_primary_offset: f64, new_primary_offset: f64) -> bool {
884        let old_slots = self.get_required_viewport_slots(old_primary_offset);
885        let new_slots = self.get_required_viewport_slots(new_primary_offset);
886
887        old_slots != new_slots
888    }
889
890    /// Sets the scroll offset.
891    fn set_offset_core(&self, value: Vector) {
892        if self.uses_viewport_fraction_layout() {
893            let old_primary_offset = self.get_primary_offset(self.offset.get());
894            let new_primary_offset = self.get_primary_offset(value);
895
896            if MathUtilities::are_close(old_primary_offset, new_primary_offset) {
897                self.offset.set(value);
898                return;
899            }
900
901            self.offset.set(value);
902
903            let range_changed = self.viewport_slots_changed(old_primary_offset, new_primary_offset);
904
905            if range_changed {
906                self.invalidate_measure();
907            } else {
908                self.invalidate_arrange();
909            }
910
911            self.invoke_scroll_invalidated();
912            return;
913        }
914
915        if (self.offset.get().x as i32) as f64 != value.x {
916            self.invalidate_measure();
917        }
918
919        self.offset.set(value);
920    }
921
922    /// The container at the position in the fractional viewport, if there
923    /// is one.
924    #[inline]
925    fn viewport_realized_at(&self, position: usize) -> Option<ViewportRealizedItem> {
926        self.viewport_realized.borrow().get(position).cloned()
927    }
928
929    fn clear_viewport_realized(&self) {
930        if self.viewport_realized.borrow().is_empty() {
931            return;
932        }
933
934        let mut position = 0;
935        while let Some(entry) = self.viewport_realized_at(position) {
936            self.recycle_element(&entry.control);
937            position += 1;
938        }
939
940        self.viewport_realized.borrow_mut().clear();
941    }
942
943    fn reset_single_page_state(&self) {
944        let transition = self.transition.borrow().clone();
945        if let Some(transition) = transition {
946            transition.cancel();
947        }
948        *self.transition.borrow_mut() = None;
949        *self.transition_task.borrow_mut() = None;
950
951        if let Some(transition_from) = self.transition_from() {
952            self.recycle_element(&transition_from);
953        }
954
955        if let Some(swipe_target) = self.swipe_target() {
956            self.recycle_element(&swipe_target);
957        }
958
959        if let Some(realized) = self.realized() {
960            self.recycle_element(&realized);
961        }
962
963        *self.transition_from.borrow_mut() = None;
964        self.transition_from_index.set(-1);
965        *self.swipe_target.borrow_mut() = None;
966        self.swipe_target_index.set(-1);
967        *self.realized.borrow_mut() = None;
968        self.realized_index.set(-1);
969    }
970
971    fn cancel_offset_animation(&self) {
972        let offset_animation_cts = self.offset_animation_cts.borrow().clone();
973        if let Some(offset_animation_cts) = offset_animation_cts {
974            offset_animation_cts.cancel();
975        }
976        *self.offset_animation_cts.borrow_mut() = None;
977    }
978
979    fn measure_single_page_override(&self, available_size: Size) -> Size {
980        let items = self.items();
981        let item_count = items.count() as i32;
982        let index = match self.carousel() {
983            Some(carousel) => carousel.selected_index(),
984            None => self.offset.get().x as i32,
985        };
986
987        self.complete_finished_transition_if_needed();
988
989        if index != self.realized_index.get() {
990            if let Some(realized) = self.realized() {
991                // Cancel any already running transition, and recycle the
992                // element we're transitioning from.
993                let transition = self.transition.borrow().clone();
994                if let Some(transition) = transition {
995                    transition.cancel();
996                    *self.transition.borrow_mut() = None;
997                    *self.transition_task.borrow_mut() = None;
998                    if let Some(transition_from) = self.transition_from() {
999                        self.recycle_element(&transition_from);
1000                    }
1001                    *self.transition_from.borrow_mut() = None;
1002                    self.transition_from_index.set(-1);
1003                    self.reset_transition_state(Some(&realized));
1004                }
1005
1006                if self.get_transition().is_none() {
1007                    self.recycle_element(&realized);
1008                } else {
1009                    // Record the current element as the element we're
1010                    // transitioning from and we'll start the transition in
1011                    // the arrange pass.
1012                    *self.transition_from.borrow_mut() = Some(realized);
1013                    self.transition_from_index.set(self.realized_index.get());
1014                }
1015
1016                *self.realized.borrow_mut() = None;
1017                self.realized_index.set(-1);
1018            }
1019
1020            if index >= 0 && index < item_count {
1021                let element = self.get_or_create_element(&items, index);
1022                *self.realized.borrow_mut() = Some(element);
1023                self.realized_index.set(index);
1024            }
1025        }
1026
1027        let Some(realized) = self.realized() else {
1028            self.set_viewport(Size::new(0.0, 0.0));
1029            self.set_extent(Size::new(0.0, 0.0));
1030            *self.transition_from.borrow_mut() = None;
1031            self.transition_from_index.set(-1);
1032            return Size::default();
1033        };
1034
1035        realized.measure(available_size);
1036        self.set_extent(Size::new(item_count as f64, 1.0));
1037        self.set_viewport(Size::new(1.0, 1.0));
1038
1039        realized.desired_size()
1040    }
1041
1042    fn measure_viewport_fraction_override(&self, available_size: Size) -> Size {
1043        self.reset_single_page_state();
1044
1045        let items = self.items();
1046        let item_count = items.count() as i32;
1047
1048        if item_count == 0 {
1049            self.clear_viewport_realized();
1050            self.set_viewport(Size::new(0.0, 0.0));
1051            self.set_extent(Size::new(0.0, 0.0));
1052            return Size::default();
1053        }
1054
1055        let layout_size = self.resolve_layout_size(available_size);
1056        let primary_size = self.get_primary_size(layout_size);
1057        let cross_size = self.get_cross_size(layout_size);
1058        let viewport_units = self.get_viewport_units();
1059
1060        if primary_size <= 0.0 || viewport_units <= 0.0 {
1061            self.clear_viewport_realized();
1062            self.set_viewport(Size::new(0.0, 0.0));
1063            self.set_extent(Size::new(0.0, 0.0));
1064            return Size::default();
1065        }
1066
1067        let item_primary_size = primary_size / viewport_units;
1068        let item_size = self.create_item_size(item_primary_size, cross_size);
1069        let required_slots = self.get_required_viewport_slots(self.get_primary_offset(self.offset.get()));
1070
1071        // Recycle the containers of the slots that are no longer required.
1072        let mut position = 0;
1073        while let Some(entry) = self.viewport_realized_at(position) {
1074            let required =
1075                required_slots.iter().any(|slot| slot.0 == entry.logical_index && slot.1 == entry.item_index);
1076
1077            if required {
1078                position += 1;
1079            } else {
1080                self.recycle_element(&entry.control);
1081
1082                let mut viewport_realized = self.viewport_realized.borrow_mut();
1083                match viewport_realized.iter().position(|x| x.logical_index == entry.logical_index) {
1084                    Some(removed) => {
1085                        viewport_realized.remove(removed);
1086                        if removed > position {
1087                            position += 1;
1088                        }
1089                    }
1090                    None => position += 1,
1091                }
1092            }
1093        }
1094
1095        for &(logical_index, item_index) in &required_slots {
1096            let is_realized = self.viewport_realized.borrow().iter().any(|x| x.logical_index == logical_index);
1097
1098            if !is_realized {
1099                let control = self.get_or_create_element(&items, item_index);
1100                let mut viewport_realized = self.viewport_realized.borrow_mut();
1101
1102                match viewport_realized.binary_search_by_key(&logical_index, |x| x.logical_index) {
1103                    Ok(existing) => viewport_realized[existing] = ViewportRealizedItem { logical_index, item_index, control },
1104                    Err(insert_at) => {
1105                        viewport_realized.insert(insert_at, ViewportRealizedItem { logical_index, item_index, control })
1106                    }
1107                }
1108            }
1109        }
1110
1111        let mut max_cross_desired_size: f64 = 0.0;
1112
1113        let mut position = 0;
1114        while let Some(entry) = self.viewport_realized_at(position) {
1115            entry.control.measure(item_size);
1116            max_cross_desired_size = max_cross_desired_size.max(self.get_cross_size(entry.control.desired_size()));
1117            position += 1;
1118        }
1119
1120        self.set_viewport(self.create_logical_size(viewport_units));
1121        self.set_extent(self.create_logical_size((item_count as f64 + viewport_units - 1.0).max(0.0)));
1122
1123        let desired_primary = if primary_size.is_infinite() { item_primary_size * viewport_units } else { primary_size };
1124        let desired_cross = if cross_size.is_infinite() { max_cross_desired_size } else { cross_size };
1125        self.create_item_size(desired_primary, desired_cross)
1126    }
1127
1128    fn arrange_viewport_fraction_override(&self, final_size: Size) -> Size {
1129        let primary_size = self.get_primary_size(final_size);
1130        let cross_size = self.get_cross_size(final_size);
1131        let viewport_units = self.get_viewport_units();
1132
1133        if primary_size <= 0.0 || viewport_units <= 0.0 {
1134            return final_size;
1135        }
1136
1137        if self.viewport_realized.borrow().is_empty() && self.item_count() > 0 {
1138            self.invalidate_measure();
1139            return final_size;
1140        }
1141
1142        let item_primary_size = primary_size / viewport_units;
1143        let edge_inset = (viewport_units - 1.0) / 2.0;
1144        let primary_offset = self.get_primary_offset(self.offset.get());
1145
1146        let mut position = 0;
1147        while let Some(entry) = self.viewport_realized_at(position) {
1148            let item_offset = (edge_inset + entry.logical_index as f64 - primary_offset) * item_primary_size;
1149            let rect = self.create_item_rect(item_offset, item_primary_size, cross_size);
1150            entry.control.set_is_visible(true);
1151            entry.control.arrange(rect);
1152            position += 1;
1153        }
1154
1155        final_size
1156    }
1157
1158    fn get_or_create_element(&self, items: &ItemsSourceView, index: i32) -> Ref<Control> {
1159        if let Some(element) = self.get_realized_element(index) {
1160            return element;
1161        }
1162
1163        let item = items.get_at(index as usize);
1164        let generator = self.generator();
1165        let (needs_container, recycle_key) = generator.needs_container(&item, index);
1166
1167        if needs_container {
1168            match self.get_recycled_element(&generator, &item, index, recycle_key) {
1169                Some(recycled) => recycled,
1170                None => self.create_element(&generator, &item, index, recycle_key),
1171            }
1172        } else {
1173            self.get_item_as_own_container(&generator, &item, index)
1174        }
1175    }
1176
1177    fn get_realized_element(&self, index: i32) -> Option<Ref<Control>> {
1178        if let Some(viewport_realized) = self.find_viewport_control(index) {
1179            return Some(viewport_realized);
1180        }
1181
1182        if self.realized_index.get() == index {
1183            self.realized()
1184        } else {
1185            None
1186        }
1187    }
1188
1189    fn get_item_as_own_container(
1190        &self,
1191        generator: &ItemContainerGenerator,
1192        item: &Option<BoxedValue>,
1193        index: i32,
1194    ) -> Ref<Control> {
1195        let control_item = match item.as_ref().and_then(Control::from_boxed) {
1196            Some(control_item) => control_item,
1197            None => panic!("An item that is its own container must be a control."),
1198        };
1199
1200        if !control_item.is_set(Self::recycle_key_property().as_property()) {
1201            generator.prepare_item_container(&control_item, item, index);
1202            self.add_internal_child(&control_item);
1203            control_item.set_value(Self::recycle_key_property(), Some(Self::item_is_its_own_container()));
1204            generator.item_container_prepared(&control_item, item, index);
1205        }
1206
1207        control_item.set_is_visible(true);
1208        control_item
1209    }
1210
1211    fn get_recycled_element(
1212        &self,
1213        generator: &ItemContainerGenerator,
1214        item: &Option<BoxedValue>,
1215        index: i32,
1216        recycle_key: Option<RecycleKey>,
1217    ) -> Option<Ref<Control>> {
1218        let recycle_key = recycle_key?;
1219
1220        let recycled = self.recycle_pool.borrow_mut().get_mut(&recycle_key).and_then(Vec::pop)?;
1221
1222        recycled.set_is_visible(true);
1223        generator.prepare_item_container(&recycled, item, index);
1224        generator.item_container_prepared(&recycled, item, index);
1225        Some(recycled)
1226    }
1227
1228    fn create_element(
1229        &self,
1230        generator: &ItemContainerGenerator,
1231        item: &Option<BoxedValue>,
1232        index: i32,
1233        recycle_key: Option<RecycleKey>,
1234    ) -> Ref<Control> {
1235        let container = generator.create_container(item, index, recycle_key);
1236
1237        container.set_value(Self::recycle_key_property(), recycle_key);
1238        generator.prepare_item_container(&container, item, index);
1239        self.add_internal_child(&container);
1240        generator.item_container_prepared(&container, item, index);
1241
1242        container
1243    }
1244
1245    fn recycle_element(&self, element: &Ref<Control>) {
1246        let recycle_key = element.get_value(Self::recycle_key_property());
1247        debug_assert!(recycle_key.is_some());
1248
1249        // Hide first so cleanup doesn't visibly snap transforms/opacity for
1250        // a frame.
1251        element.set_is_visible(false);
1252        self.reset_transition_state(Some(element));
1253
1254        if recycle_key == Some(Self::item_is_its_own_container()) {
1255            return;
1256        }
1257
1258        self.generator().clear_item_container(element);
1259
1260        let Some(recycle_key) = recycle_key else {
1261            panic!("The recycle key of a container of the carousel panel cannot be null.");
1262        };
1263
1264        self.recycle_pool.borrow_mut().entry(recycle_key).or_default().push(element.clone());
1265    }
1266
1267    fn get_transition(&self) -> Option<Rc<dyn IPageTransition>> {
1268        self.carousel().and_then(|carousel| carousel.page_transition())
1269    }
1270
1271    fn complete_finished_transition_if_needed(&self) {
1272        let is_task_completed = self.transition_task.borrow().as_ref().is_some_and(|task| task.is_completed());
1273
1274        if self.transition.borrow().is_some() && is_task_completed {
1275            if let Some(transition_from) = self.transition_from() {
1276                self.recycle_element(&transition_from);
1277            }
1278
1279            *self.transition.borrow_mut() = None;
1280            *self.transition_task.borrow_mut() = None;
1281            *self.transition_from.borrow_mut() = None;
1282            self.transition_from_index.set(-1);
1283        }
1284    }
1285
1286    /// The scheduler that runs the asynchronous methods of the panel: the
1287    /// one of the current synchronization context.
1288    fn task_scheduler() -> DispatcherTaskScheduler {
1289        let context = FerroSynchronizationContext::current().unwrap_or_else(|| {
1290            FerroSynchronizationContext::with_dispatcher(&Dispatcher::current_dispatcher(), DispatcherPriority::NORMAL)
1291        });
1292        context.to_task_scheduler()
1293    }
1294
1295    fn run_transition_async(
1296        &self,
1297        transition_cts: Rc<CancellationTokenSource>,
1298        transition_from: Ref<Control>,
1299        transition_to: Ref<Control>,
1300        forward: bool,
1301        transition: Rc<dyn IPageTransition>,
1302    ) -> DispatcherTask<()> {
1303        let this = self.to_ref().downgrade();
1304
1305        Self::task_scheduler().start_local(async move {
1306            let from: Ref<Visual> = transition_from.upcast();
1307            let to: Ref<Visual> = transition_to.upcast();
1308            let task = transition.start(Some(&from), Some(&to), forward, transition_cts.token());
1309
1310            // An interruption is expected when a transition is interrupted
1311            // by a newer navigation action.
1312            TransitionEnd { task }.await;
1313
1314            let Some(this) = this.upgrade() else {
1315                return;
1316            };
1317
1318            let is_current =
1319                this.transition.borrow().as_ref().is_some_and(|current| Rc::ptr_eq(current, &transition_cts));
1320            if transition_cts.is_cancellation_requested() || !is_current {
1321                return;
1322            }
1323
1324            if let Some(transition_from) = this.transition_from() {
1325                this.recycle_element(&transition_from);
1326            }
1327            *this.transition.borrow_mut() = None;
1328            *this.transition_task.borrow_mut() = None;
1329            *this.transition_from.borrow_mut() = None;
1330            this.transition_from_index.set(-1);
1331        })
1332    }
1333
1334    pub(crate) fn sync_selection_offset(&self, selected_index: i32) {
1335        if !self.uses_viewport_fraction_layout() {
1336            self.set_offset_core(self.with_primary_offset(self.offset.get(), selected_index as f64));
1337            return;
1338        }
1339
1340        let current_offset = self.get_primary_offset(self.offset.get());
1341        let target_offset = self.get_nearest_logical_offset(selected_index, current_offset);
1342
1343        if MathUtilities::are_close(current_offset, target_offset) {
1344            self.set_offset_core(self.with_primary_offset(self.offset.get(), target_offset));
1345            return;
1346        }
1347
1348        if self.is_dragging.get() {
1349            return;
1350        }
1351
1352        let transition = self.get_transition();
1353        let can_animate = transition.is_some() && (target_offset - current_offset).abs() <= 1.001;
1354
1355        if !can_animate {
1356            self.reset_viewport_transition_state();
1357            self.clear_fractional_progress_context();
1358            self.set_offset_core(self.with_primary_offset(self.offset.get(), target_offset));
1359            return;
1360        }
1361
1362        let item_count = self.item_count();
1363        let from_index = if item_count > 0 {
1364            Self::normalize_index(current_offset.round_ties_even() as i32, item_count)
1365        } else {
1366            -1
1367        };
1368        let forward = target_offset > current_offset;
1369
1370        self.reset_viewport_transition_state();
1371        self.set_fractional_progress_context(from_index, selected_index, forward, current_offset, target_offset);
1372        self.animate_viewport_offset_async(
1373            current_offset,
1374            target_offset,
1375            TimeSpan::from_seconds(MAX_COMPLETION_DURATION),
1376            QuadraticEaseOut::new().into(),
1377            |this| {
1378                this.reset_viewport_transition_state();
1379                this.clear_fractional_progress_context();
1380                // The carousel does not sync the scroll offset during the
1381                // animation and the layout after the animation still sees a
1382                // live cancellation source, so re-sync explicitly in case
1383                // the selected index changed.
1384                if let Some(carousel) = this.carousel() {
1385                    this.sync_selection_offset(carousel.selected_index());
1386                }
1387            },
1388        );
1389    }
1390
1391    /// Refreshes the gesture recognizer based on the carousel's
1392    /// `is_swipe_enabled` and `page_transition` settings.
1393    pub(crate) fn refresh_gesture_recognizer(&self) {
1394        self.teardown_gesture_recognizer();
1395
1396        let Some(carousel) = self.carousel() else {
1397            return;
1398        };
1399        if !carousel.is_swipe_enabled() {
1400            return;
1401        }
1402
1403        let swipe_axis = if self.uses_viewport_fraction_layout() {
1404            Some(carousel.get_layout_axis())
1405        } else {
1406            carousel.get_transition_axis()
1407        };
1408        self.swipe_axis.set(swipe_axis);
1409
1410        let swipe_gesture_recognizer = SwipeGestureRecognizer::new();
1411        swipe_gesture_recognizer.set_can_horizontally_swipe(swipe_axis != Some(SlideAxis::Vertical));
1412        swipe_gesture_recognizer.set_can_vertically_swipe(swipe_axis != Some(SlideAxis::Horizontal));
1413        *self.swipe_gesture_recognizer.borrow_mut() = Some(swipe_gesture_recognizer.clone());
1414
1415        self.gesture_recognizers().add(swipe_gesture_recognizer);
1416
1417        let this = self.to_ref().downgrade();
1418        let handler = self.add_handler(InputElement::swipe_gesture_event(), move |_, e| {
1419            if let Some(this) = this.upgrade() {
1420                this.on_swipe_gesture(e);
1421            }
1422        });
1423        self.swipe_gesture_handler.set(Some(handler));
1424
1425        let this = self.to_ref().downgrade();
1426        let handler = self.add_handler(InputElement::swipe_gesture_ended_event(), move |_, e| {
1427            if let Some(this) = this.upgrade() {
1428                this.on_swipe_gesture_ended(e);
1429            }
1430        });
1431        self.swipe_gesture_ended_handler.set(Some(handler));
1432    }
1433
1434    fn teardown_gesture_recognizer(&self) {
1435        let completion_cts = self.completion_cts.borrow().clone();
1436        if let Some(completion_cts) = completion_cts {
1437            completion_cts.cancel();
1438        }
1439        *self.completion_cts.borrow_mut() = None;
1440        self.cancel_offset_animation();
1441
1442        let swipe_gesture_recognizer = self.swipe_gesture_recognizer.take();
1443        if let Some(swipe_gesture_recognizer) = swipe_gesture_recognizer {
1444            self.gesture_recognizers().remove(&swipe_gesture_recognizer);
1445        }
1446
1447        if let Some(handler) = self.swipe_gesture_handler.take() {
1448            self.remove_handler(InputElement::swipe_gesture_event(), handler);
1449        }
1450        if let Some(handler) = self.swipe_gesture_ended_handler.take() {
1451            self.remove_handler(InputElement::swipe_gesture_ended_event(), handler);
1452        }
1453        self.reset_swipe_state();
1454    }
1455
1456    fn find_viewport_control(&self, item_index: i32) -> Option<Ref<Control>> {
1457        let viewport_realized = self.viewport_realized.borrow();
1458        viewport_realized.iter().find(|x| x.item_index == item_index).map(|x| x.control.clone())
1459    }
1460
1461    fn set_fractional_progress_context(
1462        &self,
1463        from_index: i32,
1464        to_index: i32,
1465        forward: bool,
1466        start_offset: f64,
1467        target_offset: f64,
1468    ) {
1469        self.progress_from_index.set(from_index);
1470        self.progress_to_index.set(to_index);
1471        self.is_forward.set(forward);
1472        self.progress_start_offset.set(start_offset);
1473        self.active_viewport_target_offset.set(target_offset);
1474    }
1475
1476    fn clear_fractional_progress_context(&self) {
1477        self.progress_from_index.set(-1);
1478        self.progress_to_index.set(-1);
1479        self.progress_start_offset.set(0.0);
1480        self.active_viewport_target_offset.set(0.0);
1481    }
1482
1483    fn get_fractional_transition_progress(&self, current_offset: f64) -> f64 {
1484        let total_distance = (self.active_viewport_target_offset.get() - self.progress_start_offset.get()).abs();
1485        if total_distance <= 0.0 {
1486            return 0.0;
1487        }
1488
1489        ((current_offset - self.progress_start_offset.get()).abs() / total_distance).clamp(0.0, 1.0)
1490    }
1491
1492    fn reset_viewport_transition_state(&self) {
1493        let mut position = 0;
1494        while let Some(entry) = self.viewport_realized_at(position) {
1495            self.reset_transition_state(Some(&entry.control));
1496            position += 1;
1497        }
1498    }
1499
1500    fn on_swipe_gesture(&self, e: &SwipeGestureEventArgs) {
1501        let Some(carousel) = self.carousel() else {
1502            return;
1503        };
1504        if !carousel.is_swipe_enabled() {
1505            return;
1506        }
1507
1508        if self.uses_viewport_fraction_layout() {
1509            self.on_viewport_fraction_swipe_gesture(&carousel, e);
1510            return;
1511        }
1512
1513        let item_count = self.item_count();
1514
1515        if self.realized_index.get() < 0 || item_count == 0 {
1516            return;
1517        }
1518
1519        if Self::is_active(&self.completion_cts) {
1520            let completion_cts = self.completion_cts.take();
1521            if let Some(completion_cts) = completion_cts {
1522                completion_cts.cancel();
1523            }
1524
1525            let was_commit = self.completion_end_progress.get() > 0.5;
1526            let swipe_target = self.swipe_target();
1527            match swipe_target {
1528                Some(swipe_target) if was_commit => {
1529                    if let Some(realized) = self.realized() {
1530                        self.recycle_element(&realized);
1531                    }
1532
1533                    *self.realized.borrow_mut() = Some(swipe_target);
1534                    self.realized_index.set(self.swipe_target_index.get());
1535                    carousel.set_selected_index(self.swipe_target_index.get());
1536                }
1537                _ => self.reset_swipe_state(),
1538            }
1539
1540            *self.swipe_target.borrow_mut() = None;
1541            self.swipe_target_index.set(-1);
1542            self.total_delta.set(0.0);
1543        }
1544
1545        if self.is_dragging.get() && e.id() != self.swipe_gesture_id.get() {
1546            return;
1547        }
1548
1549        if !self.is_dragging.get() {
1550            // Lock the axis on gesture start to keep diagonal drags stable.
1551            self.locked_axis.set(self.swipe_axis.get().unwrap_or(if e.delta().x.abs() >= e.delta().y.abs() {
1552                SlideAxis::Horizontal
1553            } else {
1554                SlideAxis::Vertical
1555            }));
1556        }
1557
1558        let delta = if self.locked_axis.get() == SlideAxis::Horizontal { e.delta().x } else { e.delta().y };
1559
1560        if !self.is_dragging.get() {
1561            self.is_forward.set(delta > 0.0);
1562            self.is_rubber_banding.set(false);
1563            let current_index = self.realized_index.get();
1564            let mut target_index = if self.is_forward.get() { current_index + 1 } else { current_index - 1 };
1565
1566            if target_index >= item_count {
1567                if carousel.wrap_selection() {
1568                    target_index = 0;
1569                } else {
1570                    self.is_rubber_banding.set(true);
1571                }
1572            } else if target_index < 0 {
1573                if carousel.wrap_selection() {
1574                    target_index = item_count - 1;
1575                } else {
1576                    self.is_rubber_banding.set(true);
1577                }
1578            }
1579
1580            if !self.is_rubber_banding.get()
1581                && (target_index == current_index || target_index < 0 || target_index >= item_count)
1582            {
1583                return;
1584            }
1585
1586            self.is_dragging.set(true);
1587            self.swipe_gesture_id.set(e.id());
1588            self.total_delta.set(0.0);
1589            self.swipe_target_index.set(if self.is_rubber_banding.get() { -1 } else { target_index });
1590            carousel.set_is_swiping(true);
1591
1592            let transition = self.transition.borrow().clone();
1593            if let Some(transition) = transition {
1594                transition.cancel();
1595                *self.transition.borrow_mut() = None;
1596                if let Some(transition_from) = self.transition_from() {
1597                    self.recycle_element(&transition_from);
1598                }
1599                *self.transition_from.borrow_mut() = None;
1600                self.transition_from_index.set(-1);
1601            }
1602
1603            if !self.is_rubber_banding.get() {
1604                let swipe_target = self.get_or_create_element(&self.items(), self.swipe_target_index.get());
1605                *self.swipe_target.borrow_mut() = Some(swipe_target.clone());
1606                let size = self.bounds().size();
1607                swipe_target.measure(size);
1608                swipe_target.arrange(Rect::from_size(size));
1609                swipe_target.set_is_visible(true);
1610            }
1611        }
1612
1613        self.accumulate_delta(delta);
1614
1615        let size = self.locked_axis_size();
1616        if size <= 0.0 {
1617            return;
1618        }
1619
1620        let raw_progress = (self.total_delta.get().abs() / size).clamp(0.0, 1.0);
1621        let progress =
1622            if self.is_rubber_banding.get() { RUBBER_BAND_FACTOR * raw_progress.sqrt() } else { raw_progress };
1623
1624        let transition = self.get_transition();
1625        if let Some(progressive) = transition.as_ref().and_then(|t| t.as_progress_page_transition()) {
1626            let swipe_target = if self.is_rubber_banding.get() { None } else { self.swipe_target() };
1627            progressive.update(
1628                progress,
1629                Self::as_visual(self.realized()).as_ref(),
1630                Self::as_visual(swipe_target).as_ref(),
1631                self.is_forward.get(),
1632                size,
1633                &[],
1634            );
1635        }
1636
1637        e.set_handled(true);
1638    }
1639
1640    /// Adds the delta of a swipe event to the total, clamped so that the
1641    /// total cannot cross zero (absorbs touch jitter).
1642    fn accumulate_delta(&self, delta: f64) {
1643        let total_delta = self.total_delta.get() + delta;
1644
1645        self.total_delta.set(if self.is_forward.get() { total_delta.max(0.0) } else { total_delta.min(0.0) });
1646    }
1647
1648    fn on_viewport_fraction_swipe_gesture(&self, carousel: &Ref<Carousel>, e: &SwipeGestureEventArgs) {
1649        if Self::is_active(&self.offset_animation_cts) {
1650            self.cancel_offset_animation();
1651            self.set_offset_core(self.with_primary_offset(
1652                self.offset.get(),
1653                self.get_nearest_logical_offset(carousel.selected_index(), self.get_primary_offset(self.offset.get())),
1654            ));
1655        }
1656
1657        if self.is_dragging.get() && e.id() != self.swipe_gesture_id.get() {
1658            return;
1659        }
1660
1661        let delta = if self.locked_axis.get() == SlideAxis::Horizontal { e.delta().x } else { e.delta().y };
1662        let item_count = self.item_count();
1663
1664        if !self.is_dragging.get() {
1665            self.locked_axis.set(carousel.get_layout_axis());
1666            self.swipe_gesture_id.set(e.id());
1667            self.drag_start_offset.set(
1668                self.get_nearest_logical_offset(carousel.selected_index(), self.get_primary_offset(self.offset.get())),
1669            );
1670            self.total_delta.set(0.0);
1671            self.is_dragging.set(true);
1672            self.is_rubber_banding.set(false);
1673            carousel.set_is_swiping(true);
1674            self.is_forward.set(delta > 0.0);
1675            let mut target_index =
1676                if self.is_forward.get() { carousel.selected_index() + 1 } else { carousel.selected_index() - 1 };
1677
1678            if target_index >= item_count || target_index < 0 {
1679                if carousel.wrap_selection() && item_count > 1 {
1680                    target_index = Self::normalize_index(target_index, item_count);
1681                } else {
1682                    self.is_rubber_banding.set(true);
1683                }
1684            }
1685
1686            let drag_start_offset = self.drag_start_offset.get();
1687            let target_offset = if self.is_forward.get() { drag_start_offset + 1.0 } else { drag_start_offset - 1.0 };
1688            self.set_fractional_progress_context(
1689                carousel.selected_index(),
1690                if self.is_rubber_banding.get() { -1 } else { target_index },
1691                self.is_forward.get(),
1692                drag_start_offset,
1693                target_offset,
1694            );
1695            self.reset_viewport_transition_state();
1696        }
1697
1698        self.accumulate_delta(delta);
1699
1700        let item_extent = self.get_viewport_item_extent(self.bounds().size());
1701        if item_extent <= 0.0 {
1702            return;
1703        }
1704
1705        let drag_start_offset = self.drag_start_offset.get();
1706        let logical_delta = (self.total_delta.get().abs() / item_extent).clamp(0.0, 1.0);
1707        let mut proposed_offset = drag_start_offset + if self.is_forward.get() { logical_delta } else { -logical_delta };
1708
1709        if !self.is_rubber_banding.get() {
1710            let active_viewport_target_offset = self.active_viewport_target_offset.get();
1711            proposed_offset = proposed_offset.clamp(
1712                drag_start_offset.min(active_viewport_target_offset),
1713                drag_start_offset.max(active_viewport_target_offset),
1714            );
1715        } else if proposed_offset < 0.0 {
1716            proposed_offset = -(RUBBER_BAND_FACTOR * (-proposed_offset).sqrt());
1717        } else {
1718            let max_offset = (item_count - 1).max(0) as f64;
1719            proposed_offset = max_offset + (RUBBER_BAND_FACTOR * (proposed_offset - max_offset).sqrt());
1720        }
1721
1722        self.set_offset_core(self.with_primary_offset(self.offset.get(), proposed_offset));
1723
1724        let transition = self.get_transition();
1725        if let Some(progressive) = transition.as_ref().and_then(|t| t.as_progress_page_transition()) {
1726            let current_offset = self.get_primary_offset(self.offset.get());
1727            let progress = (current_offset - drag_start_offset).abs().clamp(0.0, 1.0);
1728            progressive.update(
1729                progress,
1730                Self::as_visual(self.find_viewport_control(self.progress_from_index.get())).as_ref(),
1731                Self::as_visual(self.find_viewport_control(self.progress_to_index.get())).as_ref(),
1732                self.is_forward.get(),
1733                self.get_viewport_item_extent(self.bounds().size()),
1734                &self.build_fractional_visible_items(current_offset),
1735            );
1736        }
1737
1738        e.set_handled(true);
1739    }
1740
1741    fn on_viewport_fraction_swipe_gesture_ended(&self, carousel: Ref<Carousel>, e: &SwipeGestureEndedEventArgs) {
1742        let item_extent = self.get_viewport_item_extent(self.bounds().size());
1743        let current_offset = self.get_primary_offset(self.offset.get());
1744        let current_progress = (current_offset - self.drag_start_offset.get()).abs();
1745        let velocity =
1746            if self.locked_axis.get() == SlideAxis::Horizontal { e.velocity().x.abs() } else { e.velocity().y.abs() };
1747        let target_index = self.progress_to_index.get();
1748        let is_rubber_banding = self.is_rubber_banding.get();
1749        let can_commit = !is_rubber_banding && target_index >= 0;
1750        let commit = can_commit
1751            && (current_progress >= SWIPE_COMMIT_THRESHOLD
1752                || (velocity > VELOCITY_COMMIT_THRESHOLD && current_progress >= MIN_SWIPE_DISTANCE_FOR_VELOCITY_COMMIT));
1753        let end_offset = if commit {
1754            self.active_viewport_target_offset.get()
1755        } else {
1756            self.get_nearest_logical_offset(carousel.selected_index(), current_offset)
1757        };
1758        let remaining_distance = (end_offset - current_offset).abs();
1759        let duration_seconds = if is_rubber_banding {
1760            RUBBER_BAND_RETURN_DURATION
1761        } else if velocity > 0.0 && item_extent > 0.0 {
1762            (remaining_distance * item_extent / velocity).clamp(MIN_COMPLETION_DURATION, MAX_COMPLETION_DURATION)
1763        } else {
1764            MAX_COMPLETION_DURATION
1765        };
1766        let easing: Easing = if is_rubber_banding { SineEaseOut::new().into() } else { QuadraticEaseOut::new().into() };
1767
1768        self.is_dragging.set(false);
1769        self.animate_viewport_offset_async(
1770            current_offset,
1771            end_offset,
1772            TimeSpan::from_seconds(duration_seconds),
1773            easing,
1774            move |this| {
1775                this.total_delta.set(0.0);
1776                this.is_rubber_banding.set(false);
1777                carousel.set_is_swiping(false);
1778
1779                if commit {
1780                    this.set_offset_core(this.with_primary_offset(
1781                        this.offset.get(),
1782                        this.get_nearest_logical_offset(target_index, end_offset),
1783                    ));
1784                    carousel.set_selected_index(target_index);
1785                } else {
1786                    this.set_offset_core(this.with_primary_offset(
1787                        this.offset.get(),
1788                        this.get_nearest_logical_offset(carousel.selected_index(), end_offset),
1789                    ));
1790                }
1791
1792                this.reset_viewport_transition_state();
1793                this.clear_fractional_progress_context();
1794            },
1795        );
1796    }
1797
1798    /// An animation of a progress property of the panel between two values.
1799    fn create_progress_animation(
1800        property: &'static StyledProperty<f64>,
1801        from: f64,
1802        to: f64,
1803        duration: TimeSpan,
1804        easing: Easing,
1805    ) -> Ref<Animation> {
1806        let animation = Animation::new();
1807        animation.set_fill_mode(FillMode::Forward);
1808        animation.set_duration(duration);
1809        animation.set_easing(easing);
1810        animation.children().add(KeyFrame::with_cue(Cue::new(0.0), [Setter::new(property, from) as _]));
1811        animation.children().add(KeyFrame::with_cue(Cue::new(1.0), [Setter::new(property, to) as _]));
1812        animation
1813    }
1814
1815    fn animate_viewport_offset_async(
1816        &self,
1817        from_offset: f64,
1818        to_offset: f64,
1819        duration: TimeSpan,
1820        easing: Easing,
1821        on_completed: impl FnOnce(&Self) + 'static,
1822    ) {
1823        self.cancel_offset_animation();
1824        let offset_animation_cts = Rc::new(CancellationTokenSource::new());
1825        *self.offset_animation_cts.borrow_mut() = Some(offset_animation_cts.clone());
1826        let cancellation_token = offset_animation_cts.token();
1827
1828        let animation =
1829            Self::create_progress_animation(Self::offset_animation_progress_property(), 0.0, 1.0, duration, easing);
1830
1831        self.offset_animation_start.set(from_offset);
1832        self.offset_animation_target.set(to_offset);
1833        self.set_value(Self::offset_animation_progress_property(), 0.0);
1834
1835        let this = self.to_ref().downgrade();
1836        let _finally = ClearSourceOnExit {
1837            panel: this.clone(),
1838            field: |panel| &panel.offset_animation_cts,
1839            source: offset_animation_cts,
1840        };
1841        let run = animation.run_async(self, cancellation_token.clone());
1842
1843        let _ = Self::task_scheduler().start_local(async move {
1844            let _finally = _finally;
1845
1846            if run.await.is_err() {
1847                return;
1848            }
1849
1850            if cancellation_token.is_cancellation_requested() {
1851                return;
1852            }
1853
1854            let Some(this) = this.upgrade() else {
1855                return;
1856            };
1857
1858            this.set_offset_core(this.with_primary_offset(this.offset.get(), to_offset));
1859
1860            if this.uses_viewport_fraction_layout() {
1861                let transition = this.get_transition();
1862                if let Some(progressive) = transition.as_ref().and_then(|t| t.as_progress_page_transition()) {
1863                    let transition_progress = this.get_fractional_transition_progress(to_offset);
1864                    progressive.update(
1865                        transition_progress,
1866                        Self::as_visual(this.find_viewport_control(this.progress_from_index.get())).as_ref(),
1867                        Self::as_visual(this.find_viewport_control(this.progress_to_index.get())).as_ref(),
1868                        this.is_forward.get(),
1869                        this.get_viewport_item_extent(this.bounds().size()),
1870                        &this.build_fractional_visible_items(to_offset),
1871                    );
1872                }
1873            }
1874
1875            on_completed(&this);
1876        });
1877    }
1878
1879    fn on_swipe_gesture_ended(&self, e: &SwipeGestureEndedEventArgs) {
1880        if !self.is_dragging.get() || e.id() != self.swipe_gesture_id.get() {
1881            return;
1882        }
1883        let Some(carousel) = self.carousel() else {
1884            return;
1885        };
1886
1887        if self.uses_viewport_fraction_layout() {
1888            self.on_viewport_fraction_swipe_gesture_ended(carousel, e);
1889            return;
1890        }
1891
1892        let is_rubber_banding = self.is_rubber_banding.get();
1893        let size = self.locked_axis_size();
1894        let raw_progress = if size > 0.0 { self.total_delta.get().abs() / size } else { 0.0 };
1895        let current_progress = if is_rubber_banding { RUBBER_BAND_FACTOR * raw_progress.sqrt() } else { raw_progress };
1896        let velocity =
1897            if self.locked_axis.get() == SlideAxis::Horizontal { e.velocity().x.abs() } else { e.velocity().y.abs() };
1898        let commit = !is_rubber_banding
1899            && (current_progress >= SWIPE_COMMIT_THRESHOLD
1900                || (velocity > VELOCITY_COMMIT_THRESHOLD && current_progress >= MIN_SWIPE_DISTANCE_FOR_VELOCITY_COMMIT))
1901            && self.swipe_target.borrow().is_some();
1902
1903        self.completion_end_progress.set(if commit { 1.0 } else { 0.0 });
1904        let completion_end_progress = self.completion_end_progress.get();
1905        let remaining_distance = (completion_end_progress - current_progress).abs();
1906        let duration_seconds = if is_rubber_banding {
1907            RUBBER_BAND_RETURN_DURATION
1908        } else if velocity > 0.0 {
1909            (remaining_distance * size / velocity).clamp(MIN_COMPLETION_DURATION, MAX_COMPLETION_DURATION)
1910        } else {
1911            MAX_COMPLETION_DURATION
1912        };
1913        let easing: Easing = if is_rubber_banding { SineEaseOut::new().into() } else { QuadraticEaseOut::new().into() };
1914
1915        let previous_completion_cts = self.completion_cts.borrow().clone();
1916        if let Some(previous_completion_cts) = previous_completion_cts {
1917            previous_completion_cts.cancel();
1918        }
1919        let completion_cts = Rc::new(CancellationTokenSource::new());
1920        *self.completion_cts.borrow_mut() = Some(completion_cts.clone());
1921
1922        self.set_value(Self::completion_progress_property(), current_progress);
1923
1924        let animation = Self::create_progress_animation(
1925            Self::completion_progress_property(),
1926            current_progress,
1927            completion_end_progress,
1928            TimeSpan::from_seconds(duration_seconds),
1929            easing,
1930        );
1931
1932        self.is_dragging.set(false);
1933        self.run_completion_animation(animation, carousel, completion_cts);
1934    }
1935
1936    fn run_completion_animation(
1937        &self,
1938        animation: Ref<Animation>,
1939        carousel: Ref<Carousel>,
1940        completion_cts: Rc<CancellationTokenSource>,
1941    ) {
1942        let cancellation_token = completion_cts.token();
1943
1944        let this = self.to_ref().downgrade();
1945        let _finally =
1946            ClearSourceOnExit { panel: this.clone(), field: |panel| &panel.completion_cts, source: completion_cts };
1947        let run = animation.run_async(self, cancellation_token.clone());
1948
1949        let _ = Self::task_scheduler().start_local(async move {
1950            let _finally = _finally;
1951
1952            if run.await.is_err() {
1953                return;
1954            }
1955
1956            if cancellation_token.is_cancellation_requested() {
1957                return;
1958            }
1959
1960            let Some(this) = this.upgrade() else {
1961                return;
1962            };
1963
1964            let transition = this.get_transition();
1965            if let Some(progressive) = transition.as_ref().and_then(|t| t.as_progress_page_transition()) {
1966                let realized = this.realized();
1967                let swipe_target = this.swipe_target().filter(|target| realized.as_ref() != Some(target));
1968                let size = this.locked_axis_size();
1969                progressive.update(
1970                    this.completion_end_progress.get(),
1971                    Self::as_visual(realized).as_ref(),
1972                    Self::as_visual(swipe_target).as_ref(),
1973                    this.is_forward.get(),
1974                    size,
1975                    &[],
1976                );
1977            }
1978
1979            let commit = this.completion_end_progress.get() > 0.5;
1980
1981            match this.swipe_target() {
1982                Some(target_element) if commit => {
1983                    let target_index = this.swipe_target_index.get();
1984
1985                    // Clear swipe target state before promoting it to the
1986                    // realized element so interactive transitions never
1987                    // receive the same control as both from/to.
1988                    *this.swipe_target.borrow_mut() = None;
1989                    this.swipe_target_index.set(-1);
1990
1991                    if let Some(realized) = this.realized() {
1992                        this.recycle_element(&realized);
1993                    }
1994
1995                    *this.realized.borrow_mut() = Some(target_element);
1996                    this.realized_index.set(target_index);
1997
1998                    carousel.set_selected_index(target_index);
1999                }
2000                _ => this.reset_swipe_state(),
2001            }
2002
2003            this.total_delta.set(0.0);
2004            *this.swipe_target.borrow_mut() = None;
2005            this.swipe_target_index.set(-1);
2006            this.is_rubber_banding.set(false);
2007            carousel.set_is_swiping(false);
2008        });
2009    }
2010
2011    fn build_fractional_visible_items(&self, current_offset: f64) -> Vec<PageTransitionItem> {
2012        let viewport_realized = self.viewport_realized.borrow();
2013        viewport_realized
2014            .iter()
2015            .map(|entry| {
2016                PageTransitionItem::new(
2017                    entry.item_index,
2018                    entry.control.clone().upcast(),
2019                    entry.logical_index as f64 - current_offset,
2020                )
2021            })
2022            .collect()
2023    }
2024
2025    fn reset_swipe_state(&self) {
2026        if let Some(carousel) = self.carousel() {
2027            carousel.set_is_swiping(false);
2028        }
2029
2030        self.cancel_offset_animation();
2031
2032        self.reset_viewport_transition_state();
2033        self.reset_transition_state(self.realized().as_ref());
2034
2035        if let Some(swipe_target) = self.swipe_target() {
2036            self.recycle_element(&swipe_target);
2037        }
2038
2039        self.is_dragging.set(false);
2040        self.total_delta.set(0.0);
2041        *self.swipe_target.borrow_mut() = None;
2042        self.swipe_target_index.set(-1);
2043        self.is_rubber_banding.set(false);
2044        self.clear_fractional_progress_context();
2045
2046        if self.uses_viewport_fraction_layout() {
2047            if let Some(viewport_carousel) = self.carousel() {
2048                self.set_offset_core(self.with_primary_offset(
2049                    self.offset.get(),
2050                    self.get_nearest_logical_offset(
2051                        viewport_carousel.selected_index(),
2052                        self.get_primary_offset(self.offset.get()),
2053                    ),
2054                ));
2055            }
2056        }
2057    }
2058
2059    fn reset_transition_state(&self, control: Option<&Ref<Control>>) {
2060        let Some(control) = control else {
2061            return;
2062        };
2063
2064        let transition = self.get_transition();
2065        match transition.as_ref().and_then(|t| t.as_progress_page_transition()) {
2066            Some(progressive) => progressive.reset(&control.clone().upcast()),
2067            None => Self::reset_visual_state(Some(control)),
2068        }
2069    }
2070
2071    fn reset_visual_state(control: Option<&Ref<Control>>) {
2072        let Some(control) = control else {
2073            return;
2074        };
2075        control.set_render_transform(None);
2076        control.set_opacity(1.0);
2077        control.set_z_index(0);
2078        control.set_value(Visual::clip_property(), None);
2079    }
2080}