Skip to main content

ferroui_controls/presenters/
scroll_content_presenter.rs

1use super::{ContentPresenter, ItemsPresenter};
2use crate::primitives::{
3    as_logical_scrollable, as_scroll_snap_points_info, register_scroll_snap_points_info, ScrollBarVisibility,
4    SnapPointsAlignment, SnapPointsChangedHandler, SnapPointsType,
5};
6use crate::{
7    register_scroll_anchor_provider, Control, ControlImpl, IScrollAnchorProvider, ItemsControl,
8    RequestBringIntoViewEventArgs, ScrollViewer, StackPanel,
9};
10use ferroui_base::data::BindingPriority;
11use ferroui_base::input::gesture_recognizers::ScrollGestureRecognizer;
12use ferroui_base::input::{
13    IScrollable, InputElement, InputElementImpl, KeyModifiers, PointerWheelEventArgs, ScrollGestureEndedEventArgs,
14    ScrollGestureEventArgs, ScrollGestureInertiaStartingEventArgs,
15};
16use ferroui_base::interactivity::InteractiveImpl;
17use ferroui_base::layout::{LayoutHelper, Layoutable, LayoutableImpl, LayoutableImplExt, Orientation};
18use ferroui_base::media::FlowDirection;
19use ferroui_base::reactive::{CompositeDisposable, IDisposable, ObservableExt};
20use ferroui_base::utilities::MathUtilities;
21use ferroui_base::{
22    ferro_class, ferro_impl_classes, ferro_property, instantiate, AttachedProperty, BoxedValue, DirectProperty,
23    FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Rect,
24    Ref, Size, StyledElementImpl, StyledProperty, StyledPropertyMetadata, Thickness, Vector, Visual, VisualImpl,
25    VisualImplExt,
26    VisualTreeAttachmentEventArgs, WeakRef,
27};
28use std::cell::{Cell, RefCell};
29use std::cmp::Ordering;
30use rustc_hash::FxHashMap;
31use std::collections::HashMap;
32use std::rc::Rc;
33
34const EDGE_DETECTION_TOLERANCE: f64 = 0.1;
35
36/// Presents a scrolling view of content inside a [`ScrollViewer`].
37#[repr(C)]
38pub struct ScrollContentPresenter {
39    base: ContentPresenter,
40    arranging: Cell<bool>,
41    extent: Cell<Size>,
42    logical_scroll_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
43    viewport: Cell<Size>,
44    active_logical_gesture_scrolls: RefCell<Option<HashMap<i32, Vector>>>,
45    scroll_gesture_snap_points: RefCell<Option<HashMap<i32, Vector>>>,
46    anchor_candidates: RefCell<Option<AnchorCandidates>>,
47    anchor_element: RefCell<Option<Ref<Control>>>,
48    anchor_element_bounds: Cell<Rect>,
49    is_anchor_element_dirty: Cell<bool>,
50    are_vertical_snap_points_regular: Cell<bool>,
51    are_horizontal_snap_points_regular: Cell<bool>,
52    horizontal_snap_points: RefCell<Option<Vec<f64>>>,
53    horizontal_snap_point: Cell<f64>,
54    vertical_snap_points: RefCell<Option<Vec<f64>>>,
55    vertical_snap_point: Cell<f64>,
56    vertical_snap_point_offset: Cell<f64>,
57    horizontal_snap_point_offset: Cell<f64>,
58    owner_subscriptions: RefCell<Option<Rc<CompositeDisposable>>>,
59    owner: RefCell<Option<WeakRef<ScrollViewer>>>,
60    scroll_snap_points_info: RefCell<Option<Ref<Control>>>,
61    /// The snap points changed subscriptions on `scroll_snap_points_info`
62    /// (vertical, then horizontal).
63    scroll_snap_points_subscriptions: RefCell<Option<[Rc<dyn IDisposable>; 2]>>,
64    is_snap_points_updated: Cell<bool>,
65}
66
67ferro_class!(ScrollContentPresenter: ContentPresenter);
68ferroui_base::ferro_class_info!(ScrollContentPresenter { new: ScrollContentPresenter::new });
69ferro_impl_classes!(ScrollContentPresenter: StyledElementImpl, InteractiveImpl, ControlImpl);
70
71impl FerroObjectImpl for ScrollContentPresenter {
72    fn constructed(this: &Self) {
73        Self::parent_constructed(this);
74
75        this.add_handler(
76            Control::request_bring_into_view_event(),
77            |sender, e: &RequestBringIntoViewEventArgs| {
78                if let Some(presenter) = sender.downcast_ref::<ScrollContentPresenter>() {
79                    presenter.bring_into_view_requested(e);
80                }
81            },
82        );
83        this.add_handler(
84            InputElement::scroll_gesture_event(),
85            |sender, e: &ScrollGestureEventArgs| {
86                if let Some(presenter) = sender.downcast_ref::<ScrollContentPresenter>() {
87                    presenter.on_scroll_gesture(e);
88                }
89            },
90        );
91        this.add_handler(
92            InputElement::scroll_gesture_ended_event(),
93            |sender, e: &ScrollGestureEndedEventArgs| {
94                if let Some(presenter) = sender.downcast_ref::<ScrollContentPresenter>() {
95                    presenter.on_scroll_gesture_ended(e);
96                }
97            },
98        );
99        this.add_handler(
100            InputElement::scroll_gesture_inertia_starting_event(),
101            |sender, e: &ScrollGestureInertiaStartingEventArgs| {
102                if let Some(presenter) = sender.downcast_ref::<ScrollContentPresenter>() {
103                    presenter.on_scroll_gesture_inertia_starting_ended(e);
104                }
105            },
106        );
107
108        let weak = this.to_ref().downgrade();
109        let _ = FerroObjectExtensions::get_observable(fo(&this), ContentPresenter::child_property()).subscribe_fn(
110            move |child| {
111                if let Some(this) = weak.upgrade() {
112                    this.update_scrollable_subscription(child);
113                }
114            },
115        );
116    }
117
118    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
119        let property = change.property();
120
121        if property == Self::offset_property().as_property() {
122            if !this.arranging.get() {
123                this.invalidate_arrange();
124            }
125
126            if let Some(owner) = this.owner() {
127                owner.set_current_value(ScrollViewer::offset_property(), change.get_new_value::<Vector>());
128            }
129        } else if property == ContentPresenter::child_property().as_property() {
130            this.child_changed(change);
131        } else if property == Self::horizontal_snap_points_alignment_property().as_property()
132            || property == Self::vertical_snap_points_alignment_property().as_property()
133        {
134            this.update_snap_points();
135        } else if property == Self::extent_property().as_property() {
136            if let Some(owner) = this.owner() {
137                owner.set_extent(change.get_new_value::<Size>());
138            }
139            this.coerce_value(Self::offset_property().as_property());
140        } else if property == Self::viewport_property().as_property() {
141            if let Some(owner) = this.owner() {
142                owner.set_viewport(change.get_new_value::<Size>());
143            }
144            this.coerce_value(Self::offset_property().as_property());
145        }
146
147        Self::parent_on_property_changed(this, change);
148    }
149}
150
151impl VisualImpl for ScrollContentPresenter {
152    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
153        Self::parent_on_attached_to_visual_tree(this, e);
154        this.attach_to_scroll_viewer();
155    }
156
157    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
158        Self::parent_on_detached_from_visual_tree(this, e);
159
160        // A replaced template no longer has the ScrollViewer as an ancestor
161        // and must release its content. Keep the bindings when the entire
162        // owner is detached.
163        if this.find_ancestor_of_type::<ScrollViewer>(false).is_none() {
164            this.detach_from_scroll_viewer();
165        }
166    }
167}
168
169impl LayoutableImpl for ScrollContentPresenter {
170    fn measure_override(this: &Self, available_size: Size) -> Size {
171        let child = match this.child() {
172            Some(child) if this.logical_scroll_subscription.borrow().is_none() => child,
173            _ => return Self::parent_measure_override(this, available_size),
174        };
175
176        let padding = this.get_child_padding();
177        let deflated = available_size.deflate(padding);
178
179        let constraint = Size::new(
180            if this.can_horizontally_scroll() {
181                f64::INFINITY
182            } else {
183                deflated.width
184            },
185            if this.can_vertically_scroll() {
186                f64::INFINITY
187            } else {
188                deflated.height
189            },
190        );
191
192        child.measure(constraint);
193
194        if !this.is_snap_points_updated.get() {
195            this.is_snap_points_updated.set(true);
196            this.update_snap_points();
197        }
198
199        child.desired_size().inflate(padding).constrain(available_size)
200    }
201
202    fn arrange_override(this: &Self, final_size: Size) -> Size {
203        let child = match this.child() {
204            Some(child) if this.logical_scroll_subscription.borrow().is_none() => child,
205            _ => return Self::parent_arrange_override(this, final_size),
206        };
207
208        this.arrange_with_anchoring(&child, final_size)
209    }
210}
211
212impl InputElementImpl for ScrollContentPresenter {
213    fn on_pointer_wheel_changed(this: &Self, e: &PointerWheelEventArgs) {
214        let extent = this.extent();
215        let viewport = this.viewport();
216
217        if extent.height > viewport.height || extent.width > viewport.width {
218            let child = this.child();
219            let scrollable = child.as_ref().and_then(|child| as_logical_scrollable(child));
220            let logical = scrollable.filter(|scrollable| scrollable.is_logical_scroll_enabled());
221
222            let offset = this.offset();
223            let mut x = offset.x;
224            let mut y = offset.y;
225            let mut delta = e.delta();
226
227            // KeyModifiers.Shift should scroll in horizontal direction. This
228            // does not work on every platform. If Shift-Key is pressed and X
229            // is close to 0 we swap the Vector.
230            if e.key_modifiers() == KeyModifiers::SHIFT && MathUtilities::is_zero(delta.x) {
231                delta = Vector::new(delta.y, delta.x);
232            } else {
233                delta = Self::adjust_delta_for_flow_direction(delta, this.flow_direction());
234            }
235
236            if extent.height > viewport.height {
237                let height = match logical {
238                    Some(scrollable) => scrollable.scroll_size().height,
239                    None => 50.0,
240                };
241                y += -delta.y * height;
242                y = y.max(0.0);
243                y = y.min(extent.height - viewport.height);
244            }
245
246            if extent.width > viewport.width {
247                let width = match logical {
248                    Some(scrollable) => scrollable.scroll_size().width,
249                    None => 50.0,
250                };
251                x += -delta.x * width;
252                x = x.max(0.0);
253                x = x.min(extent.width - viewport.width);
254            }
255
256            let new_offset = this.snap_offset(Vector::new(x, y), delta, true);
257
258            let offset_changed = new_offset != this.offset();
259            this.set_current_value(Self::offset_property(), new_offset);
260
261            e.set_handled(!this.is_scroll_chaining_enabled() || offset_changed);
262        }
263    }
264
265    fn as_scrollable(this: &Self) -> Option<&dyn IScrollable> {
266        Some(this)
267    }
268}
269
270impl IScrollable for ScrollContentPresenter {
271    fn extent(&self) -> Size {
272        ScrollContentPresenter::extent(self)
273    }
274
275    fn offset(&self) -> Vector {
276        ScrollContentPresenter::offset(self)
277    }
278
279    fn set_offset(&self, value: Vector) {
280        ScrollContentPresenter::set_offset(self, value)
281    }
282
283    fn viewport(&self) -> Size {
284        ScrollContentPresenter::viewport(self)
285    }
286
287    fn can_horizontally_scroll(&self) -> bool {
288        ScrollContentPresenter::can_horizontally_scroll(self)
289    }
290
291    fn can_vertically_scroll(&self) -> bool {
292        ScrollContentPresenter::can_vertically_scroll(self)
293    }
294}
295
296impl IScrollAnchorProvider for ScrollContentPresenter {
297    fn current_anchor(&self) -> Option<Ref<Control>> {
298        self.ensure_anchor_element_selection();
299        self.anchor_element.borrow().clone()
300    }
301
302    fn register_anchor_candidate(&self, element: &Ref<Control>) {
303        if !self.is_visual_ancestor_of(element) {
304            panic!("An anchor control must be a visual descendent of the ScrollContentPresenter.");
305        }
306
307        self.anchor_candidates.borrow_mut().get_or_insert_with(AnchorCandidates::default).add(element);
308        self.is_anchor_element_dirty.set(true);
309    }
310
311    fn unregister_anchor_candidate(&self, element: &Ref<Control>) {
312        if let Some(candidates) = self.anchor_candidates.borrow_mut().as_mut() {
313            candidates.remove(element);
314        }
315        self.is_anchor_element_dirty.set(true);
316
317        let is_anchor = self.anchor_element.borrow().as_ref() == Some(element);
318        if is_anchor {
319            *self.anchor_element.borrow_mut() = None;
320        }
321    }
322}
323
324ferroui_base::ferro_properties! { impl ScrollContentPresenter {
325    ferro_property!(
326        /// Defines the `CanHorizontallyScroll` property.
327        pub fn can_horizontally_scroll_property() -> StyledProperty<bool> {
328            FerroProperty::register::<ScrollContentPresenter, _>("CanHorizontallyScroll", false)
329        }
330    );
331
332    ferro_property!(
333        /// Defines the `CanVerticallyScroll` property.
334        pub fn can_vertically_scroll_property() -> StyledProperty<bool> {
335            FerroProperty::register::<ScrollContentPresenter, _>("CanVerticallyScroll", false)
336        }
337    );
338
339    ferro_property!(
340        /// Defines the `Extent` property.
341        pub fn extent_property() -> DirectProperty<ScrollContentPresenter, Size> {
342            ScrollViewer::extent_property().add_owner::<ScrollContentPresenter>(|o| o.extent(), None, None)
343        }
344    );
345
346    ferro_property!(
347        /// Defines the `Offset` property.
348        pub fn offset_property() -> StyledProperty<Vector> {
349            ScrollViewer::offset_property().add_owner_with::<ScrollContentPresenter>(
350                StyledPropertyMetadata::new(None).with_coerce(ScrollViewer::coerce_offset),
351            )
352        }
353    );
354
355    ferro_property!(
356        /// Defines the `Viewport` property.
357        pub fn viewport_property() -> DirectProperty<ScrollContentPresenter, Size> {
358            ScrollViewer::viewport_property().add_owner::<ScrollContentPresenter>(|o| o.viewport(), None, None)
359        }
360    );
361
362    ferro_property!(
363        /// Defines the `HorizontalSnapPointsType` property.
364        pub fn horizontal_snap_points_type_property() -> AttachedProperty<SnapPointsType> {
365            ScrollViewer::horizontal_snap_points_type_property().add_owner::<ScrollContentPresenter>()
366        }
367    );
368
369    ferro_property!(
370        /// Defines the `VerticalSnapPointsType` property.
371        pub fn vertical_snap_points_type_property() -> AttachedProperty<SnapPointsType> {
372            ScrollViewer::vertical_snap_points_type_property().add_owner::<ScrollContentPresenter>()
373        }
374    );
375
376    ferro_property!(
377        /// Defines the `HorizontalSnapPointsAlignment` property.
378        pub fn horizontal_snap_points_alignment_property() -> AttachedProperty<SnapPointsAlignment> {
379            ScrollViewer::horizontal_snap_points_alignment_property().add_owner::<ScrollContentPresenter>()
380        }
381    );
382
383    ferro_property!(
384        /// Defines the `VerticalSnapPointsAlignment` property.
385        pub fn vertical_snap_points_alignment_property() -> AttachedProperty<SnapPointsAlignment> {
386            ScrollViewer::vertical_snap_points_alignment_property().add_owner::<ScrollContentPresenter>()
387        }
388    );
389
390    ferro_property!(
391        /// Defines the `IsScrollChainingEnabled` property.
392        pub fn is_scroll_chaining_enabled_property() -> AttachedProperty<bool> {
393            ScrollViewer::is_scroll_chaining_enabled_property().add_owner::<ScrollContentPresenter>()
394        }
395    );
396} }
397
398impl ScrollContentPresenter {
399    /// Initializes static members of the class.
400    fn static_constructor() {
401        register_scroll_anchor_provider::<ScrollContentPresenter>();
402        register_scroll_snap_points_info::<StackPanel>();
403
404        Visual::clip_to_bounds_property().override_default_value::<ScrollContentPresenter>(true);
405        Layoutable::affects_measure::<ScrollContentPresenter>(&[
406            Self::can_horizontally_scroll_property().as_property(),
407            Self::can_vertically_scroll_property().as_property(),
408        ]);
409    }
410
411    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
412    pub fn construct() -> Self {
413        Self {
414            base: ContentPresenter::construct(),
415            arranging: Cell::new(false),
416            extent: Cell::new(Size::default()),
417            logical_scroll_subscription: RefCell::new(None),
418            viewport: Cell::new(Size::default()),
419            active_logical_gesture_scrolls: RefCell::new(None),
420            scroll_gesture_snap_points: RefCell::new(None),
421            anchor_candidates: RefCell::new(None),
422            anchor_element: RefCell::new(None),
423            anchor_element_bounds: Cell::new(Rect::default()),
424            is_anchor_element_dirty: Cell::new(false),
425            are_vertical_snap_points_regular: Cell::new(false),
426            are_horizontal_snap_points_regular: Cell::new(false),
427            horizontal_snap_points: RefCell::new(None),
428            horizontal_snap_point: Cell::new(0.0),
429            vertical_snap_points: RefCell::new(None),
430            vertical_snap_point: Cell::new(0.0),
431            vertical_snap_point_offset: Cell::new(0.0),
432            horizontal_snap_point_offset: Cell::new(0.0),
433            owner_subscriptions: RefCell::new(None),
434            owner: RefCell::new(None),
435            scroll_snap_points_info: RefCell::new(None),
436            scroll_snap_points_subscriptions: RefCell::new(None),
437            is_snap_points_updated: Cell::new(false),
438        }
439    }
440
441    /// Initializes a new instance of the class.
442    pub fn new() -> Ref<Self> {
443        instantiate(Self::construct())
444    }
445
446    /// Gets a value indicating whether the content can be scrolled
447    /// horizontally.
448    pub fn can_horizontally_scroll(&self) -> bool {
449        self.get_value(Self::can_horizontally_scroll_property())
450    }
451
452    /// Sets a value indicating whether the content can be scrolled
453    /// horizontally.
454    pub fn set_can_horizontally_scroll(&self, value: bool) {
455        self.set_value(Self::can_horizontally_scroll_property(), value)
456    }
457
458    /// Gets a value indicating whether the content can be scrolled
459    /// vertically.
460    pub fn can_vertically_scroll(&self) -> bool {
461        self.get_value(Self::can_vertically_scroll_property())
462    }
463
464    /// Sets a value indicating whether the content can be scrolled
465    /// vertically.
466    pub fn set_can_vertically_scroll(&self, value: bool) {
467        self.set_value(Self::can_vertically_scroll_property(), value)
468    }
469
470    /// Gets the extent of the scrollable content.
471    pub fn extent(&self) -> Size {
472        self.extent.get()
473    }
474
475    fn set_extent(&self, value: Size) {
476        self.set_and_raise_cell(Self::extent_property(), &self.extent, value);
477    }
478
479    /// Gets the current scroll offset.
480    pub fn offset(&self) -> Vector {
481        self.get_value(Self::offset_property())
482    }
483
484    /// Sets the current scroll offset.
485    pub fn set_offset(&self, value: Vector) {
486        self.set_value(Self::offset_property(), value)
487    }
488
489    /// Gets the size of the viewport on the scrollable content.
490    pub fn viewport(&self) -> Size {
491        self.viewport.get()
492    }
493
494    fn set_viewport(&self, value: Size) {
495        self.set_and_raise_cell(Self::viewport_property(), &self.viewport, value);
496    }
497
498    /// Gets how scroll gesture reacts to the snap points along the
499    /// horizontal axis.
500    pub fn horizontal_snap_points_type(&self) -> SnapPointsType {
501        self.get_value(Self::horizontal_snap_points_type_property())
502    }
503
504    /// Sets how scroll gesture reacts to the snap points along the
505    /// horizontal axis.
506    pub fn set_horizontal_snap_points_type(&self, value: SnapPointsType) {
507        self.set_value(Self::horizontal_snap_points_type_property(), value)
508    }
509
510    /// Gets how scroll gesture reacts to the snap points along the vertical
511    /// axis.
512    pub fn vertical_snap_points_type(&self) -> SnapPointsType {
513        self.get_value(Self::vertical_snap_points_type_property())
514    }
515
516    /// Sets how scroll gesture reacts to the snap points along the vertical
517    /// axis.
518    pub fn set_vertical_snap_points_type(&self, value: SnapPointsType) {
519        self.set_value(Self::vertical_snap_points_type_property(), value)
520    }
521
522    /// Gets how the existing snap points are horizontally aligned versus the
523    /// initial viewport.
524    pub fn horizontal_snap_points_alignment(&self) -> SnapPointsAlignment {
525        self.get_value(Self::horizontal_snap_points_alignment_property())
526    }
527
528    /// Sets how the existing snap points are horizontally aligned versus the
529    /// initial viewport.
530    pub fn set_horizontal_snap_points_alignment(&self, value: SnapPointsAlignment) {
531        self.set_value(Self::horizontal_snap_points_alignment_property(), value)
532    }
533
534    /// Gets how the existing snap points are vertically aligned versus the
535    /// initial viewport.
536    pub fn vertical_snap_points_alignment(&self) -> SnapPointsAlignment {
537        self.get_value(Self::vertical_snap_points_alignment_property())
538    }
539
540    /// Sets how the existing snap points are vertically aligned versus the
541    /// initial viewport.
542    pub fn set_vertical_snap_points_alignment(&self, value: SnapPointsAlignment) {
543        self.set_value(Self::vertical_snap_points_alignment_property(), value)
544    }
545
546    /// Gets if scroll chaining is enabled. The default value is true.
547    ///
548    /// After a user hits a scroll limit on an element that has been nested
549    /// within another scrollable element, you can specify whether that
550    /// parent element should continue the scrolling operation begun in its
551    /// child element. This is called scroll chaining.
552    pub fn is_scroll_chaining_enabled(&self) -> bool {
553        self.get_value(Self::is_scroll_chaining_enabled_property())
554    }
555
556    /// Sets if scroll chaining is enabled.
557    pub fn set_is_scroll_chaining_enabled(&self, value: bool) {
558        self.set_value(Self::is_scroll_chaining_enabled_property(), value)
559    }
560
561    fn owner(&self) -> Option<Ref<ScrollViewer>> {
562        self.owner.borrow().as_ref().and_then(WeakRef::upgrade)
563    }
564
565    /// Attempts to bring a portion of the target visual into view by
566    /// scrolling the content.
567    ///
568    /// `target` is the target visual and `target_rect` the portion of the
569    /// target visual to bring into view. Returns true if the scroll offset
570    /// was changed; otherwise false.
571    pub fn bring_descendant_into_view(&self, target: &Visual, target_rect: Rect) -> bool {
572        let child = match self.child() {
573            Some(child) if child.is_effectively_visible() => child,
574            _ => return false,
575        };
576
577        if let Some(scrollable) = as_logical_scrollable(&child) {
578            if scrollable.is_logical_scroll_enabled() {
579                if let Some(control) = target.to_ref().cast::<Control>() {
580                    return scrollable.bring_into_view(&control, target_rect);
581                }
582            }
583        }
584
585        // The `viewport` rectangle computed below is in extent coordinates,
586        // so transform `target_rect` into that space too. Going via the
587        // child rather than via this + offset keeps the result independent
588        // of the offset, which may have changed since the last arrange.
589        let Some(transform) = target.transform_to_visual(&child) else {
590            return false;
591        };
592
593        let child_padding = self.get_child_padding();
594        let child_margin = self.get_child_margin();
595        let child_content_origin = Vector::new(
596            child_padding.left + child_margin.left,
597            child_padding.top + child_margin.top,
598        );
599        let rectangle = target_rect.transform_to_aabb(transform).translate(child_content_origin);
600        let current = self.offset();
601        let viewport_size = self.viewport();
602        let viewport = Rect::new(current.x, current.y, viewport_size.width, viewport_size.height);
603
604        let min_x = Self::compute_scroll_offset_with_minimal_scroll(
605            viewport.x,
606            viewport.right(),
607            rectangle.x,
608            rectangle.right(),
609        );
610        let min_y = Self::compute_scroll_offset_with_minimal_scroll(
611            viewport.y,
612            viewport.bottom(),
613            rectangle.y,
614            rectangle.bottom(),
615        );
616        let offset = Vector::new(min_x, min_y);
617
618        if self.offset().nearly_equals(offset) {
619            return false;
620        }
621
622        let old_offset = self.offset();
623        self.set_current_value(Self::offset_property(), offset);
624
625        // It's possible that the offset coercion has changed the offset back
626        // to its previous value, this is common for floating point rounding
627        // errors.
628        !self.offset().nearly_equals(old_offset)
629    }
630
631    /// Computes the closest offset to ensure most of the child is visible in
632    /// the viewport along an axis.
633    ///
634    /// `viewport_start` is the left or top of the viewport, `viewport_end`
635    /// the right or bottom of the viewport, `child_start` the left or top of
636    /// the child and `child_end` the right or bottom of the child.
637    pub(crate) fn compute_scroll_offset_with_minimal_scroll(
638        viewport_start: f64,
639        viewport_end: f64,
640        child_start: f64,
641        child_end: f64,
642    ) -> f64 {
643        // If child is at least partially above viewport, i.e. top of child is
644        // above viewport top and bottom of child is above viewport bottom.
645        let is_child_above =
646            MathUtilities::less_than(child_start, viewport_start) && MathUtilities::less_than(child_end, viewport_end);
647
648        // If child is at least partially below viewport, i.e. top of child is
649        // below viewport top and bottom of child is below viewport bottom.
650        let is_child_below = MathUtilities::greater_than(child_end, viewport_end)
651            && MathUtilities::greater_than(child_start, viewport_start);
652        let is_child_larger = (child_end - child_start) > (viewport_end - viewport_start);
653
654        // Value if no updates is needed. The child is fully visible in the
655        // viewport, or the viewport is completely within the child's bounds.
656        let mut res = viewport_start;
657
658        // The child is above the viewport and is smaller than the viewport,
659        // or if the child's top is below the viewport top and is larger than
660        // the viewport, we align the child top to the top of the viewport.
661        if (is_child_above && !is_child_larger) || (is_child_below && is_child_larger) {
662            res = child_start;
663        }
664        // The child is above the viewport and is larger than the viewport, or
665        // if the child's smaller but is below the viewport, we align the
666        // child's bottom to the bottom of the viewport.
667        else if is_child_above || is_child_below {
668            res = child_end - (viewport_end - viewport_start);
669        }
670
671        res
672    }
673
674    /// Locates the first [`ScrollViewer`] ancestor and binds to it.
675    /// Properties which have been set through other means are not bound.
676    ///
677    /// This method is automatically called when the control is attached to a
678    /// visual tree.
679    pub(crate) fn attach_to_scroll_viewer(&self) {
680        let Some(owner) = self.find_ancestor_of_type::<ScrollViewer>(false) else {
681            self.detach_from_scroll_viewer();
682            return;
683        };
684
685        if self.owner().as_ref() == Some(&owner) {
686            return;
687        }
688
689        let old_subscriptions = self.owner_subscriptions.borrow_mut().take();
690        if let Some(old_subscriptions) = old_subscriptions {
691            old_subscriptions.dispose();
692        }
693        *self.owner.borrow_mut() = Some(owner.downgrade());
694
695        fn not_disabled(v: ScrollBarVisibility) -> bool {
696            v != ScrollBarVisibility::Disabled
697        }
698
699        let mut subscription_disposables: Vec<Rc<dyn IDisposable>> = Vec::new();
700
701        if !self.is_set(Self::can_horizontally_scroll_property().as_property()) {
702            subscription_disposables.push(self.bind(
703                Self::can_horizontally_scroll_property(),
704                FerroObjectExtensions::get_observable_with(
705                    fo(&owner),
706                    ScrollViewer::horizontal_scroll_bar_visibility_property(),
707                    not_disabled,
708                ),
709                BindingPriority::Template,
710            ));
711        }
712
713        if !self.is_set(Self::can_vertically_scroll_property().as_property()) {
714            subscription_disposables.push(self.bind(
715                Self::can_vertically_scroll_property(),
716                FerroObjectExtensions::get_observable_with(
717                    fo(&owner),
718                    ScrollViewer::vertical_scroll_bar_visibility_property(),
719                    not_disabled,
720                ),
721                BindingPriority::Template,
722            ));
723        }
724
725        if !self.is_set(Self::offset_property().as_property()) {
726            subscription_disposables.push(self.bind_value(
727                Self::offset_property(),
728                FerroObjectExtensions::get_binding_observable(fo(&owner), ScrollViewer::offset_property()),
729                BindingPriority::Template,
730            ));
731        }
732
733        if !self.is_set(Self::is_scroll_chaining_enabled_property().as_property()) {
734            subscription_disposables.push(self.bind_value(
735                Self::is_scroll_chaining_enabled_property(),
736                FerroObjectExtensions::get_binding_observable(
737                    fo(&owner),
738                    ScrollViewer::is_scroll_chaining_enabled_property(),
739                ),
740                BindingPriority::Template,
741            ));
742        }
743
744        if !self.is_set(ContentPresenter::content_property().as_property()) {
745            subscription_disposables.push(self.bind_value(
746                ContentPresenter::content_property(),
747                FerroObjectExtensions::get_binding_observable(fo(&owner), ContentPresenter::content_property()),
748                BindingPriority::Template,
749            ));
750        }
751
752        *self.owner_subscriptions.borrow_mut() =
753            Some(Rc::new(CompositeDisposable::from_disposables(subscription_disposables)));
754    }
755
756    /// Clears the owning [`ScrollViewer`] and disposes the bindings to it.
757    fn detach_from_scroll_viewer(&self) {
758        *self.owner.borrow_mut() = None;
759        let subscriptions = self.owner_subscriptions.borrow_mut().take();
760        if let Some(subscriptions) = subscriptions {
761            subscriptions.dispose();
762        }
763    }
764
765    fn arrange_with_anchoring(&self, child: &Ref<Control>, final_size: Size) -> Size {
766        let padding = self.get_child_padding();
767        let desired_size = child.desired_size().inflate(padding);
768
769        let size = Size::new(
770            if self.can_horizontally_scroll() {
771                desired_size.width.max(final_size.width)
772            } else {
773                final_size.width
774            },
775            if self.can_vertically_scroll() {
776                desired_size.height.max(final_size.height)
777            } else {
778                final_size.height
779            },
780        );
781
782        let track_anchor = || -> Vector {
783            // If we have an anchor and its position relative to the child
784            // has changed during the arrange then that change wasn't just
785            // due to scrolling (as scrolling doesn't adjust relative
786            // positions within the child).
787            let anchor_element = self.anchor_element.borrow().clone();
788            if let Some(anchor_element) = anchor_element {
789                if let Some(updated_bounds) = Self::try_translate_bounds(&anchor_element, child) {
790                    let anchor_bounds = self.anchor_element_bounds.get();
791                    if updated_bounds.position() != anchor_bounds.position() {
792                        return (updated_bounds.position() - anchor_bounds.position()).into();
793                    }
794                }
795            }
796
797            Vector::default()
798        };
799
800        let offset = self.offset();
801        let is_anchoring = offset.x >= EDGE_DETECTION_TOLERANCE || offset.y >= EDGE_DETECTION_TOLERANCE;
802
803        if is_anchoring {
804            // Calculate the new anchor element if necessary.
805            self.ensure_anchor_element_selection();
806
807            // Do the arrange.
808            self.arrange_override_impl(size, -self.offset());
809
810            // If the anchor moved during the arrange, we need to adjust the
811            // offset and do another arrange.
812            let anchor_shift = track_anchor();
813
814            if anchor_shift != Vector::default() {
815                let new_offset = self.offset() + anchor_shift;
816                let extent = self.extent();
817                let viewport = self.viewport();
818                let mut new_extent = extent;
819                let max_offset = Vector::new(extent.width - viewport.width, extent.height - viewport.height);
820
821                if new_offset.x > max_offset.x {
822                    new_extent = new_extent.with_width(new_offset.x + viewport.width);
823                }
824
825                if new_offset.y > max_offset.y {
826                    new_extent = new_extent.with_height(new_offset.y + viewport.height);
827                }
828
829                self.set_extent(new_extent);
830
831                {
832                    struct Arranging<'a>(&'a Cell<bool>);
833
834                    impl Drop for Arranging<'_> {
835                        fn drop(&mut self) {
836                            self.0.set(false);
837                        }
838                    }
839
840                    self.arranging.set(true);
841                    let _arranging = Arranging(&self.arranging);
842                    self.set_current_value(Self::offset_property(), new_offset);
843                }
844
845                self.arrange_override_impl(size, -self.offset());
846            }
847        } else {
848            self.arrange_override_impl(size, -self.offset());
849        }
850
851        self.set_viewport(final_size);
852        self.set_extent(self.compute_extent(child, final_size, padding));
853        self.is_anchor_element_dirty.set(true);
854
855        final_size
856    }
857
858    fn get_child_padding(&self) -> Thickness {
859        let mut padding = self.padding();
860        let mut border_thickness = self.border_thickness();
861
862        if self.use_layout_rounding() {
863            let scale = LayoutHelper::get_layout_scale(self);
864            padding = LayoutHelper::round_layout_thickness(padding, scale);
865            border_thickness = LayoutHelper::round_layout_thickness(border_thickness, scale);
866        }
867
868        padding + border_thickness
869    }
870
871    fn get_child_margin(&self) -> Thickness {
872        let Some(child) = self.child() else {
873            return Thickness::default();
874        };
875
876        let mut margin = child.margin();
877
878        if child.use_layout_rounding() {
879            let scale = LayoutHelper::get_layout_scale(&child);
880            margin = LayoutHelper::round_layout_thickness(margin, scale);
881        }
882
883        margin
884    }
885
886    fn compute_extent(&self, child: &Ref<Control>, viewport_size: Size, padding: Thickness) -> Size {
887        let child_margin = self.get_child_margin();
888
889        let mut extent = child.bounds().size().inflate(child_margin).inflate(padding);
890
891        if MathUtilities::are_close_eps(extent.width, viewport_size.width, LayoutHelper::LAYOUT_EPSILON) {
892            extent = extent.with_width(viewport_size.width);
893        }
894
895        if MathUtilities::are_close_eps(extent.height, viewport_size.height, LayoutHelper::LAYOUT_EPSILON) {
896            extent = extent.with_height(viewport_size.height);
897        }
898
899        extent
900    }
901
902    fn on_scroll_gesture(&self, e: &ScrollGestureEventArgs) {
903        let extent = self.extent();
904        let viewport = self.viewport();
905
906        if extent.height > viewport.height || extent.width > viewport.width {
907            let child = self.child();
908            let scrollable = child.as_ref().and_then(|child| as_logical_scrollable(child));
909            let is_logical = scrollable.is_some_and(|scrollable| scrollable.is_logical_scroll_enabled());
910            let mut logical_scroll_item_size = Vector::new(1.0, 1.0);
911            let mut can_x_scroll = false;
912            let mut can_y_scroll = false;
913
914            let offset = self.offset();
915            let mut x = offset.x;
916            let mut y = offset.y;
917
918            let mut delta = Vector::default();
919            if is_logical {
920                if let Some(active) = self.active_logical_gesture_scrolls.borrow().as_ref() {
921                    if let Some(value) = active.get(&e.id()) {
922                        delta = *value;
923                    }
924                }
925            }
926            delta += Self::adjust_delta_for_flow_direction(e.delta(), self.flow_direction());
927
928            if let Some(scrollable) = scrollable.filter(|_| is_logical) {
929                logical_scroll_item_size = self.bounds().size() / scrollable.viewport();
930            }
931
932            if extent.height > viewport.height {
933                let dy;
934                if is_logical {
935                    let logical_units = delta.y / logical_scroll_item_size.y;
936                    delta = delta.with_y(delta.y - logical_units * logical_scroll_item_size.y);
937                    dy = logical_units;
938                } else {
939                    dy = delta.y;
940                }
941
942                y += dy;
943                y = y.max(0.0);
944                y = y.min(extent.height - viewport.height);
945
946                can_y_scroll = dy != 0.0;
947            }
948
949            if extent.width > viewport.width {
950                let dx;
951                if is_logical {
952                    let logical_units = delta.x / logical_scroll_item_size.x;
953                    delta = delta.with_x(delta.x - logical_units * logical_scroll_item_size.x);
954                    dx = logical_units;
955                } else {
956                    dx = delta.x;
957                }
958                x += dx;
959                x = x.max(0.0);
960                x = x.min(extent.width - viewport.width);
961
962                can_x_scroll = dx != 0.0;
963            }
964
965            if is_logical {
966                self.active_logical_gesture_scrolls
967                    .borrow_mut()
968                    .get_or_insert_with(HashMap::new)
969                    .insert(e.id(), delta);
970            }
971
972            let mut new_offset = Vector::new(x, y);
973
974            let snap_point = self
975                .scroll_gesture_snap_points
976                .borrow()
977                .as_ref()
978                .and_then(|points| points.get(&e.id()).copied());
979            if let Some(snap_point) = snap_point {
980                let mut x_offset = x;
981                let mut y_offset = y;
982
983                if self.horizontal_snap_points_type() != SnapPointsType::None {
984                    x_offset = if delta.x < 0.0 {
985                        snap_point.x.max(new_offset.x)
986                    } else {
987                        snap_point.x.min(new_offset.x)
988                    };
989                }
990
991                if self.vertical_snap_points_type() != SnapPointsType::None {
992                    y_offset = if delta.y < 0.0 {
993                        snap_point.y.max(new_offset.y)
994                    } else {
995                        snap_point.y.min(new_offset.y)
996                    };
997                }
998
999                new_offset = Vector::new(x_offset, y_offset);
1000            }
1001
1002            let offset_changed = new_offset != self.offset();
1003            self.set_current_value(Self::offset_property(), new_offset);
1004
1005            e.set_handled(!self.is_scroll_chaining_enabled() || offset_changed);
1006
1007            if !e.handled() && !self.is_scroll_chaining_enabled() {
1008                // Gesture may cause an overscroll so we mark the event as
1009                // handled if it did.
1010                e.set_handled(can_x_scroll || can_y_scroll);
1011            }
1012
1013            e.set_should_end_scroll_gesture(!self.is_scroll_chaining_enabled() && !offset_changed);
1014        }
1015    }
1016
1017    fn on_scroll_gesture_ended(&self, e: &ScrollGestureEndedEventArgs) {
1018        if let Some(active) = self.active_logical_gesture_scrolls.borrow_mut().as_mut() {
1019            active.remove(&e.id());
1020        }
1021        if let Some(points) = self.scroll_gesture_snap_points.borrow_mut().as_mut() {
1022            points.remove(&e.id());
1023        }
1024
1025        let offset = self.snap_offset(self.offset(), Vector::default(), false);
1026        self.set_current_value(Self::offset_property(), offset);
1027    }
1028
1029    fn on_scroll_gesture_inertia_starting_ended(&self, e: &ScrollGestureInertiaStartingEventArgs) {
1030        let mut scrollable = self.content().as_ref().and_then(Control::from_boxed);
1031
1032        if let Some(items_control) = scrollable.as_ref().and_then(|content| content.downcast_ref::<ItemsControl>()) {
1033            scrollable = items_control.presenter().and_then(|presenter| presenter.panel()).map(Ref::upcast);
1034        }
1035
1036        if !scrollable.is_some_and(|scrollable| as_scroll_snap_points_info(&scrollable).is_some()) {
1037            return;
1038        }
1039
1040        self.scroll_gesture_snap_points
1041            .borrow_mut()
1042            .get_or_insert_with(HashMap::new);
1043
1044        let mut offset = self.offset();
1045
1046        if self.horizontal_snap_points_type() != SnapPointsType::None
1047            && self.vertical_snap_points_type() != SnapPointsType::None
1048        {
1049            return;
1050        }
1051
1052        fn get_distance(speed: f64) -> f64 {
1053            let time =
1054                (ScrollGestureRecognizer::INERTIAL_SCROLL_SPEED_END / speed.abs()).ln() / ScrollGestureRecognizer::INERTIAL_RESISTANCE.ln();
1055
1056            let mut time_elapsed = 0.0;
1057            let mut distance = 0.0;
1058            let mut step = 0.0;
1059
1060            while time_elapsed <= time {
1061                let s = speed * ScrollGestureRecognizer::INERTIAL_RESISTANCE.powf(time_elapsed);
1062                distance += s * step;
1063
1064                time_elapsed += 0.016f32 as f64;
1065                step = 0.016f32 as f64;
1066            }
1067
1068            distance
1069        }
1070
1071        let mut x_distance = 0.0;
1072        let mut y_distance = 0.0;
1073
1074        if self.horizontal_snap_points_type() != SnapPointsType::None {
1075            x_distance = if self.horizontal_snap_points_type() == SnapPointsType::Mandatory {
1076                get_distance(e.inertia().x)
1077            } else {
1078                0.0
1079            };
1080        }
1081
1082        if self.vertical_snap_points_type() != SnapPointsType::None {
1083            y_distance = if self.vertical_snap_points_type() == SnapPointsType::Mandatory {
1084                get_distance(e.inertia().y)
1085            } else {
1086                0.0
1087            };
1088        }
1089
1090        offset = Vector::new(offset.x + x_distance, offset.y + y_distance);
1091
1092        let snapped = self.snap_offset(offset, Vector::default(), false);
1093        let mut points = self.scroll_gesture_snap_points.borrow_mut();
1094        let points = points.get_or_insert_with(HashMap::new);
1095        points.insert(e.id(), snapped);
1096    }
1097
1098    fn bring_into_view_requested(&self, e: &RequestBringIntoViewEventArgs) {
1099        if let Some(target_object) = &e.target_object {
1100            e.set_handled(self.bring_descendant_into_view(target_object, e.target_rect()));
1101        }
1102    }
1103
1104    fn child_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1105        let (old_value, new_value) = e.get_old_and_new_value::<Option<Ref<Control>>>();
1106        self.update_scrollable_subscription(new_value);
1107
1108        if old_value.is_some() {
1109            self.set_current_value(Self::offset_property(), Vector::default());
1110        }
1111    }
1112
1113    fn update_scrollable_subscription(&self, child: Option<Ref<Control>>) {
1114        let old_subscription = self.logical_scroll_subscription.borrow_mut().take();
1115        if let Some(old_subscription) = old_subscription {
1116            old_subscription.dispose();
1117        }
1118
1119        let Some(child) = child else { return };
1120        let Some(scrollable) = as_logical_scrollable(&child) else {
1121            return;
1122        };
1123
1124        let weak_this = self.to_ref().downgrade();
1125        let weak_sender = child.downgrade();
1126        let scroll_invalidated = scrollable.scroll_invalidated(Rc::new(move || {
1127            if let (Some(this), Some(sender)) = (weak_this.upgrade(), weak_sender.upgrade()) {
1128                this.update_from_scrollable(&sender);
1129            }
1130        }));
1131
1132        if scrollable.is_logical_scroll_enabled() {
1133            let target = child.clone();
1134            let can_horizontally_scroll =
1135                FerroObjectExtensions::get_observable(fo(&self), Self::can_horizontally_scroll_property())
1136                    .subscribe_fn(move |x| {
1137                        if let Some(scrollable) = as_logical_scrollable(&target) {
1138                            scrollable.set_can_horizontally_scroll(x);
1139                        }
1140                    });
1141
1142            let target = child.clone();
1143            let can_vertically_scroll =
1144                FerroObjectExtensions::get_observable(fo(&self), Self::can_vertically_scroll_property()).subscribe_fn(
1145                    move |x| {
1146                        if let Some(scrollable) = as_logical_scrollable(&target) {
1147                            scrollable.set_can_vertically_scroll(x);
1148                        }
1149                    },
1150                );
1151
1152            let target = child.clone();
1153            let skipped = Cell::new(false);
1154            let offset =
1155                FerroObjectExtensions::get_observable(fo(&self), Self::offset_property()).subscribe_fn(move |x| {
1156                    if !skipped.replace(true) {
1157                        return;
1158                    }
1159                    if let Some(scrollable) = as_logical_scrollable(&target) {
1160                        scrollable.set_offset(x);
1161                    }
1162                });
1163
1164            let subscription: Rc<dyn IDisposable> = Rc::new(CompositeDisposable::from_disposables([
1165                can_horizontally_scroll,
1166                can_vertically_scroll,
1167                offset,
1168                scroll_invalidated,
1169            ]));
1170            *self.logical_scroll_subscription.borrow_mut() = Some(subscription);
1171            self.update_from_scrollable(&child);
1172        }
1173    }
1174
1175    fn update_from_scrollable(&self, child: &Ref<Control>) {
1176        let Some(scrollable) = as_logical_scrollable(child) else {
1177            return;
1178        };
1179        let logical_scroll = self.logical_scroll_subscription.borrow().is_some();
1180
1181        if logical_scroll != scrollable.is_logical_scroll_enabled() {
1182            self.update_scrollable_subscription(self.child());
1183            self.set_current_value(Self::offset_property(), Vector::default());
1184            self.invalidate_measure();
1185        } else if scrollable.is_logical_scroll_enabled() {
1186            self.set_viewport(scrollable.viewport());
1187            self.set_extent(scrollable.extent());
1188            self.set_current_value(Self::offset_property(), scrollable.offset());
1189        }
1190    }
1191
1192    fn ensure_anchor_element_selection(&self) {
1193        if !self.is_anchor_element_dirty.get() {
1194            return;
1195        }
1196        let Some(candidates) = self.anchor_candidates.borrow().as_ref().map(|candidates| candidates.items.clone()) else {
1197            return;
1198        };
1199
1200        *self.anchor_element.borrow_mut() = None;
1201        self.anchor_element_bounds.set(Rect::default());
1202        self.is_anchor_element_dirty.set(false);
1203
1204        let Some(child) = self.child() else {
1205            panic!("The scroll content presenter has anchor candidates but no child.");
1206        };
1207
1208        let mut best_candidate: Option<Ref<Control>> = None;
1209        let mut best_candidate_distance = f64::MAX;
1210
1211        // Find the anchor candidate that is scrolled closest to the top-left
1212        // of this ScrollContentPresenter.
1213        for element in candidates.iter() {
1214            if !element.is_visible() {
1215                continue;
1216            }
1217
1218            if let Some(bounds) = self.get_viewport_bounds(element, &child) {
1219                let distance: Vector = bounds.position().into();
1220                let candidate_distance = distance.length().abs();
1221
1222                if candidate_distance < best_candidate_distance {
1223                    best_candidate = Some(element.clone());
1224                    best_candidate_distance = candidate_distance;
1225                }
1226            }
1227        }
1228
1229        if let Some(best_candidate) = best_candidate {
1230            // We have a candidate, calculate its bounds relative to the
1231            // child. Because these bounds aren't relative to the
1232            // ScrollContentPresenter itself, if they change then we know it
1233            // wasn't just due to scrolling.
1234            let unscrolled_bounds = Self::translate_bounds(&best_candidate, &child);
1235            *self.anchor_element.borrow_mut() = Some(best_candidate);
1236            self.anchor_element_bounds.set(unscrolled_bounds);
1237        }
1238    }
1239
1240    /// The bounds of `element` relative to the viewport, when they intersect
1241    /// it.
1242    fn get_viewport_bounds(&self, element: &Ref<Control>, child: &Ref<Control>) -> Option<Rect> {
1243        let child_bounds = Self::try_translate_bounds(element, child)?;
1244
1245        // We want the bounds relative to the new offset, regardless of
1246        // whether the child control has actually been arranged to this
1247        // offset yet, so translate first to the child control and then apply
1248        // the offset rather than translating directly to this control.
1249        let this_bounds = Rect::from_size(self.bounds().size());
1250        let bounds = Rect::from_position_size(child_bounds.position() - self.offset(), child_bounds.size());
1251        bounds.intersects(this_bounds).then_some(bounds)
1252    }
1253
1254    fn translate_bounds(control: &Ref<Control>, to: &Ref<Control>) -> Rect {
1255        match Self::try_translate_bounds(control, to) {
1256            Some(bounds) => bounds,
1257            None => panic!("The control's bounds could not be translated to the requested control."),
1258        }
1259    }
1260
1261    fn try_translate_bounds(control: &Ref<Control>, to: &Ref<Control>) -> Option<Rect> {
1262        if !control.is_visible() {
1263            return None;
1264        }
1265
1266        let p = control.translate_point(Default::default(), to)?;
1267        Some(Rect::from_position_size(p, control.bounds().size()))
1268    }
1269
1270    fn update_snap_points(&self) {
1271        let content = self.content();
1272        let scrollable = self.get_scroll_snap_points_info(content.as_ref());
1273
1274        if let Some(scroll_snap_points_info) = scrollable.as_ref().and_then(|s| as_scroll_snap_points_info(s)) {
1275            self.are_vertical_snap_points_regular
1276                .set(scroll_snap_points_info.are_vertical_snap_points_regular());
1277            self.are_horizontal_snap_points_regular
1278                .set(scroll_snap_points_info.are_horizontal_snap_points_regular());
1279
1280            if !self.are_vertical_snap_points_regular.get() {
1281                let points = scroll_snap_points_info
1282                    .get_irregular_snap_points(Orientation::Vertical, self.vertical_snap_points_alignment());
1283                *self.vertical_snap_points.borrow_mut() = Some(points);
1284            } else {
1285                *self.vertical_snap_points.borrow_mut() = Some(Vec::new());
1286                let (snap_point, offset) = scroll_snap_points_info
1287                    .get_regular_snap_points(Orientation::Vertical, self.vertical_snap_points_alignment());
1288                self.vertical_snap_point.set(snap_point);
1289                self.vertical_snap_point_offset.set(offset);
1290            }
1291
1292            if !self.are_horizontal_snap_points_regular.get() {
1293                let points = scroll_snap_points_info
1294                    .get_irregular_snap_points(Orientation::Horizontal, self.horizontal_snap_points_alignment());
1295                *self.horizontal_snap_points.borrow_mut() = Some(points);
1296            } else {
1297                *self.horizontal_snap_points.borrow_mut() = Some(Vec::new());
1298                let (snap_point, offset) = scroll_snap_points_info
1299                    .get_regular_snap_points(Orientation::Horizontal, self.horizontal_snap_points_alignment());
1300                self.horizontal_snap_point.set(snap_point);
1301                self.horizontal_snap_point_offset.set(offset);
1302            }
1303        } else {
1304            *self.horizontal_snap_points.borrow_mut() = Some(Vec::new());
1305            *self.vertical_snap_points.borrow_mut() = Some(Vec::new());
1306        }
1307    }
1308
1309    fn snap_offset(&self, mut offset: Vector, direction: Vector, snap_to_next: bool) -> Vector {
1310        let content = self.content();
1311        let scrollable = self.get_scroll_snap_points_info(content.as_ref());
1312
1313        if scrollable.is_none()
1314            || (self.vertical_snap_points_type() == SnapPointsType::None
1315                && self.horizontal_snap_points_type() == SnapPointsType::None)
1316        {
1317            return offset;
1318        }
1319
1320        let diff = self.get_alignment_diff();
1321
1322        let has_vertical_points = self
1323            .vertical_snap_points
1324            .borrow()
1325            .as_ref()
1326            .is_some_and(|points| !points.is_empty());
1327        if self.vertical_snap_points_type() != SnapPointsType::None
1328            && (self.are_vertical_snap_points_regular.get() || has_vertical_points)
1329            && (!snap_to_next || direction.y != 0.0)
1330        {
1331            let estimated_offset = Vector::new(offset.x, offset.y + diff.y);
1332            let mut previous_snap_point = 0.0;
1333            let mut next_snap_point = 0.0;
1334            let mut mid_point = 0.0;
1335
1336            if self.are_vertical_snap_points_regular.get() {
1337                let snap_point = self.vertical_snap_point.get();
1338                previous_snap_point = ((estimated_offset.y / snap_point) as i32) as f64 * snap_point
1339                    + self.vertical_snap_point_offset.get();
1340                next_snap_point = previous_snap_point + snap_point;
1341                mid_point = (previous_snap_point + next_snap_point) / 2.0;
1342            } else if has_vertical_points {
1343                if let Some(points) = self.vertical_snap_points.borrow().as_ref() {
1344                    (previous_snap_point, next_snap_point) = Self::find_nearest_snap_point(points, estimated_offset.y);
1345                }
1346                mid_point = (previous_snap_point + next_snap_point) / 2.0;
1347            }
1348
1349            let nearest_snap_point = if snap_to_next {
1350                if direction.y > 0.0 {
1351                    previous_snap_point
1352                } else {
1353                    next_snap_point
1354                }
1355            } else if estimated_offset.y < mid_point {
1356                previous_snap_point
1357            } else {
1358                next_snap_point
1359            };
1360
1361            offset = Vector::new(offset.x, nearest_snap_point - diff.y);
1362        }
1363
1364        let has_horizontal_points = self
1365            .horizontal_snap_points
1366            .borrow()
1367            .as_ref()
1368            .is_some_and(|points| !points.is_empty());
1369        if self.horizontal_snap_points_type() != SnapPointsType::None
1370            && (self.are_horizontal_snap_points_regular.get() || has_horizontal_points)
1371            && (!snap_to_next || direction.x != 0.0)
1372        {
1373            let estimated_offset = Vector::new(offset.x + diff.x, offset.y);
1374            let mut previous_snap_point = 0.0;
1375            let mut next_snap_point = 0.0;
1376            let mut mid_point = 0.0;
1377
1378            if self.are_horizontal_snap_points_regular.get() {
1379                let snap_point = self.horizontal_snap_point.get();
1380                previous_snap_point = ((estimated_offset.x / snap_point) as i32) as f64 * snap_point
1381                    + self.horizontal_snap_point_offset.get();
1382                next_snap_point = previous_snap_point + snap_point;
1383                mid_point = (previous_snap_point + next_snap_point) / 2.0;
1384            } else if has_horizontal_points {
1385                if let Some(points) = self.horizontal_snap_points.borrow().as_ref() {
1386                    (previous_snap_point, next_snap_point) = Self::find_nearest_snap_point(points, estimated_offset.x);
1387                }
1388                mid_point = (previous_snap_point + next_snap_point) / 2.0;
1389            }
1390
1391            let nearest_snap_point = if snap_to_next {
1392                if direction.x > 0.0 {
1393                    previous_snap_point
1394                } else {
1395                    next_snap_point
1396                }
1397            } else if estimated_offset.x < mid_point {
1398                previous_snap_point
1399            } else {
1400                next_snap_point
1401            };
1402
1403            offset = Vector::new(nearest_snap_point - diff.x, offset.y);
1404        }
1405
1406        offset
1407    }
1408
1409    fn get_alignment_diff(&self) -> Vector {
1410        let mut vector = Vector::default();
1411        let viewport = self.viewport();
1412
1413        match self.vertical_snap_points_alignment() {
1414            SnapPointsAlignment::Center => vector += Vector::new(0.0, viewport.height / 2.0),
1415            SnapPointsAlignment::Far => vector += Vector::new(0.0, viewport.height),
1416            SnapPointsAlignment::Near => {}
1417        }
1418
1419        match self.horizontal_snap_points_alignment() {
1420            SnapPointsAlignment::Center => vector += Vector::new(viewport.width / 2.0, 0.0),
1421            SnapPointsAlignment::Far => vector += Vector::new(viewport.width, 0.0),
1422            SnapPointsAlignment::Near => {}
1423        }
1424
1425        vector
1426    }
1427
1428    /// The snap points before and after `value` in the sorted, non-empty
1429    /// `snap_points`.
1430    fn find_nearest_snap_point(snap_points: &[f64], value: f64) -> (f64, f64) {
1431        match snap_points.binary_search_by(|point| point.partial_cmp(&value).unwrap_or(Ordering::Equal)) {
1432            Err(point) => {
1433                let previous_snap_point = snap_points[point.saturating_sub(1)];
1434                let next_snap_point = if point >= snap_points.len() {
1435                    snap_points[snap_points.len() - 1]
1436                } else {
1437                    snap_points[point]
1438                };
1439                (previous_snap_point, next_snap_point)
1440            }
1441            Ok(point) => (snap_points[point], snap_points[point]),
1442        }
1443    }
1444
1445    /// The content viewed as a provider of snap points, if it is one. Keeps
1446    /// the snap points changed subscriptions on the current provider.
1447    fn get_scroll_snap_points_info(&self, content: Option<&BoxedValue>) -> Option<Ref<Control>> {
1448        let mut scrollable = content.and_then(Control::from_boxed);
1449
1450        let own_content = self.content().as_ref().and_then(Control::from_boxed);
1451
1452        if let Some(items_control) = own_content.as_ref().and_then(|content| content.downcast_ref::<ItemsControl>()) {
1453            scrollable = items_control.presenter().and_then(|presenter| presenter.panel()).map(Ref::upcast);
1454        }
1455
1456        if let Some(items_presenter) = own_content.as_ref().and_then(|content| content.downcast_ref::<ItemsPresenter>())
1457        {
1458            scrollable = items_presenter.panel().map(Ref::upcast);
1459        }
1460
1461        let snap_points_info = scrollable.filter(|scrollable| as_scroll_snap_points_info(scrollable).is_some());
1462
1463        let current = self.scroll_snap_points_info.borrow().clone();
1464        if snap_points_info != current {
1465            let old_subscriptions = self.scroll_snap_points_subscriptions.borrow_mut().take();
1466            if let Some([vertical, horizontal]) = old_subscriptions {
1467                vertical.dispose();
1468                horizontal.dispose();
1469            }
1470
1471            *self.scroll_snap_points_info.borrow_mut() = snap_points_info.clone();
1472
1473            if let Some(info) = snap_points_info
1474                .as_ref()
1475                .and_then(|info| as_scroll_snap_points_info(info))
1476            {
1477                let weak = self.to_ref().downgrade();
1478                let handler: SnapPointsChangedHandler = Rc::new(move |_, _| {
1479                    if let Some(this) = weak.upgrade() {
1480                        this.update_snap_points();
1481                    }
1482                });
1483                let vertical = info.vertical_snap_points_changed(handler.clone());
1484                let horizontal = info.horizontal_snap_points_changed(handler);
1485                *self.scroll_snap_points_subscriptions.borrow_mut() = Some([vertical, horizontal]);
1486            }
1487        }
1488
1489        snap_points_info
1490    }
1491
1492    fn adjust_delta_for_flow_direction(delta: Vector, flow_direction: FlowDirection) -> Vector {
1493        if flow_direction == FlowDirection::RightToLeft {
1494            return delta.with_x(-delta.x);
1495        }
1496        delta
1497    }
1498}
1499
1500/// The object as its root class, for calls that would otherwise resolve to
1501/// a member of an intermediate class.
1502fn fo(object: &ferroui_base::FerroObject) -> &ferroui_base::FerroObject {
1503    object
1504}
1505
1506/// The anchor candidates of a presenter (upstream's `HashSet<Control>`): the controls, and the
1507/// position of each in the list by its address, so that adding and removing one does not search
1508/// the list.
1509#[derive(Default)]
1510struct AnchorCandidates {
1511    items: Vec<Ref<Control>>,
1512    positions: FxHashMap<*const Control, usize>,
1513}
1514
1515impl AnchorCandidates {
1516    fn add(&mut self, element: &Ref<Control>) {
1517        let key: *const Control = &**element;
1518        if !self.positions.contains_key(&key) {
1519            self.positions.insert(key, self.items.len());
1520            self.items.push(element.clone());
1521        }
1522    }
1523
1524    fn remove(&mut self, element: &Ref<Control>) {
1525        let key: *const Control = &**element;
1526        let Some(position) = self.positions.remove(&key) else { return };
1527        self.items.swap_remove(position);
1528        if let Some(moved) = self.items.get(position) {
1529            let moved: *const Control = &**moved;
1530            self.positions.insert(moved, position);
1531        }
1532    }
1533}