Skip to main content

ferroui_controls/
virtualizing_stack_panel.rs

1use crate::generators::{ItemContainerGenerator, RecycleKey};
2use crate::i_scroll_anchor_provider::as_scroll_anchor_provider;
3use crate::items_source::ItemsChangedEventArgs;
4use crate::primitives::{
5    register_scroll_snap_points_info, IScrollSnapPointsInfo, SnapPointsAlignment, SnapPointsChangedHandler,
6};
7use crate::utils::{RealizedStackElements, VirtualizingSnapPointsList};
8use crate::{
9    Control, ControlImpl, ItemsControl, ItemsSourceView, PanelImpl, StackPanel, VirtualizingPanel,
10    VirtualizingPanelImpl, VirtualizingPanelImplExt,
11};
12use ferroui_base::collections::NotifyCollectionChangedAction;
13use ferroui_base::input::{InputElement, InputElementImpl, KeyboardNavigation, NavigationDirection};
14use ferroui_base::interactivity::{
15    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
16};
17use ferroui_base::layout::{EffectiveViewportChangedEventArgs, LayoutableImpl, Orientation};
18use ferroui_base::logging::{LogArea, LogEventLevel, Logger};
19use ferroui_base::reactive::IDisposable;
20use ferroui_base::utilities::MathUtilities;
21use ferroui_base::{
22    ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, AttachedProperty, BoxedValue,
23    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Rect, Ref, Size,
24    StyledElementImpl, StyledProperty, StyledPropertyOptions, Visual, VisualImpl, VisualImplExt,
25    VisualTreeAttachmentEventArgs, WeakRef,
26};
27use std::any::Any;
28use std::cell::{Cell, RefCell};
29use std::collections::HashMap;
30use std::rc::Rc;
31
32/// Arranges and virtualizes content on a single line that is oriented either
33/// horizontally or vertically.
34#[repr(C)]
35pub struct VirtualizingStackPanel {
36    base: VirtualizingPanel,
37    scroll_to_index: Cell<i32>,
38    scroll_to_element: RefCell<Option<Ref<Control>>>,
39    is_in_layout: Cell<bool>,
40    is_waiting_for_viewport_update: Cell<bool>,
41    last_estimated_element_size_u: Cell<f64>,
42    measure_elements: RefCell<Option<Rc<RealizedStackElements>>>,
43    realized_elements: RefCell<Option<Rc<RealizedStackElements>>>,
44    /// The ancestor that implements the scroll anchor provider contract.
45    scroll_anchor_provider: RefCell<Option<WeakRef<Visual>>>,
46    viewport: Cell<Rect>,
47    recycle_pool: RefCell<HashMap<RecycleKey, Vec<Ref<Control>>>>,
48    focused_element: RefCell<Option<Ref<Control>>>,
49    focused_index: Cell<i32>,
50    realizing_element: RefCell<Option<Ref<Control>>>,
51    realizing_index: Cell<i32>,
52    buffer_factor: Cell<f64>,
53
54    has_reached_start: Cell<bool>,
55    has_reached_end: Cell<bool>,
56    last_measured_extended_viewport: Cell<Rect>,
57    last_known_extended_viewport: Cell<Rect>,
58
59    /// The subscription to the property changes of the items control.
60    items_control_property_changed: RefCell<Option<Rc<dyn IDisposable>>>,
61}
62
63ferro_class!(VirtualizingStackPanel: VirtualizingPanel);
64ferroui_base::ferro_class_info!(VirtualizingStackPanel { new: VirtualizingStackPanel::new });
65ferro_impl_classes!(
66    VirtualizingStackPanel: StyledElementImpl,
67    InteractiveImpl,
68    InputElementImpl,
69    ControlImpl,
70    PanelImpl
71);
72
73#[derive(Clone, Copy, Default)]
74struct MeasureViewport {
75    anchor_index: i32,
76    anchor_u: f64,
77    viewport_u_start: f64,
78    viewport_u_end: f64,
79    measured_v: f64,
80    realized_end_u: f64,
81    last_index: i32,
82    viewport_is_disjunct: bool,
83}
84
85/// Clears the in-layout flag when a layout override is left.
86struct InLayoutGuard<'a>(&'a Cell<bool>);
87
88impl Drop for InLayoutGuard<'_> {
89    fn drop(&mut self) {
90        self.0.set(false);
91    }
92}
93
94impl FerroObjectImpl for VirtualizingStackPanel {
95    fn constructed(this: &Self) {
96        Self::parent_constructed(this);
97
98        this.buffer_factor.set(this.cache_length().max(0.0));
99
100        let weak = this.to_ref().downgrade();
101        let _ = this.effective_viewport_changed(move |e| {
102            if let Some(this) = weak.upgrade() {
103                this.on_effective_viewport_changed(e);
104            }
105        });
106    }
107}
108
109impl LayoutableImpl for VirtualizingStackPanel {
110    fn measure_override(this: &Self, available_size: Size) -> Size {
111        let items = this.items();
112        let item_count = items.count() as i32;
113
114        if item_count == 0 {
115            return Size::default();
116        }
117
118        let orientation = this.orientation();
119
120        // If we're bringing an item into view, ignore any layout passes until
121        // we receive a new effective viewport.
122        if this.is_waiting_for_viewport_update.get() {
123            return this.estimate_desired_size(orientation, item_count);
124        }
125
126        this.is_in_layout.set(true);
127        let _in_layout = InLayoutGuard(&this.is_in_layout);
128
129        let realized_elements = this.realized_elements.borrow().clone();
130        if let Some(realized_elements) = &realized_elements {
131            realized_elements.validate_start_u(this.orientation());
132        }
133        let realized_elements = match realized_elements {
134            Some(realized_elements) => realized_elements,
135            None => {
136                let realized_elements = Rc::new(RealizedStackElements::new());
137                *this.realized_elements.borrow_mut() = Some(realized_elements.clone());
138                realized_elements
139            }
140        };
141        let measure_elements = this.measure_elements.borrow().clone();
142        let measure_elements = match measure_elements {
143            Some(measure_elements) => measure_elements,
144            None => {
145                let measure_elements = Rc::new(RealizedStackElements::new());
146                *this.measure_elements.borrow_mut() = Some(measure_elements.clone());
147                measure_elements
148            }
149        };
150
151        // We need to set the last estimated element size before calling
152        // `calculate_desired_size`.
153        let _ = this.estimate_element_size_u();
154
155        // We handle horizontal and vertical layouts here so X and Y are
156        // abstracted to:
157        // - Horizontal layouts: U = horizontal, V = vertical
158        // - Vertical layouts: U = vertical, V = horizontal
159        let mut viewport = this.calculate_measure_viewport(orientation, item_count, &realized_elements);
160
161        // If the viewport is disjunct then we can recycle everything.
162        if viewport.viewport_is_disjunct {
163            realized_elements.recycle_all_elements(|element, index| this.recycle_element(element, index));
164        }
165
166        // Do the measure, creating/recycling elements as necessary to fill
167        // the viewport. Don't write to the realized elements yet, only the
168        // measure elements.
169        this.realize_elements(&items, available_size, &mut viewport, &realized_elements, &measure_elements);
170
171        // Now swap the measure elements and realized elements collection.
172        *this.measure_elements.borrow_mut() = Some(realized_elements.clone());
173        *this.realized_elements.borrow_mut() = Some(measure_elements);
174        realized_elements.reset_for_reuse();
175
176        // If there is a focused element is outside the visible viewport (i.e.
177        // the focused element is non-null), ensure it's measured.
178        let focused_element = this.focused_element.borrow().clone();
179        if let Some(focused_element) = focused_element {
180            focused_element.measure(available_size);
181        }
182
183        this.calculate_desired_size(orientation, item_count, &viewport)
184    }
185
186    fn arrange_override(this: &Self, final_size: Size) -> Size {
187        let Some(realized_elements) = this.realized_elements.borrow().clone() else {
188            return Size::default();
189        };
190
191        this.is_in_layout.set(true);
192
193        {
194            let _in_layout = InLayoutGuard(&this.is_in_layout);
195            let orientation = this.orientation();
196            let mut u = realized_elements.start_u();
197
198            // Collection changes before the realized range make its exact
199            // position unstable until the next measure. `scroll_into_view`
200            // can intentionally defer that measure while waiting for an
201            // updated viewport, so use the same position estimate used when
202            // realizing an element instead of arranging the range at NaN.
203            if u.is_nan() {
204                u = this.get_or_estimate_element_u(realized_elements.first_index());
205            }
206
207            let provider_visual = this.scroll_anchor_provider();
208            let provider = provider_visual.as_ref().and_then(|visual| as_scroll_anchor_provider(visual));
209            let this_visual: Option<Ref<Visual>> = provider.map(|_| this.to_ref().upcast());
210
211            let mut i = 0;
212            while i < realized_elements.count() {
213                if let Some(e) = realized_elements.element_at(i) {
214                    let size_u = realized_elements.size_u_at(i);
215                    let rect = if orientation == Orientation::Horizontal {
216                        Rect::new(u, 0.0, size_u, final_size.height)
217                    } else {
218                        Rect::new(0.0, u, final_size.width, size_u)
219                    };
220
221                    e.arrange(rect);
222
223                    if let Some(provider) = provider {
224                        if e.is_visible() && this.viewport.get().intersects(rect) {
225                            // An anchor candidate must be a visual descendant
226                            // of the provider: an element of this panel is.
227                            if e.visual_parent() == this_visual {
228                                provider.register_anchor_candidate(&e);
229                            } else if let Some(logger) = Logger::try_get(LogEventLevel::Verbose, LogArea::LAYOUT) {
230                                // Element might have been removed/reparented
231                                // during virtualization; ignore but log for
232                                // diagnostics.
233                                logger.log_with_values(
234                                    Some(this as &dyn Any),
235                                    "RegisterAnchorCandidate ignored for {Element}: not a descendant of ScrollAnchorProvider. {Message}",
236                                    &[
237                                        &e.get_type().name(),
238                                        &"An anchor control must be a visual descendent of the scroll anchor provider.",
239                                    ],
240                                );
241                            }
242                        }
243                    }
244
245                    u += if orientation == Orientation::Horizontal { rect.width } else { rect.height };
246                }
247
248                i += 1;
249            }
250
251            // Ensure that the focused element is in the correct position.
252            let focused_element = this.focused_element.borrow().clone();
253            let focused_index = this.focused_index.get();
254            if let Some(focused_element) = focused_element.filter(|_| focused_index >= 0) {
255                let realized_end_u = u;
256                let size_u = if orientation == Orientation::Horizontal {
257                    focused_element.desired_size().width
258                } else {
259                    focused_element.desired_size().height
260                };
261
262                u = this.get_or_estimate_element_u(focused_index);
263
264                // The focused element's position is estimated as it's outside
265                // the realized range. A bad estimate must not place it over
266                // the realized elements in the viewport: an element before
267                // the realized range always ends at or before its start, and
268                // one after it always starts at or after its end.
269                if realized_elements.count() > 0 && !realized_elements.start_u().is_nan() {
270                    if focused_index < realized_elements.first_index() {
271                        u = u.min(realized_elements.start_u() - size_u);
272                    } else if focused_index > realized_elements.last_index() {
273                        u = u.max(realized_end_u);
274                    }
275                }
276
277                let rect = if orientation == Orientation::Horizontal {
278                    Rect::new(u, 0.0, size_u, final_size.height)
279                } else {
280                    Rect::new(0.0, u, final_size.width, size_u)
281                };
282
283                focused_element.arrange(rect);
284            }
285        }
286
287        let event = if this.orientation() == Orientation::Horizontal {
288            Self::horizontal_snap_points_changed_event()
289        } else {
290            Self::vertical_snap_points_changed_event()
291        };
292        this.raise_event(&RoutedEventArgs::with_event(event));
293
294        final_size
295    }
296}
297
298impl VisualImpl for VirtualizingStackPanel {
299    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
300        Self::parent_on_attached_to_visual_tree(this, e);
301
302        let mut provider = None;
303        let mut current = this.visual_parent();
304        while let Some(visual) = current {
305            if as_scroll_anchor_provider(&visual).is_some() {
306                provider = Some(visual.downgrade());
307                break;
308            }
309            current = visual.visual_parent();
310        }
311        *this.scroll_anchor_provider.borrow_mut() = provider;
312    }
313
314    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
315        Self::parent_on_detached_from_visual_tree(this, e);
316        *this.scroll_anchor_provider.borrow_mut() = None;
317    }
318}
319
320impl VirtualizingPanelImpl for VirtualizingStackPanel {
321    fn on_items_changed(this: &Self, _items: &Rc<ItemsSourceView>, e: &ItemsChangedEventArgs<'_>) {
322        this.invalidate_measure();
323
324        // Always update special elements.
325        this.update_special_elements_on_items_changed(e);
326
327        let Some(realized_elements) = this.realized_elements.borrow().clone() else {
328            return;
329        };
330
331        let update_element_index =
332            |element: &Ref<Control>, old_index: i32, new_index: i32| this.update_element_index(element, old_index, new_index);
333        let recycle_element_on_item_removed = |element: &Ref<Control>| this.recycle_element_on_item_removed(element);
334
335        let mut action = e.action;
336        if action == NotifyCollectionChangedAction::Move && e.old_starting_index < 0 {
337            action = NotifyCollectionChangedAction::Reset;
338        }
339
340        match action {
341            NotifyCollectionChangedAction::Add => {
342                realized_elements.items_inserted(e.new_starting_index, e.new_items.len() as i32, update_element_index);
343            }
344            NotifyCollectionChangedAction::Remove => {
345                realized_elements.items_removed(
346                    e.old_starting_index,
347                    e.old_items.len() as i32,
348                    update_element_index,
349                    recycle_element_on_item_removed,
350                );
351            }
352            NotifyCollectionChangedAction::Replace => {
353                realized_elements.items_replaced(
354                    e.old_starting_index,
355                    e.old_items.len() as i32,
356                    recycle_element_on_item_removed,
357                );
358            }
359            NotifyCollectionChangedAction::Move => {
360                realized_elements.items_removed(
361                    e.old_starting_index,
362                    e.old_items.len() as i32,
363                    update_element_index,
364                    recycle_element_on_item_removed,
365                );
366                let mut insert_index = e.new_starting_index;
367
368                if e.new_starting_index > e.old_starting_index {
369                    insert_index -= e.old_items.len() as i32 - 1;
370                }
371
372                realized_elements.items_inserted(insert_index, e.new_items.len() as i32, update_element_index);
373            }
374            NotifyCollectionChangedAction::Reset => {
375                realized_elements.items_reset(recycle_element_on_item_removed);
376            }
377        }
378    }
379
380    fn on_items_control_changed(this: &Self, old_value: Option<&Ref<ItemsControl>>) {
381        Self::parent_on_items_control_changed(this, old_value);
382
383        if old_value.is_some() {
384            if let Some(subscription) = this.items_control_property_changed.take() {
385                subscription.dispose();
386            }
387        }
388        let items_control = this.items_control();
389        if let Some(items_control) = &items_control {
390            let weak = this.to_ref().downgrade();
391            let subscription = items_control.property_changed(move |e| {
392                if let Some(this) = weak.upgrade() {
393                    this.on_items_control_property_changed(e);
394                }
395            });
396            *this.items_control_property_changed.borrow_mut() = Some(subscription);
397        }
398
399        let realized_elements = this.realized_elements.borrow().clone();
400        if let Some(realized_elements) = realized_elements {
401            realized_elements.reset_for_reuse();
402        }
403        let measure_elements = this.measure_elements.borrow().clone();
404        if let Some(measure_elements) = measure_elements {
405            measure_elements.reset_for_reuse();
406        }
407        if items_control.is_some() && this.focused_element.borrow().is_some() {
408            this.recycle_focused_element();
409        }
410        if items_control.is_some() && this.scroll_to_element.borrow().is_some() {
411            this.recycle_scroll_to_element();
412        }
413        if items_control.is_none() {
414            *this.focused_element.borrow_mut() = None;
415            *this.scroll_to_element.borrow_mut() = None;
416        }
417        this.focused_index.set(-1);
418        this.scroll_to_index.set(-1);
419    }
420
421    fn get_control_in_direction(
422        this: &Self,
423        direction: NavigationDirection,
424        from: Option<&Ref<InputElement>>,
425        wrap: bool,
426    ) -> Option<Ref<InputElement>> {
427        let count = this.items().count() as i32;
428        let from_control = from.and_then(|from| from.cast::<Control>());
429
430        if count == 0
431            || (from_control.is_none()
432                && direction != NavigationDirection::First
433                && direction != NavigationDirection::Last)
434        {
435            return None;
436        }
437
438        let horiz = this.orientation() == Orientation::Horizontal;
439        let from_index = match &from_control {
440            Some(from_control) => this.index_from_container(from_control),
441            None => -1,
442        };
443        let mut to_index = from_index;
444
445        match direction {
446            NavigationDirection::First => to_index = 0,
447            NavigationDirection::Last => to_index = count - 1,
448            NavigationDirection::Next => to_index += 1,
449            NavigationDirection::Previous => to_index -= 1,
450            NavigationDirection::Left => {
451                if horiz {
452                    to_index -= 1;
453                }
454            }
455            NavigationDirection::Right => {
456                if horiz {
457                    to_index += 1;
458                }
459            }
460            NavigationDirection::Up => {
461                if !horiz {
462                    to_index -= 1;
463                }
464            }
465            NavigationDirection::Down => {
466                if !horiz {
467                    to_index += 1;
468                }
469            }
470            _ => return None,
471        }
472
473        if from_index == to_index {
474            return from.cloned();
475        }
476
477        if wrap {
478            if to_index < 0 {
479                to_index = count - 1;
480            } else if to_index >= count {
481                to_index = 0;
482            }
483        }
484
485        this.scroll_into_view(to_index).map(|element| element.upcast())
486    }
487
488    fn get_realized_containers(this: &Self) -> Option<Vec<Ref<Control>>> {
489        let realized_elements = this.realized_elements.borrow().clone()?;
490        let elements = realized_elements.elements();
491        Some(elements.iter().flatten().cloned().collect())
492    }
493
494    fn container_from_index(this: &Self, index: i32) -> Option<Ref<Control>> {
495        let items = this.items();
496
497        if index < 0 || index as usize >= items.count() {
498            return None;
499        }
500        if this.scroll_to_index.get() == index {
501            return this.scroll_to_element.borrow().clone();
502        }
503        if this.focused_index.get() == index {
504            return this.focused_element.borrow().clone();
505        }
506        if index == this.realizing_index.get() {
507            return this.realizing_element.borrow().clone();
508        }
509        if let Some(realized) = this.get_realized_element(index) {
510            return Some(realized);
511        }
512        if let Some(c) = items.get_at(index as usize).as_ref().and_then(Control::from_boxed) {
513            if c.get_value(Self::recycle_key_property()) == Some(Self::item_is_its_own_container()) {
514                return Some(c);
515            }
516        }
517        None
518    }
519
520    fn index_from_container(this: &Self, container: &Ref<Control>) -> i32 {
521        if this.scroll_to_element.borrow().as_ref() == Some(container) {
522            return this.scroll_to_index.get();
523        }
524        if this.focused_element.borrow().as_ref() == Some(container) {
525            return this.focused_index.get();
526        }
527        if this.realizing_element.borrow().as_ref() == Some(container) {
528            return this.realizing_index.get();
529        }
530        let realized_elements = this.realized_elements.borrow().clone();
531        realized_elements.map_or(-1, |realized_elements| realized_elements.get_index(container))
532    }
533
534    fn scroll_into_view(this: &Self, index: i32) -> Option<Ref<Control>> {
535        let items = this.items();
536
537        if this.is_in_layout.get()
538            || index < 0
539            || index as usize >= items.count()
540            || this.realized_elements.borrow().is_none()
541            || !this.is_effectively_visible()
542        {
543            return None;
544        }
545
546        if let Some(element) = this.get_realized_element(index) {
547            element.bring_into_view();
548            return Some(element);
549        } else if let Some(root) = this.get_layout_root() {
550            // Create and measure the element to be brought into view. Store
551            // it in a field so that it can be re-used in the layout pass.
552            let generator = this.generator();
553            let scroll_to_element = this.get_or_create_element(&items, &generator, index);
554
555            scroll_to_element.measure(Size::INFINITY);
556
557            // Get the expected position of the element and put it in place.
558            let anchor_u = this.get_or_estimate_element_u(index);
559            let desired_size = scroll_to_element.desired_size();
560            let rect = if this.orientation() == Orientation::Horizontal {
561                Rect::new(anchor_u, 0.0, desired_size.width, desired_size.height)
562            } else {
563                Rect::new(0.0, anchor_u, desired_size.width, desired_size.height)
564            };
565            scroll_to_element.arrange(rect);
566
567            // Store the element and index so that they can be used in the
568            // layout pass.
569            *this.scroll_to_element.borrow_mut() = Some(scroll_to_element.clone());
570            this.scroll_to_index.set(index);
571
572            // If the item being brought into view was added since the last
573            // layout pass then our bounds won't be updated, so any containing
574            // scroll viewers will not have an updated extent. Do a layout
575            // pass to ensure that the containing scroll viewers will be able
576            // to scroll the new item into view.
577            if !this.bounds().contains_rect(rect) && !this.viewport.get().contains_rect(rect) {
578                this.is_waiting_for_viewport_update.set(true);
579                root.layout_manager().execute_layout_pass();
580                this.is_waiting_for_viewport_update.set(false);
581            }
582
583            // Try to bring the item into view.
584            scroll_to_element.bring_into_view();
585
586            // If the viewport does not contain the item to scroll to, set
587            // the waiting flag: this should cause the following chain of
588            // events:
589            // - Measure is first done with the old viewport (which will be a
590            //   no-op, see `measure_override`)
591            // - The viewport is then updated by the layout system which
592            //   invalidates our measure
593            // - Measure is then done with the new viewport.
594            this.is_waiting_for_viewport_update.set(!this.viewport.get().contains_rect(rect));
595            root.layout_manager().execute_layout_pass();
596
597            // If for some reason the layout system didn't give us a new
598            // viewport during the layout, we need to do another layout pass
599            // as the one that took place was a no-op.
600            if this.is_waiting_for_viewport_update.get() {
601                this.is_waiting_for_viewport_update.set(false);
602                this.invalidate_measure();
603                root.layout_manager().execute_layout_pass();
604            }
605
606            // During the previous bring into view, the scroll width extent
607            // might have been out of date if elements have different widths.
608            // Because of that, the scroll viewer might not scroll to the
609            // correct offset. After the previous bring into view, Y offset
610            // should be correct and an extra layout pass has been executed,
611            // hence the width extent should be correct now, and we can try to
612            // scroll again.
613            scroll_to_element.bring_into_view();
614
615            // A layout pass normally adopts the temporary element into the
616            // realized range and clears the scroll-to element. If it cannot
617            // (for example, because the containing pane has no usable
618            // width), the element remains an internal child. Recycle only
619            // the temporary element created by this call so it cannot remain
620            // visible and unindexed.
621            if this.scroll_to_element.borrow().as_ref() == Some(&scroll_to_element) {
622                let scroll_to_index = this.scroll_to_index.get();
623                *this.scroll_to_element.borrow_mut() = None;
624                this.scroll_to_index.set(-1);
625                this.recycle_element(&scroll_to_element, scroll_to_index);
626                return None;
627            }
628
629            return Some(scroll_to_element);
630        }
631
632        None
633    }
634}
635
636ferroui_base::ferro_properties! { impl VirtualizingStackPanel, also [
637    VirtualizingStackPanel::cache_length_property,
638    VirtualizingStackPanel::recycle_key_property,
639] {
640    ferro_property!(
641        /// Defines the `Orientation` property.
642        pub fn orientation_property() -> StyledProperty<Orientation> {
643            StackPanel::orientation_property().add_owner::<VirtualizingStackPanel>()
644        }
645    );
646
647    ferro_property!(
648        /// Defines the `AreHorizontalSnapPointsRegular` property.
649        pub fn are_horizontal_snap_points_regular_property() -> StyledProperty<bool> {
650            FerroProperty::register::<VirtualizingStackPanel, _>("AreHorizontalSnapPointsRegular", false)
651        }
652    );
653
654    ferro_property!(
655        /// Defines the `AreVerticalSnapPointsRegular` property.
656        pub fn are_vertical_snap_points_regular_property() -> StyledProperty<bool> {
657            FerroProperty::register::<VirtualizingStackPanel, _>("AreVerticalSnapPointsRegular", false)
658        }
659    );
660} }
661
662impl VirtualizingStackPanel {
663    ferro_routed_event!(
664        /// Defines the `HorizontalSnapPointsChanged` event.
665        pub fn horizontal_snap_points_changed_event() -> RoutedEvent<RoutedEventArgs> {
666            RoutedEvent::register::<VirtualizingStackPanel, _>("HorizontalSnapPointsChanged", RoutingStrategies::BUBBLE)
667        }
668    );
669
670    ferro_routed_event!(
671        /// Defines the `VerticalSnapPointsChanged` event.
672        pub fn vertical_snap_points_changed_event() -> RoutedEvent<RoutedEventArgs> {
673            RoutedEvent::register::<VirtualizingStackPanel, _>("VerticalSnapPointsChanged", RoutingStrategies::BUBBLE)
674        }
675    );
676
677    ferro_property!(for VirtualizingStackPanel;
678        /// Defines the `CacheLength` property.
679        pub fn cache_length_property() -> StyledProperty<f64> {
680            FerroProperty::register_with::<VirtualizingStackPanel, _>(
681                "CacheLength",
682                StyledPropertyOptions::new(0.0).validate(|v| *v >= 0.0 && *v <= 2.0),
683            )
684        }
685    );
686
687    ferro_property!(for VirtualizingStackPanel;
688        fn recycle_key_property() -> AttachedProperty<Option<RecycleKey>> {
689            FerroProperty::register_attached::<VirtualizingStackPanel, Control, _>("RecycleKey", None)
690        }
691    );
692
693    /// The recycle key that marks an item which is its own container.
694    fn item_is_its_own_container() -> RecycleKey {
695        ferroui_base::ferro_thread_local! {
696            static KEY: RecycleKey = RecycleKey::new_unique();
697        }
698        KEY.with(|key| *key)
699    }
700
701    fn static_constructor() {
702        register_scroll_snap_points_info::<VirtualizingStackPanel>();
703        Self::cache_length_property()
704            .changed()
705            .add_class_handler::<VirtualizingStackPanel>(|x, e| x.on_cache_length_changed(e));
706    }
707
708    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
709    pub fn construct() -> Self {
710        Self {
711            base: VirtualizingPanel::construct(),
712            scroll_to_index: Cell::new(-1),
713            scroll_to_element: RefCell::new(None),
714            is_in_layout: Cell::new(false),
715            is_waiting_for_viewport_update: Cell::new(false),
716            last_estimated_element_size_u: Cell::new(25.0),
717            measure_elements: RefCell::new(None),
718            realized_elements: RefCell::new(None),
719            scroll_anchor_provider: RefCell::new(None),
720            viewport: Cell::new(Rect::default()),
721            recycle_pool: RefCell::new(HashMap::new()),
722            focused_element: RefCell::new(None),
723            focused_index: Cell::new(-1),
724            realizing_element: RefCell::new(None),
725            realizing_index: Cell::new(-1),
726            buffer_factor: Cell::new(0.0),
727            has_reached_start: Cell::new(false),
728            has_reached_end: Cell::new(false),
729            last_measured_extended_viewport: Cell::new(Rect::default()),
730            last_known_extended_viewport: Cell::new(Rect::default()),
731            items_control_property_changed: RefCell::new(None),
732        }
733    }
734
735    pub fn new() -> Ref<Self> {
736        instantiate(Self::construct())
737    }
738
739    /// Gets or sets the axis along which items are laid out.
740    ///
741    /// One of the enumeration values that specifies the axis along which
742    /// items are laid out. The default is vertical.
743    pub fn orientation(&self) -> Orientation {
744        self.get_value(Self::orientation_property())
745    }
746
747    pub fn set_orientation(&self, value: Orientation) {
748        self.set_value(Self::orientation_property(), value)
749    }
750
751    /// Occurs when the measurements for horizontal snap points change.
752    pub fn horizontal_snap_points_changed(
753        &self,
754        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
755    ) -> RoutedEventHandlerToken {
756        self.add_handler(Self::horizontal_snap_points_changed_event(), handler)
757    }
758
759    /// Occurs when the measurements for vertical snap points change.
760    pub fn vertical_snap_points_changed(
761        &self,
762        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
763    ) -> RoutedEventHandlerToken {
764        self.add_handler(Self::vertical_snap_points_changed_event(), handler)
765    }
766
767    /// Gets or sets whether the horizontal snap points for the
768    /// [`VirtualizingStackPanel`] are equidistant from each other.
769    pub fn are_horizontal_snap_points_regular(&self) -> bool {
770        self.get_value(Self::are_horizontal_snap_points_regular_property())
771    }
772
773    pub fn set_are_horizontal_snap_points_regular(&self, value: bool) {
774        self.set_value(Self::are_horizontal_snap_points_regular_property(), value)
775    }
776
777    /// Gets or sets whether the vertical snap points for the
778    /// [`VirtualizingStackPanel`] are equidistant from each other.
779    pub fn are_vertical_snap_points_regular(&self) -> bool {
780        self.get_value(Self::are_vertical_snap_points_regular_property())
781    }
782
783    pub fn set_are_vertical_snap_points_regular(&self, value: bool) {
784        self.set_value(Self::are_vertical_snap_points_regular_property(), value)
785    }
786
787    /// Gets or sets the cache length.
788    ///
789    /// The factor determines how much additional space to maintain above and
790    /// below the viewport. A value of 0.5 means half the viewport size will
791    /// be buffered on each side (up-down or left-right). This uses more
792    /// memory as more UI elements are realized, but greatly reduces the
793    /// number of measure-arrange cycles which can cause heavy memory
794    /// pressure depending on the complexity of the item layouts.
795    pub fn cache_length(&self) -> f64 {
796        self.get_value(Self::cache_length_property())
797    }
798
799    pub fn set_cache_length(&self, value: f64) {
800        self.set_value(Self::cache_length_property(), value)
801    }
802
803    /// Gets the index of the first realized element, or -1 if no elements
804    /// are realized.
805    pub fn first_realized_index(&self) -> i32 {
806        let realized_elements = self.realized_elements.borrow();
807        realized_elements.as_ref().map_or(-1, |realized_elements| realized_elements.first_index())
808    }
809
810    /// Gets the index of the last realized element, or -1 if no elements are
811    /// realized.
812    pub fn last_realized_index(&self) -> i32 {
813        let realized_elements = self.realized_elements.borrow();
814        realized_elements.as_ref().map_or(-1, |realized_elements| realized_elements.last_index())
815    }
816
817    /// Returns the viewport that contains any visible elements.
818    #[allow(dead_code)]
819    pub(crate) fn view_port(&self) -> Rect {
820        self.viewport.get()
821    }
822
823    /// Returns the extended viewport that contains any visible elements and
824    /// the additional elements for fast scrolling (viewport * cache length *
825    /// 2).
826    #[allow(dead_code)]
827    pub(crate) fn last_measured_extended_view_port(&self) -> Rect {
828        self.last_measured_extended_viewport.get()
829    }
830
831    /// The realized elements, with an empty slot for each item of the
832    /// realized range that has no element yet.
833    #[allow(dead_code)]
834    pub(crate) fn get_realized_elements(&self) -> Vec<Option<Ref<Control>>> {
835        let realized_elements = self.realized_elements.borrow();
836        realized_elements.as_ref().map_or_else(Vec::new, |realized_elements| realized_elements.elements().clone())
837    }
838
839    /// The ancestor implementing the scroll anchor provider contract.
840    #[inline]
841    fn scroll_anchor_provider(&self) -> Option<Ref<Visual>> {
842        self.scroll_anchor_provider.borrow().as_ref().and_then(WeakRef::upgrade)
843    }
844
845    /// The generator of the items control. Panics if the panel does not
846    /// belong to an items control.
847    #[inline]
848    fn generator(&self) -> Rc<ItemContainerGenerator> {
849        match self.item_container_generator() {
850            Some(generator) => generator,
851            None => panic!("The VirtualizingPanel does not belong to an ItemsControl."),
852        }
853    }
854
855    fn update_special_elements_on_items_changed(&self, e: &ItemsChangedEventArgs<'_>) {
856        let new_count = e.new_items.len() as i32;
857        let old_count = e.old_items.len() as i32;
858
859        let mut action = e.action;
860        if action == NotifyCollectionChangedAction::Move && e.old_starting_index < 0 {
861            action = NotifyCollectionChangedAction::Reset;
862        }
863
864        match action {
865            NotifyCollectionChangedAction::Add => {
866                let focused_element = self.focused_element.borrow().clone();
867                if let Some(focused_element) = focused_element {
868                    if e.new_starting_index <= self.focused_index.get() {
869                        let old_index = self.focused_index.get();
870                        self.focused_index.set(old_index + new_count);
871                        self.update_element_index(&focused_element, old_index, self.focused_index.get());
872                    }
873                }
874                if self.scroll_to_element.borrow().is_some() && e.new_starting_index <= self.scroll_to_index.get() {
875                    self.scroll_to_index.set(self.scroll_to_index.get() + new_count);
876                }
877            }
878            NotifyCollectionChangedAction::Remove => {
879                let focused_element = self.focused_element.borrow().clone();
880                if let Some(focused_element) = focused_element {
881                    let focused_index = self.focused_index.get();
882                    if e.old_starting_index <= focused_index && focused_index < e.old_starting_index + old_count {
883                        self.recycle_focused_element();
884                    } else if e.old_starting_index < focused_index {
885                        let old_index = focused_index;
886                        self.focused_index.set(focused_index - old_count);
887                        self.update_element_index(&focused_element, old_index, self.focused_index.get());
888                    }
889                }
890                if self.scroll_to_element.borrow().is_some() {
891                    let scroll_to_index = self.scroll_to_index.get();
892                    if e.old_starting_index <= scroll_to_index && scroll_to_index < e.old_starting_index + old_count {
893                        self.recycle_scroll_to_element();
894                    } else if e.old_starting_index < scroll_to_index {
895                        self.scroll_to_index.set(scroll_to_index - old_count);
896                    }
897                }
898            }
899            NotifyCollectionChangedAction::Replace => {
900                let focused_index = self.focused_index.get();
901                if self.focused_element.borrow().is_some()
902                    && e.old_starting_index <= focused_index
903                    && focused_index < e.old_starting_index + old_count
904                {
905                    self.recycle_focused_element();
906                }
907                let scroll_to_index = self.scroll_to_index.get();
908                if self.scroll_to_element.borrow().is_some()
909                    && e.old_starting_index <= scroll_to_index
910                    && scroll_to_index < e.old_starting_index + old_count
911                {
912                    self.recycle_scroll_to_element();
913                }
914            }
915            NotifyCollectionChangedAction::Move => {
916                let focused_element = self.focused_element.borrow().clone();
917                if let Some(focused_element) = focused_element {
918                    let focused_index = self.focused_index.get();
919                    if e.old_starting_index <= focused_index && focused_index < e.old_starting_index + old_count {
920                        let old_index = focused_index;
921                        self.focused_index.set(e.new_starting_index + (focused_index - e.old_starting_index));
922                        self.update_element_index(&focused_element, old_index, self.focused_index.get());
923                    } else {
924                        let mut new_focused_index = focused_index;
925
926                        if e.old_starting_index < focused_index {
927                            new_focused_index -= old_count;
928                        }
929
930                        if e.new_starting_index <= new_focused_index {
931                            new_focused_index += new_count;
932                        }
933
934                        if new_focused_index != focused_index {
935                            let old_index = focused_index;
936                            self.focused_index.set(new_focused_index);
937                            self.update_element_index(&focused_element, old_index, new_focused_index);
938                        }
939                    }
940                }
941
942                if self.scroll_to_element.borrow().is_some() {
943                    let scroll_to_index = self.scroll_to_index.get();
944                    if e.old_starting_index <= scroll_to_index && scroll_to_index < e.old_starting_index + old_count {
945                        self.scroll_to_index.set(e.new_starting_index + (scroll_to_index - e.old_starting_index));
946                    } else {
947                        let mut new_scroll_to_index = scroll_to_index;
948
949                        if e.old_starting_index < scroll_to_index {
950                            new_scroll_to_index -= old_count;
951                        }
952
953                        if e.new_starting_index <= new_scroll_to_index {
954                            new_scroll_to_index += new_count;
955                        }
956
957                        self.scroll_to_index.set(new_scroll_to_index);
958                    }
959                }
960            }
961            NotifyCollectionChangedAction::Reset => {
962                if self.focused_element.borrow().is_some() {
963                    self.recycle_focused_element();
964                }
965                if self.scroll_to_element.borrow().is_some() {
966                    self.recycle_scroll_to_element();
967                }
968            }
969        }
970    }
971
972    fn calculate_measure_viewport(
973        &self,
974        orientation: Orientation,
975        item_count: i32,
976        realized_elements: &RealizedStackElements,
977    ) -> MeasureViewport {
978        // Use the extended viewport for calculations.
979        let viewport = self.last_measured_extended_viewport.get();
980
981        // Get the viewport in the orientation direction.
982        let viewport_start = if orientation == Orientation::Horizontal { viewport.x } else { viewport.y };
983        let viewport_end = if orientation == Orientation::Horizontal { viewport.right() } else { viewport.bottom() };
984
985        // Get or estimate the anchor element from which to start realization.
986        // If we are scrolling to an element, use that as the anchor element.
987        // Otherwise, estimate the anchor element based on the current
988        // viewport.
989        let anchor_index;
990        let anchor_u;
991
992        let scroll_to_bounds = if self.scroll_to_index.get() >= 0 {
993            self.scroll_to_element.borrow().as_ref().map(|element| element.bounds())
994        } else {
995            None
996        };
997
998        if let Some(bounds) = scroll_to_bounds {
999            anchor_index = self.scroll_to_index.get();
1000            anchor_u = if orientation == Orientation::Horizontal { bounds.left() } else { bounds.top() };
1001        } else {
1002            (anchor_index, anchor_u) =
1003                self.get_or_estimate_anchor_element_for_viewport(viewport_start, viewport_end, item_count);
1004        }
1005
1006        // Check if the anchor element is not within the currently realized
1007        // elements.
1008        let disjunct = anchor_index < realized_elements.first_index() || anchor_index > realized_elements.last_index();
1009
1010        MeasureViewport {
1011            anchor_index,
1012            anchor_u,
1013            viewport_u_start: viewport_start,
1014            viewport_u_end: viewport_end,
1015            viewport_is_disjunct: disjunct,
1016            ..MeasureViewport::default()
1017        }
1018    }
1019
1020    fn calculate_desired_size(&self, orientation: Orientation, item_count: i32, viewport: &MeasureViewport) -> Size {
1021        let mut size_u = 0.0;
1022        let size_v = viewport.measured_v;
1023
1024        if viewport.last_index >= 0 {
1025            let remaining = item_count - viewport.last_index - 1;
1026            size_u = viewport.realized_end_u + (remaining as f64 * self.last_estimated_element_size_u.get());
1027        }
1028
1029        if orientation == Orientation::Horizontal {
1030            Size::new(size_u, size_v)
1031        } else {
1032            Size::new(size_v, size_u)
1033        }
1034    }
1035
1036    fn estimate_desired_size(&self, orientation: Orientation, item_count: i32) -> Size {
1037        let scroll_to_bounds = if self.scroll_to_index.get() >= 0 {
1038            self.scroll_to_element.borrow().as_ref().map(|element| element.bounds())
1039        } else {
1040            None
1041        };
1042
1043        if let Some(bounds) = scroll_to_bounds {
1044            // We have an element to scroll to, so we can estimate the desired
1045            // size based on the element's position and the remaining
1046            // elements.
1047            let remaining = item_count - self.scroll_to_index.get() - 1;
1048            let u = if orientation == Orientation::Horizontal { bounds.right() } else { bounds.bottom() };
1049            let size_u = u + (remaining as f64 * self.last_estimated_element_size_u.get());
1050            return if orientation == Orientation::Horizontal {
1051                Size::new(size_u, self.desired_size().height)
1052            } else {
1053                Size::new(self.desired_size().width, size_u)
1054            };
1055        }
1056
1057        self.desired_size()
1058    }
1059
1060    fn estimate_element_size_u(&self) -> f64 {
1061        let realized_elements = self.realized_elements.borrow();
1062        let Some(realized_elements) = realized_elements.as_ref() else {
1063            return self.last_estimated_element_size_u.get();
1064        };
1065
1066        let orientation = self.orientation();
1067        let mut total = 0.0;
1068        let mut divisor = 0.0;
1069
1070        // Average the desired size of the realized, measured elements.
1071        for element in realized_elements.elements().iter() {
1072            let Some(element) = element else { continue };
1073            if !element.is_measure_valid() {
1074                continue;
1075            }
1076            let size_u = if orientation == Orientation::Horizontal {
1077                element.desired_size().width
1078            } else {
1079                element.desired_size().height
1080            };
1081            total += size_u;
1082            divisor += 1.0;
1083        }
1084
1085        // Check we have enough information on which to base our estimate.
1086        if divisor == 0.0 || total == 0.0 {
1087            return self.last_estimated_element_size_u.get();
1088        }
1089
1090        // Store and return the estimate.
1091        let estimate = total / divisor;
1092        self.last_estimated_element_size_u.set(estimate);
1093        estimate
1094    }
1095
1096    /// Returns the index and the position of the anchor element.
1097    fn get_or_estimate_anchor_element_for_viewport(
1098        &self,
1099        viewport_start_u: f64,
1100        viewport_end_u: f64,
1101        item_count: i32,
1102    ) -> (i32, f64) {
1103        // We have no elements, or we're at the start of the viewport.
1104        if item_count <= 0 || MathUtilities::is_zero(viewport_start_u) {
1105            return (0, 0.0);
1106        }
1107
1108        let horizontal = self.orientation() == Orientation::Horizontal;
1109        let viewport = self.viewport.get();
1110        let viewport_end = if horizontal { viewport.right() } else { viewport.bottom() };
1111
1112        if !self.has_reached_end.get()
1113            && MathUtilities::greater_than_or_close(
1114                viewport_end,
1115                if horizontal { self.bounds().width } else { self.bounds().height },
1116            )
1117        {
1118            return (item_count - 1, viewport_end - self.estimate_element_size_u());
1119        }
1120
1121        // If we have realised elements and a valid start position then try
1122        // to use this information to get the anchor element.
1123        {
1124            let realized_elements = self.realized_elements.borrow();
1125            if let Some(realized_elements) = realized_elements.as_ref() {
1126                let mut u = realized_elements.start_u();
1127                if !u.is_nan() {
1128                    let elements = realized_elements.elements();
1129
1130                    for (i, element) in elements.iter().enumerate() {
1131                        let Some(element) = element else { continue };
1132
1133                        let size_u =
1134                            if horizontal { element.desired_size().width } else { element.desired_size().height };
1135                        let end_u = u + size_u;
1136
1137                        if end_u > viewport_start_u && u < viewport_end_u {
1138                            return (realized_elements.first_index() + i as i32, u);
1139                        }
1140
1141                        u = end_u;
1142                    }
1143                }
1144            }
1145        }
1146
1147        // We don't have any realized elements in the requested viewport, or
1148        // can't rely on the start position being valid. Estimate the index
1149        // using only the estimated element size.
1150        let estimated_size = self.estimate_element_size_u();
1151
1152        // Estimate the element at the start of the viewport.
1153        let start_index = ((viewport_start_u / estimated_size) as i32).min(item_count - 1);
1154        (start_index, start_index as f64 * estimated_size)
1155    }
1156
1157    fn get_or_estimate_element_u(&self, index: i32) -> f64 {
1158        let realized_elements = self.realized_elements.borrow().clone();
1159
1160        // Return the position of the existing element if realized.
1161        let u = realized_elements.as_ref().map_or(f64::NAN, |realized| realized.get_element_u(index));
1162
1163        if !u.is_nan() {
1164            return u;
1165        }
1166
1167        // Estimate the element size.
1168        let estimated_size = self.estimate_element_size_u();
1169
1170        // If we have a valid start position, use it to anchor estimates
1171        // relative to the realized range.
1172        if let Some(realized) = realized_elements.as_ref().filter(|realized| !realized.start_u().is_nan()) {
1173            let first = realized.first_index();
1174            let last = realized.last_index();
1175
1176            if index < first {
1177                // Interpolate between the known panel origin and the first
1178                // realized item. Using the realized items' average size to
1179                // extrapolate backwards can place the target before the
1180                // panel origin.
1181                return realized.start_u() * index as f64 / first as f64;
1182            }
1183
1184            if index > last {
1185                let sizes = realized.size_u();
1186                let mut realized_span = 0.0;
1187
1188                for size_u in sizes.iter() {
1189                    realized_span += if size_u.is_nan() { estimated_size } else { *size_u };
1190                }
1191
1192                return realized.start_u() + realized_span + ((index - last - 1) as f64 * estimated_size);
1193            }
1194        }
1195
1196        index as f64 * estimated_size
1197    }
1198
1199    fn realize_elements(
1200        &self,
1201        items: &ItemsSourceView,
1202        available_size: Size,
1203        viewport: &mut MeasureViewport,
1204        realized_elements: &RealizedStackElements,
1205        measure_elements: &RealizedStackElements,
1206    ) {
1207        let item_count = items.count() as i32;
1208        debug_assert!(item_count > 0);
1209
1210        let generator = self.generator();
1211        let mut index = viewport.anchor_index;
1212        let horizontal = self.orientation() == Orientation::Horizontal;
1213        let mut u = viewport.anchor_u;
1214        let viewport_end = if horizontal { self.viewport.get().right() } else { self.viewport.get().bottom() };
1215
1216        let anchor_at_end = !self.has_reached_end.get()
1217            && index == item_count - 1
1218            && !MathUtilities::is_zero(viewport.viewport_u_start)
1219            && MathUtilities::greater_than_or_close(
1220                viewport_end,
1221                if horizontal { self.bounds().width } else { self.bounds().height },
1222            );
1223
1224        // Reset boundary flags.
1225        self.has_reached_start.set(false);
1226        self.has_reached_end.set(false);
1227
1228        // If the anchor element is at the beginning of, or before, the start
1229        // of the viewport then we can recycle all elements before it.
1230        if u <= viewport.anchor_u {
1231            realized_elements
1232                .recycle_elements_before(viewport.anchor_index, |element, index| self.recycle_element(element, index));
1233        }
1234
1235        // Start at the anchor element and move forwards, realizing elements.
1236        loop {
1237            self.realizing_index.set(index);
1238            let e = self.get_or_create_element(items, &generator, index);
1239            *self.realizing_element.borrow_mut() = Some(e.clone());
1240
1241            e.measure(available_size);
1242
1243            let desired_size = e.desired_size();
1244            let size_u = if horizontal { desired_size.width } else { desired_size.height };
1245            let size_v = if horizontal { desired_size.height } else { desired_size.width };
1246
1247            if anchor_at_end && index == viewport.anchor_index {
1248                u = viewport_end - size_u;
1249                viewport.anchor_u = u;
1250            }
1251
1252            measure_elements.add(index, e, u, size_u);
1253            viewport.measured_v = viewport.measured_v.max(size_v);
1254
1255            u += size_u;
1256            index += 1;
1257            self.realizing_index.set(-1);
1258            *self.realizing_element.borrow_mut() = None;
1259
1260            if !(u < viewport.viewport_u_end && index < item_count) {
1261                break;
1262            }
1263        }
1264
1265        // Check if we reached the end of the collection.
1266        self.has_reached_end.set(index >= item_count);
1267
1268        // Store the last index and end U position for the desired size
1269        // calculation.
1270        viewport.last_index = index - 1;
1271        viewport.realized_end_u = u;
1272
1273        // We can now recycle elements after the last element.
1274        realized_elements
1275            .recycle_elements_after(viewport.last_index, |element, index| self.recycle_element(element, index));
1276
1277        // Next move backwards from the anchor element, realizing elements.
1278        index = viewport.anchor_index - 1;
1279        u = viewport.anchor_u;
1280
1281        while u > viewport.viewport_u_start && index >= 0 {
1282            let e = self.get_or_create_element(items, &generator, index);
1283
1284            e.measure(available_size);
1285            let desired_size = e.desired_size();
1286            let size_u = if horizontal { desired_size.width } else { desired_size.height };
1287            let size_v = if horizontal { desired_size.height } else { desired_size.width };
1288            u -= size_u;
1289
1290            measure_elements.add(index, e, u, size_u);
1291            viewport.measured_v = viewport.measured_v.max(size_v);
1292            index -= 1;
1293        }
1294
1295        // Check if we reached the start of the collection.
1296        self.has_reached_start.set(index < 0);
1297
1298        // We can now recycle elements before the first element.
1299        realized_elements.recycle_elements_before(index + 1, |element, index| self.recycle_element(element, index));
1300    }
1301
1302    fn get_or_create_element(
1303        &self,
1304        items: &ItemsSourceView,
1305        generator: &ItemContainerGenerator,
1306        index: i32,
1307    ) -> Ref<Control> {
1308        if let Some(realized) = self
1309            .get_realized_element(index)
1310            .or_else(|| Self::get_realized_special_element(index, &self.focused_index, &self.focused_element))
1311            .or_else(|| Self::get_realized_special_element(index, &self.scroll_to_index, &self.scroll_to_element))
1312        {
1313            return realized;
1314        }
1315
1316        let item = items.get_at(index as usize);
1317        let (needs_container, recycle_key) = generator.needs_container(&item, index);
1318
1319        if needs_container {
1320            match self.get_recycled_element(generator, &item, index, recycle_key) {
1321                Some(recycled) => recycled,
1322                None => self.create_element(generator, &item, index, recycle_key),
1323            }
1324        } else {
1325            self.get_item_as_own_container(generator, &item, index)
1326        }
1327    }
1328
1329    #[inline]
1330    fn get_realized_element(&self, index: i32) -> Option<Ref<Control>> {
1331        let realized_elements = self.realized_elements.borrow();
1332        realized_elements.as_ref().and_then(|realized_elements| realized_elements.get_element(index))
1333    }
1334
1335    #[inline]
1336    fn get_realized_special_element(
1337        index: i32,
1338        special_index: &Cell<i32>,
1339        special_element: &RefCell<Option<Ref<Control>>>,
1340    ) -> Option<Ref<Control>> {
1341        if special_index.get() == index {
1342            let result = special_element.take();
1343            debug_assert!(result.is_some());
1344            special_index.set(-1);
1345            return result;
1346        }
1347
1348        None
1349    }
1350
1351    fn get_item_as_own_container(
1352        &self,
1353        generator: &ItemContainerGenerator,
1354        item: &Option<BoxedValue>,
1355        index: i32,
1356    ) -> Ref<Control> {
1357        let control_item = match item.as_ref().and_then(Control::from_boxed) {
1358            Some(control_item) => control_item,
1359            None => panic!("An item that is its own container must be a control."),
1360        };
1361
1362        if !control_item.is_set(Self::recycle_key_property().as_property()) {
1363            generator.prepare_item_container(&control_item, item, index);
1364            self.add_internal_child(&control_item);
1365            control_item.set_value(Self::recycle_key_property(), Some(Self::item_is_its_own_container()));
1366            generator.item_container_prepared(&control_item, item, index);
1367        }
1368
1369        control_item.set_current_value(Visual::is_visible_property(), true);
1370        control_item
1371    }
1372
1373    fn get_recycled_element(
1374        &self,
1375        generator: &ItemContainerGenerator,
1376        item: &Option<BoxedValue>,
1377        index: i32,
1378        recycle_key: Option<RecycleKey>,
1379    ) -> Option<Ref<Control>> {
1380        let recycle_key = recycle_key?;
1381
1382        let recycled = self.recycle_pool.borrow_mut().get_mut(&recycle_key).and_then(Vec::pop)?;
1383        ferroui_base::perf_count!(ContainersReused);
1384
1385        recycled.set_current_value(Visual::is_visible_property(), true);
1386        generator.prepare_item_container(&recycled, item, index);
1387        self.add_internal_child(&recycled);
1388        generator.item_container_prepared(&recycled, item, index);
1389        Some(recycled)
1390    }
1391
1392    fn create_element(
1393        &self,
1394        generator: &ItemContainerGenerator,
1395        item: &Option<BoxedValue>,
1396        index: i32,
1397        recycle_key: Option<RecycleKey>,
1398    ) -> Ref<Control> {
1399        ferroui_base::perf_count!(ContainersCreated);
1400        let container = generator.create_container(item, index, recycle_key);
1401
1402        container.set_value(Self::recycle_key_property(), recycle_key);
1403        generator.prepare_item_container(&container, item, index);
1404        self.add_internal_child(&container);
1405        generator.item_container_prepared(&container, item, index);
1406
1407        container
1408    }
1409
1410    fn unregister_anchor_candidate(&self, element: &Ref<Control>) {
1411        if let Some(provider_visual) = self.scroll_anchor_provider() {
1412            if let Some(provider) = as_scroll_anchor_provider(&provider_visual) {
1413                provider.unregister_anchor_candidate(element);
1414            }
1415        }
1416    }
1417
1418    fn recycle_element(&self, element: &Ref<Control>, index: i32) {
1419        debug_assert!(self.items_control().is_some());
1420
1421        self.unregister_anchor_candidate(element);
1422
1423        let recycle_key = element.get_value(Self::recycle_key_property());
1424
1425        match recycle_key {
1426            None => {
1427                self.generator().clear_item_container(element);
1428                self.remove_internal_child(element);
1429            }
1430            Some(recycle_key) if recycle_key == Self::item_is_its_own_container() => {
1431                element.set_current_value(Visual::is_visible_property(), false);
1432            }
1433            Some(recycle_key) => {
1434                let tab_once_active_element =
1435                    self.items_control().and_then(|items_control| KeyboardNavigation::get_tab_once_active_element(&items_control));
1436
1437                if tab_once_active_element.is_some_and(|active| active == *element) {
1438                    *self.focused_element.borrow_mut() = Some(element.clone());
1439                    self.focused_index.set(index);
1440                } else {
1441                    ferroui_base::perf_count!(ContainersRecycled);
1442                    self.generator().clear_item_container(element);
1443                    self.push_to_recycle_pool(recycle_key, element);
1444                    element.set_current_value(Visual::is_visible_property(), false);
1445                    self.remove_internal_child(element);
1446                }
1447            }
1448        }
1449    }
1450
1451    fn recycle_element_on_item_removed(&self, element: &Ref<Control>) {
1452        self.unregister_anchor_candidate(element);
1453
1454        let recycle_key = element.get_value(Self::recycle_key_property());
1455
1456        match recycle_key {
1457            None => {
1458                self.generator().clear_item_container(element);
1459                self.remove_internal_child(element);
1460            }
1461            Some(recycle_key) if recycle_key == Self::item_is_its_own_container() => {
1462                self.remove_internal_child(element);
1463            }
1464            Some(recycle_key) => {
1465                self.generator().clear_item_container(element);
1466                self.push_to_recycle_pool(recycle_key, element);
1467                element.set_current_value(Visual::is_visible_property(), false);
1468                self.remove_internal_child(element);
1469            }
1470        }
1471    }
1472
1473    fn recycle_focused_element(&self) {
1474        let focused_element = self.focused_element.borrow().clone();
1475        if let Some(focused_element) = focused_element {
1476            self.recycle_element_on_item_removed(&focused_element);
1477        }
1478        *self.focused_element.borrow_mut() = None;
1479        self.focused_index.set(-1);
1480    }
1481
1482    fn recycle_scroll_to_element(&self) {
1483        let scroll_to_element = self.scroll_to_element.borrow().clone();
1484        if let Some(scroll_to_element) = scroll_to_element {
1485            self.recycle_element_on_item_removed(&scroll_to_element);
1486        }
1487        *self.scroll_to_element.borrow_mut() = None;
1488        self.scroll_to_index.set(-1);
1489    }
1490
1491    fn push_to_recycle_pool(&self, recycle_key: RecycleKey, element: &Ref<Control>) {
1492        self.recycle_pool.borrow_mut().entry(recycle_key).or_default().push(element.clone());
1493    }
1494
1495    fn update_element_index(&self, element: &Ref<Control>, old_index: i32, new_index: i32) {
1496        self.generator().item_container_index_changed(element, old_index, new_index);
1497    }
1498
1499    fn calculate_extended_viewport(&self, vertical: bool, _viewport_size: f64, buffer_size: f64) -> Rect {
1500        let viewport = self.viewport.get();
1501        let bounds = self.bounds();
1502
1503        let mut extended_viewport_start = if vertical {
1504            (viewport.top() - buffer_size).max(0.0)
1505        } else {
1506            (viewport.left() - buffer_size).max(0.0)
1507        };
1508
1509        let mut extended_viewport_end = if vertical {
1510            bounds.height.min(viewport.bottom() + buffer_size)
1511        } else {
1512            bounds.width.min(viewport.right() + buffer_size)
1513        };
1514
1515        // If we are at the start of the list, append 2 * cache length
1516        // additional items. If we are at the end of the list, prepend 2 *
1517        // cache length additional items - this way we always maintain "2 *
1518        // cache length * element" items.
1519        if vertical {
1520            let space_above = viewport.top() - buffer_size;
1521            let space_below = bounds.height - (viewport.bottom() + buffer_size);
1522
1523            if space_above < 0.0 && space_below >= 0.0 {
1524                extended_viewport_end = bounds.height.min(extended_viewport_end + space_above.abs());
1525            }
1526            if space_above >= 0.0 && space_below < 0.0 {
1527                extended_viewport_start = (extended_viewport_start - space_below.abs()).max(0.0);
1528            }
1529        } else {
1530            let space_left = viewport.left() - buffer_size;
1531            let space_right = bounds.width - (viewport.right() + buffer_size);
1532
1533            if space_left < 0.0 && space_right >= 0.0 {
1534                extended_viewport_end = bounds.width.min(extended_viewport_end + space_left.abs());
1535            }
1536            if space_left >= 0.0 && space_right < 0.0 {
1537                extended_viewport_start = (extended_viewport_start - space_right.abs()).max(0.0);
1538            }
1539        }
1540
1541        if vertical {
1542            Rect::new(viewport.x, extended_viewport_start, viewport.width, extended_viewport_end - extended_viewport_start)
1543        } else {
1544            Rect::new(extended_viewport_start, viewport.y, extended_viewport_end - extended_viewport_start, viewport.height)
1545        }
1546    }
1547
1548    fn on_effective_viewport_changed(&self, e: &EffectiveViewportChangedEventArgs) {
1549        let vertical = self.orientation() == Orientation::Vertical;
1550        let old_viewport = self.viewport.get();
1551        let old_extended_viewport = self.last_measured_extended_viewport.get();
1552        let old_viewport_start = if vertical { old_viewport.top() } else { old_viewport.left() };
1553        let old_viewport_end = if vertical { old_viewport.bottom() } else { old_viewport.right() };
1554        let old_extended_viewport_start =
1555            if vertical { old_extended_viewport.top() } else { old_extended_viewport.left() };
1556        let old_extended_viewport_end =
1557            if vertical { old_extended_viewport.bottom() } else { old_extended_viewport.right() };
1558
1559        // Update current viewport.
1560        let viewport = e.effective_viewport().intersect(Rect::from_size(self.bounds().size()));
1561        self.viewport.set(viewport);
1562        self.is_waiting_for_viewport_update.set(false);
1563
1564        // Calculate buffer sizes based on viewport dimensions.
1565        let viewport_size = if vertical { viewport.height } else { viewport.width };
1566        let buffer_size = viewport_size * self.buffer_factor.get();
1567
1568        let extended_viewport = self.calculate_extended_viewport(vertical, viewport_size, buffer_size);
1569
1570        // Determine if we need a new measure.
1571        let new_viewport_start = if vertical { viewport.top() } else { viewport.left() };
1572        let new_viewport_end = if vertical { viewport.bottom() } else { viewport.right() };
1573        let new_extended_viewport_start = if vertical { extended_viewport.top() } else { extended_viewport.left() };
1574        let new_extended_viewport_end = if vertical { extended_viewport.bottom() } else { extended_viewport.right() };
1575
1576        let mut needs_measure = false;
1577
1578        // Case 1: Viewport has changed significantly.
1579        if !MathUtilities::are_close(old_viewport_start, new_viewport_start)
1580            || !MathUtilities::are_close(old_viewport_end, new_viewport_end)
1581        {
1582            // Case 1a: The new viewport exceeds the old extended viewport.
1583            if new_viewport_start < old_extended_viewport_start || new_viewport_end > old_extended_viewport_end {
1584                needs_measure = true;
1585            }
1586            // Case 1b: The extended viewport has changed significantly.
1587            else if !MathUtilities::are_close(old_extended_viewport_start, new_extended_viewport_start)
1588                || !MathUtilities::are_close(old_extended_viewport_end, new_extended_viewport_end)
1589            {
1590                // Check if we're about to scroll into an area where we don't
1591                // have realized elements. This would be the case if we're
1592                // near the edge of our current extended viewport.
1593                let mut nearing_edge = false;
1594
1595                if self.realized_elements.borrow().is_some() {
1596                    // If scrolling up/left and nearing the top/left edge of
1597                    // realized elements.
1598                    if new_viewport_start < old_viewport_start
1599                        && new_viewport_start - new_extended_viewport_start < buffer_size
1600                    {
1601                        // Edge case: We're at item 0 with excess measurement
1602                        // space. Skip re-measuring since we're at the list
1603                        // start and it won't change the result. This prevents
1604                        // redundant measure-arrange cycles when at list
1605                        // beginning.
1606                        nearing_edge = !self.has_reached_start.get();
1607                    }
1608
1609                    // If scrolling down/right and nearing the bottom/right
1610                    // edge of realized elements.
1611                    if new_viewport_end > old_viewport_end
1612                        && new_extended_viewport_end - new_viewport_end < buffer_size
1613                    {
1614                        // Edge case: We're at the last item with excess
1615                        // measurement space. Skip re-measuring since we're at
1616                        // the list end and it won't change the result. This
1617                        // prevents redundant measure-arrange cycles when at
1618                        // list beginning.
1619                        nearing_edge = !self.has_reached_end.get();
1620                    }
1621                } else {
1622                    nearing_edge = true;
1623                }
1624
1625                needs_measure = nearing_edge;
1626            }
1627        }
1628
1629        // Supplementary check: detect viewport growth after a previous
1630        // shrink. The main comparison (cases 1a/1b) uses the last measured
1631        // extended viewport which only updates on measure. When the viewport
1632        // shrinks (e.g. a popup during filtering), that viewport stays
1633        // stale-large, masking subsequent growth. Compare against the last
1634        // known extended viewport (always updated) to catch this case.
1635        if !needs_measure {
1636            let last_known = self.last_known_extended_viewport.get();
1637            let last_known_start = if vertical { last_known.top() } else { last_known.left() };
1638            let last_known_end = if vertical { last_known.bottom() } else { last_known.right() };
1639            if new_viewport_start < last_known_start || new_viewport_end > last_known_end {
1640                needs_measure = true;
1641            }
1642        }
1643
1644        self.last_known_extended_viewport.set(extended_viewport);
1645
1646        if needs_measure {
1647            // Only update the measure viewport when triggering a measure.
1648            // This keeps the wider realization range available for
1649            // externally-triggered measures (e.g. from `on_items_changed`),
1650            // ensuring enough items are realized.
1651            self.last_measured_extended_viewport.set(extended_viewport);
1652            self.invalidate_measure();
1653        }
1654    }
1655
1656    fn on_items_control_property_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1657        let focused_element = self.focused_element.borrow().clone();
1658        let Some(focused_element) = focused_element else { return };
1659
1660        if e.property() == KeyboardNavigation::tab_once_active_element_property().as_property()
1661            && e.get_old_value::<Option<Ref<InputElement>>>().flatten().is_some_and(|old| old == focused_element)
1662        {
1663            // The tab once active element has moved away from the focused
1664            // element so we can recycle it.
1665            self.recycle_element(&focused_element, self.focused_index.get());
1666            *self.focused_element.borrow_mut() = None;
1667            self.focused_index.set(-1);
1668        }
1669    }
1670
1671    fn on_cache_length_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1672        let new_value = e.get_new_value::<f64>();
1673        self.buffer_factor.set(new_value);
1674
1675        // Force a recalculation of the extended viewport on the next layout
1676        // pass.
1677        self.invalidate_measure();
1678    }
1679
1680    /// Returns the set of distances between irregular snap points for a
1681    /// specified orientation and alignment.
1682    pub fn get_irregular_snap_points(
1683        &self,
1684        orientation: Orientation,
1685        snap_points_alignment: SnapPointsAlignment,
1686    ) -> Vec<f64> {
1687        let Some(realized_elements) = self.realized_elements.borrow().clone() else {
1688            return Vec::new();
1689        };
1690
1691        let count = self
1692            .items_control()
1693            .and_then(|items_control| items_control.items_source())
1694            .map_or(0, |items_source| items_source.count() as i32);
1695
1696        VirtualizingSnapPointsList::new(
1697            realized_elements,
1698            count,
1699            orientation,
1700            self.orientation(),
1701            snap_points_alignment,
1702            self.estimate_element_size_u(),
1703        )
1704        .iter()
1705        .collect()
1706    }
1707
1708    /// Gets the distance between regular snap points for a specified
1709    /// orientation and alignment, and the offset of the first snap point.
1710    ///
1711    /// Panics if the snap points of the panel's orientation are not regular.
1712    pub fn get_regular_snap_points(
1713        &self,
1714        _orientation: Orientation,
1715        snap_points_alignment: SnapPointsAlignment,
1716    ) -> (f64, f64) {
1717        let mut offset = 0.0;
1718        let first_realized_child = {
1719            let realized_elements = self.realized_elements.borrow();
1720            realized_elements.as_ref().and_then(|realized_elements| realized_elements.element_at(0))
1721        };
1722
1723        let Some(first_realized_child) = first_realized_child else {
1724            return (0.0, offset);
1725        };
1726
1727        let bounds = first_realized_child.bounds();
1728
1729        let snap_point = match self.orientation() {
1730            Orientation::Horizontal => {
1731                if !self.are_horizontal_snap_points_regular() {
1732                    panic!("The horizontal snap points are not regular.");
1733                }
1734
1735                match snap_points_alignment {
1736                    SnapPointsAlignment::Near => offset = 0.0,
1737                    SnapPointsAlignment::Center => offset = (bounds.right() - bounds.left()) / 2.0,
1738                    SnapPointsAlignment::Far => offset = bounds.width,
1739                }
1740                bounds.width
1741            }
1742            Orientation::Vertical => {
1743                if !self.are_vertical_snap_points_regular() {
1744                    panic!("The vertical snap points are not regular.");
1745                }
1746
1747                match snap_points_alignment {
1748                    SnapPointsAlignment::Near => offset = 0.0,
1749                    SnapPointsAlignment::Center => offset = (bounds.bottom() - bounds.top()) / 2.0,
1750                    SnapPointsAlignment::Far => offset = bounds.height,
1751                }
1752                bounds.height
1753            }
1754        };
1755
1756        (snap_point, offset)
1757    }
1758}
1759
1760impl IScrollSnapPointsInfo for VirtualizingStackPanel {
1761    fn are_horizontal_snap_points_regular(&self) -> bool {
1762        VirtualizingStackPanel::are_horizontal_snap_points_regular(self)
1763    }
1764
1765    fn set_are_horizontal_snap_points_regular(&self, value: bool) {
1766        VirtualizingStackPanel::set_are_horizontal_snap_points_regular(self, value)
1767    }
1768
1769    fn are_vertical_snap_points_regular(&self) -> bool {
1770        VirtualizingStackPanel::are_vertical_snap_points_regular(self)
1771    }
1772
1773    fn set_are_vertical_snap_points_regular(&self, value: bool) {
1774        VirtualizingStackPanel::set_are_vertical_snap_points_regular(self, value)
1775    }
1776
1777    fn get_irregular_snap_points(
1778        &self,
1779        orientation: Orientation,
1780        snap_points_alignment: SnapPointsAlignment,
1781    ) -> Vec<f64> {
1782        VirtualizingStackPanel::get_irregular_snap_points(self, orientation, snap_points_alignment)
1783    }
1784
1785    fn get_regular_snap_points(
1786        &self,
1787        orientation: Orientation,
1788        snap_points_alignment: SnapPointsAlignment,
1789    ) -> (f64, f64) {
1790        VirtualizingStackPanel::get_regular_snap_points(self, orientation, snap_points_alignment)
1791    }
1792
1793    fn horizontal_snap_points_changed(&self, handler: SnapPointsChangedHandler) -> Rc<dyn IDisposable> {
1794        self.add_disposable_handler(Self::horizontal_snap_points_changed_event(), move |sender, e| handler(sender, e))
1795    }
1796
1797    fn vertical_snap_points_changed(&self, handler: SnapPointsChangedHandler) -> Rc<dyn IDisposable> {
1798        self.add_disposable_handler(Self::vertical_snap_points_changed_event(), move |sender, e| handler(sender, e))
1799    }
1800}