Skip to main content

ferroui_controls/pips_pager/
pips_pager.rs

1use super::{PipsPagerSelectedIndexChangedEventArgs, PipsPagerTemplateSettings};
2use crate::automation::AutomationProperties;
3use crate::primitives::{TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl, TemplatedControlImplExt};
4use crate::{Button, Control, ControlImpl, ListBox, ScrollViewer, StackPanel};
5use ferroui_base::data::BindingMode;
6use ferroui_base::input::{InputElementImpl, InputElementImplExt, Key, KeyEventArgs};
7use ferroui_base::interactivity::{Interactive, InteractiveImpl, RoutedEvent, RoutedEventHandlerToken, RoutingStrategies};
8use ferroui_base::layout::{LayoutableImpl, Orientation};
9use ferroui_base::reactive::IDisposable;
10use ferroui_base::styling::ControlTheme;
11use ferroui_base::threading::{CancellationToken, CancellationTokenSource, Dispatcher, DispatcherPriority, DispatcherTimer};
12use ferroui_base::{
13    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, instantiate,
14    DirectProperty, DirectPropertyMetadata, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Nullable,
15    Ref, StyledElementImpl, StyledProperty, StyledPropertyOptions, Vector, VisualImpl,
16};
17use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19use std::time::Duration;
20
21const PART_PREVIOUS_BUTTON: &str = "PART_PreviousButton";
22const PART_NEXT_BUTTON: &str = "PART_NextButton";
23const PART_PIPS_PAGER_LIST: &str = "PART_PipsPagerList";
24
25const PC_FIRST_PAGE: &str = ":first-page";
26const PC_LAST_PAGE: &str = ":last-page";
27const PC_VERTICAL: &str = ":vertical";
28const PC_HORIZONTAL: &str = ":horizontal";
29
30/// Represents a control that lets the user navigate through a paginated
31/// collection using a set of pips.
32#[repr(C)]
33pub struct PipsPager {
34    base: TemplatedControl,
35    previous_button: RefCell<Option<(Ref<Button>, RoutedEventHandlerToken)>>,
36    next_button: RefCell<Option<(Ref<Button>, RoutedEventHandlerToken)>>,
37    pips_pager_list: RefCell<Option<(Ref<ListBox>, RoutedEventHandlerToken)>>,
38    /// The `ContainerPrepared` / `ContainerIndexChanged` subscriptions on the pips list.
39    pips_pager_list_container_subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
40    scroll_pending: Cell<bool>,
41    updating_pager_size: Cell<bool>,
42    is_initial_load: Cell<bool>,
43    last_selected_page_index: Cell<i32>,
44    scroll_animation_cts: RefCell<Option<CancellationTokenSource>>,
45    template_settings: RefCell<Ref<PipsPagerTemplateSettings>>,
46}
47
48ferro_class!(PipsPager: TemplatedControl);
49
50ferro_class_info!(PipsPager {
51    new: PipsPager::new,
52    markup: {
53        attributes: [
54            TemplatePart("PART_PreviousButton", type(Ref<Button>)),
55            TemplatePart("PART_NextButton", type(Ref<Button>)),
56            TemplatePart("PART_PipsPagerList", type(Ref<ListBox>)),
57            PseudoClasses(":first-page", ":last-page", ":vertical", ":horizontal"),
58        ],
59    },
60});
61
62ferro_impl_classes!(PipsPager: StyledElementImpl, VisualImpl, LayoutableImpl, InteractiveImpl);
63
64impl FerroObjectImpl for PipsPager {
65    fn constructed(this: &Self) {
66        Self::parent_constructed(this);
67        this.update_pseudo_classes();
68    }
69}
70
71impl TemplatedControlImpl for PipsPager {
72    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
73        Self::parent_on_apply_template(this, e);
74
75        if let Some(cts) = this.scroll_animation_cts.take() {
76            cts.cancel();
77        }
78        this.is_initial_load.set(true);
79
80        // Unsubscribe from previous button events
81        if let Some((button, token)) = this.previous_button.take() {
82            button.remove_handler(Button::click_event(), token);
83        }
84
85        if let Some((button, token)) = this.next_button.take() {
86            button.remove_handler(Button::click_event(), token);
87        }
88
89        // Unsubscribe from previous list events
90        if let Some((list, token)) = this.pips_pager_list.take() {
91            list.remove_handler(Control::size_changed_event(), token);
92            for subscription in this.pips_pager_list_container_subscriptions.take() {
93                subscription.dispose();
94            }
95        }
96
97        // Get template parts
98        let previous_button = e.name_scope().find_as::<Button>(PART_PREVIOUS_BUTTON);
99        let next_button = e.name_scope().find_as::<Button>(PART_NEXT_BUTTON);
100        let pips_pager_list = e.name_scope().find_as::<ListBox>(PART_PIPS_PAGER_LIST);
101
102        let weak = this.to_ref().downgrade();
103
104        // Set up previous button
105        if let Some(button) = previous_button {
106            let weak = weak.clone();
107            let token = button.click(move |_, _| {
108                if let Some(this) = weak.upgrade() {
109                    this.previous_button_click();
110                }
111            });
112            AutomationProperties::set_name(&button, Some("Previous page"));
113            *this.previous_button.borrow_mut() = Some((button, token));
114        }
115
116        // Set up next button
117        if let Some(button) = next_button {
118            let weak = weak.clone();
119            let token = button.click(move |_, _| {
120                if let Some(this) = weak.upgrade() {
121                    this.next_button_click();
122                }
123            });
124            AutomationProperties::set_name(&button, Some("Next page"));
125            *this.next_button.borrow_mut() = Some((button, token));
126        }
127
128        // Set up pips list
129        if let Some(list) = pips_pager_list {
130            let size_changed_weak = weak.clone();
131            let token = list.size_changed(move |_, _| {
132                if let Some(this) = size_changed_weak.upgrade() {
133                    this.on_pips_pager_list_size_changed();
134                }
135            });
136            let prepared_weak = weak.clone();
137            let prepared = list.container_prepared(move |e| {
138                if let Some(this) = prepared_weak.upgrade() {
139                    this.update_container_automation_properties(e.container(), e.index());
140                }
141            });
142            let index_changed = list.container_index_changed(move |e| {
143                if let Some(this) = weak.upgrade() {
144                    this.update_container_automation_properties(e.container(), e.new_index());
145                }
146            });
147            *this.pips_pager_list_container_subscriptions.borrow_mut() = vec![prepared, index_changed];
148            *this.pips_pager_list.borrow_mut() = Some((list, token));
149        }
150
151        this.update_buttons_state();
152        this.update_pseudo_classes();
153        this.update_pager_size();
154    }
155}
156
157impl ControlImpl for PipsPager {
158    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
159        crate::automation::peers::PipsPagerAutomationPeer::new(this).upcast()
160    }
161}
162
163impl InputElementImpl for PipsPager {
164    fn on_key_down(this: &Self, e: &KeyEventArgs) {
165        Self::parent_on_key_down(this, e);
166
167        if e.handled() {
168            return;
169        }
170
171        let is_horizontal = this.orientation() == Orientation::Horizontal;
172
173        match e.key {
174            Key::Left | Key::Up if (e.key == Key::Left) == is_horizontal => {
175                if this.selected_page_index() > 0 {
176                    this.set_current_value(Self::selected_page_index_property(), this.selected_page_index() - 1);
177                    e.set_handled(true);
178                }
179            }
180            Key::Right | Key::Down if (e.key == Key::Right) == is_horizontal => {
181                if this.selected_page_index() < this.number_of_pages() - 1 {
182                    this.set_current_value(Self::selected_page_index_property(), this.selected_page_index() + 1);
183                    e.set_handled(true);
184                }
185            }
186            Key::Home => {
187                this.set_current_value(Self::selected_page_index_property(), 0);
188                e.set_handled(true);
189            }
190            Key::End => {
191                if this.number_of_pages() > 0 {
192                    this.set_current_value(Self::selected_page_index_property(), this.number_of_pages() - 1);
193                    e.set_handled(true);
194                }
195            }
196            _ => {}
197        }
198    }
199}
200
201ferro_properties! {
202    impl PipsPager {
203        /// Defines the `MaxVisiblePips` property.
204        pub fn max_visible_pips_property() -> StyledProperty<i32> {
205            FerroProperty::register::<PipsPager, _>("MaxVisiblePips", 5)
206        }
207
208        /// Defines the `IsNextButtonVisible` property.
209        pub fn is_next_button_visible_property() -> StyledProperty<bool> {
210            FerroProperty::register::<PipsPager, _>("IsNextButtonVisible", true)
211        }
212
213        /// Defines the `NumberOfPages` property.
214        pub fn number_of_pages_property() -> StyledProperty<i32> {
215            FerroProperty::register::<PipsPager, _>("NumberOfPages", 0)
216        }
217
218        /// Defines the `Orientation` property.
219        pub fn orientation_property() -> StyledProperty<Orientation> {
220            FerroProperty::register::<PipsPager, _>("Orientation", Orientation::Horizontal)
221        }
222
223        /// Defines the `IsPreviousButtonVisible` property.
224        pub fn is_previous_button_visible_property() -> StyledProperty<bool> {
225            FerroProperty::register::<PipsPager, _>("IsPreviousButtonVisible", true)
226        }
227
228        /// Defines the `SelectedPageIndex` property.
229        pub fn selected_page_index_property() -> StyledProperty<i32> {
230            FerroProperty::register_with::<PipsPager, _>(
231                "SelectedPageIndex",
232                StyledPropertyOptions::new(0).default_binding_mode(BindingMode::TwoWay),
233            )
234        }
235
236        /// Defines the `TemplateSettings` property.
237        pub fn template_settings_property() -> DirectProperty<PipsPager, Ref<PipsPagerTemplateSettings>> {
238            FerroProperty::register_direct_with::<PipsPager, _>(
239                "TemplateSettings",
240                |x| x.template_settings(),
241                None,
242                DirectPropertyMetadata::new(None),
243            )
244        }
245
246        /// Defines the `PreviousButtonTheme` property.
247        pub fn previous_button_theme_property() -> StyledProperty<Option<Ref<ControlTheme>>> {
248            FerroProperty::register::<PipsPager, _>("PreviousButtonTheme", None)
249        }
250
251        /// Defines the `NextButtonTheme` property.
252        pub fn next_button_theme_property() -> StyledProperty<Option<Ref<ControlTheme>>> {
253            FerroProperty::register::<PipsPager, _>("NextButtonTheme", None)
254        }
255    }
256}
257
258impl PipsPager {
259    ferro_routed_event!(
260        /// Defines the `SelectedIndexChanged` event.
261        pub fn selected_index_changed_event() -> RoutedEvent<PipsPagerSelectedIndexChangedEventArgs> {
262            RoutedEvent::register::<PipsPager, _>("SelectedIndexChanged", RoutingStrategies::BUBBLE)
263        }
264    );
265
266    fn static_constructor() {
267        // The list of pips is a declared typed list (`ferro_markup_list!`): the binding of
268        // the control theme delivers it to an items source property.
269        crate::markup_types::lists::PipsList::register();
270
271        Self::selected_page_index_property()
272            .changed()
273            .add_class_handler::<PipsPager>(|x, e| x.on_selected_page_index_changed(e));
274        Self::number_of_pages_property().changed().add_class_handler::<PipsPager>(|x, e| x.on_number_of_pages_changed(e));
275        Self::is_previous_button_visible_property()
276            .changed()
277            .add_class_handler::<PipsPager>(|x, _| x.on_is_previous_button_visible_changed());
278        Self::is_next_button_visible_property()
279            .changed()
280            .add_class_handler::<PipsPager>(|x, _| x.on_is_next_button_visible_changed());
281        Self::orientation_property().changed().add_class_handler::<PipsPager>(|x, _| x.on_orientation_changed());
282        Self::max_visible_pips_property()
283            .changed()
284            .add_class_handler::<PipsPager>(|x, e| x.on_max_visible_pips_changed(e));
285    }
286
287    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
288    pub fn construct() -> Self {
289        Self {
290            base: TemplatedControl::construct(),
291            previous_button: RefCell::new(None),
292            next_button: RefCell::new(None),
293            pips_pager_list: RefCell::new(None),
294            pips_pager_list_container_subscriptions: RefCell::new(Vec::new()),
295            scroll_pending: Cell::new(false),
296            updating_pager_size: Cell::new(false),
297            is_initial_load: Cell::new(false),
298            last_selected_page_index: Cell::new(0),
299            scroll_animation_cts: RefCell::new(None),
300            template_settings: RefCell::new(PipsPagerTemplateSettings::new()),
301        }
302    }
303
304    /// Initializes a new instance of the pips pager.
305    pub fn new() -> Ref<Self> {
306        instantiate(Self::construct())
307    }
308
309    /// Occurs when the selected index has changed.
310    pub fn selected_index_changed(
311        &self,
312        handler: impl Fn(&Interactive, &PipsPagerSelectedIndexChangedEventArgs) + 'static,
313    ) -> RoutedEventHandlerToken {
314        self.add_handler(Self::selected_index_changed_event(), handler)
315    }
316
317    /// Gets or sets the maximum number of visible pips.
318    pub fn max_visible_pips(&self) -> i32 {
319        self.get_value(Self::max_visible_pips_property())
320    }
321
322    pub fn set_max_visible_pips(&self, value: i32) {
323        self.set_value(Self::max_visible_pips_property(), value)
324    }
325
326    /// Gets or sets the visibility of the next button.
327    pub fn is_next_button_visible(&self) -> bool {
328        self.get_value(Self::is_next_button_visible_property())
329    }
330
331    pub fn set_is_next_button_visible(&self, value: bool) {
332        self.set_value(Self::is_next_button_visible_property(), value)
333    }
334
335    fn update_container_automation_properties(&self, container: &Ref<Control>, index: i32) {
336        AutomationProperties::set_name(container, Some(&format!("Page {}", index + 1)));
337        AutomationProperties::set_position_in_set(container, index + 1);
338        AutomationProperties::set_size_of_set(container, self.number_of_pages());
339    }
340
341    /// Gets or sets the number of pages.
342    pub fn number_of_pages(&self) -> i32 {
343        self.get_value(Self::number_of_pages_property())
344    }
345
346    pub fn set_number_of_pages(&self, value: i32) {
347        self.set_value(Self::number_of_pages_property(), value)
348    }
349
350    /// Gets or sets the orientation of the pips.
351    pub fn orientation(&self) -> Orientation {
352        self.get_value(Self::orientation_property())
353    }
354
355    pub fn set_orientation(&self, value: Orientation) {
356        self.set_value(Self::orientation_property(), value)
357    }
358
359    /// Gets or sets the visibility of the previous button.
360    pub fn is_previous_button_visible(&self) -> bool {
361        self.get_value(Self::is_previous_button_visible_property())
362    }
363
364    pub fn set_is_previous_button_visible(&self, value: bool) {
365        self.set_value(Self::is_previous_button_visible_property(), value)
366    }
367
368    /// Gets or sets the current selected page index.
369    pub fn selected_page_index(&self) -> i32 {
370        self.get_value(Self::selected_page_index_property())
371    }
372
373    pub fn set_selected_page_index(&self, value: i32) {
374        self.set_value(Self::selected_page_index_property(), value)
375    }
376
377    /// Gets the template settings.
378    pub fn template_settings(&self) -> Ref<PipsPagerTemplateSettings> {
379        self.template_settings.borrow().clone()
380    }
381
382    // The upstream private setter of `TemplateSettings` is never called: the
383    // settings created with the control are kept for its lifetime.
384
385    /// Gets or sets the theme for the previous button.
386    pub fn previous_button_theme(&self) -> Option<Ref<ControlTheme>> {
387        self.get_value(Self::previous_button_theme_property())
388    }
389
390    pub fn set_previous_button_theme(&self, value: impl Into<Nullable<ControlTheme>>) {
391        self.set_value(Self::previous_button_theme_property(), value.into().0)
392    }
393
394    /// Gets or sets the theme for the next button.
395    pub fn next_button_theme(&self) -> Option<Ref<ControlTheme>> {
396        self.get_value(Self::next_button_theme_property())
397    }
398
399    pub fn set_next_button_theme(&self, value: impl Into<Nullable<ControlTheme>>) {
400        self.set_value(Self::next_button_theme_property(), value.into().0)
401    }
402
403    fn previous_button_part(&self) -> Option<Ref<Button>> {
404        self.previous_button.borrow().as_ref().map(|(button, _)| button.clone())
405    }
406
407    fn next_button_part(&self) -> Option<Ref<Button>> {
408        self.next_button.borrow().as_ref().map(|(button, _)| button.clone())
409    }
410
411    fn pips_pager_list_part(&self) -> Option<Ref<ListBox>> {
412        self.pips_pager_list.borrow().as_ref().map(|(list, _)| list.clone())
413    }
414
415    fn on_selected_page_index_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
416        let (old_index, new_index) = e.get_old_and_new_value::<i32>();
417
418        if new_index < 0 {
419            self.set_current_value(Self::selected_page_index_property(), 0);
420            return;
421        }
422
423        if self.number_of_pages() > 0 {
424            if new_index >= self.number_of_pages() {
425                self.set_current_value(Self::selected_page_index_property(), self.number_of_pages() - 1);
426                return;
427            }
428        } else if new_index > 0 {
429            self.set_current_value(Self::selected_page_index_property(), 0);
430            return;
431        }
432
433        self.last_selected_page_index.set(old_index);
434
435        self.update_buttons_state();
436        self.update_pseudo_classes();
437        self.request_scroll_to_selected_pip();
438
439        self.raise_event(&PipsPagerSelectedIndexChangedEventArgs::new(old_index, new_index));
440    }
441
442    fn on_number_of_pages_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
443        let new_value = e.get_new_value::<i32>();
444
445        if new_value < 0 {
446            self.set_current_value(Self::number_of_pages_property(), 0);
447            return;
448        }
449
450        let pips = self.template_settings().pips();
451        let count = pips.count() as i32;
452
453        if count < new_value {
454            let start = count + 1;
455            pips.add_range(start..=new_value);
456        } else if count > new_value {
457            pips.remove_range(new_value as usize, (count - new_value) as usize);
458        }
459
460        let mut index_clamped = false;
461
462        if new_value > 0 && self.selected_page_index() >= new_value {
463            self.set_current_value(Self::selected_page_index_property(), new_value - 1);
464            index_clamped = true;
465        } else if new_value == 0 && self.selected_page_index() > 0 {
466            self.set_current_value(Self::selected_page_index_property(), 0);
467            index_clamped = true;
468        }
469
470        if !index_clamped {
471            self.update_buttons_state();
472            self.update_pseudo_classes();
473        }
474
475        self.update_pager_size();
476    }
477
478    fn on_is_previous_button_visible_changed(&self) {
479        self.update_buttons_state();
480    }
481
482    fn on_is_next_button_visible_changed(&self) {
483        self.update_buttons_state();
484    }
485
486    fn on_orientation_changed(&self) {
487        self.update_pseudo_classes();
488        self.update_pager_size();
489    }
490
491    fn on_max_visible_pips_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
492        let new_value = e.get_new_value::<i32>();
493
494        if new_value < 1 {
495            self.set_current_value(Self::max_visible_pips_property(), 1);
496            return;
497        }
498
499        self.update_pager_size();
500    }
501
502    fn previous_button_click(&self) {
503        if self.selected_page_index() > 0 {
504            self.set_current_value(Self::selected_page_index_property(), self.selected_page_index() - 1);
505        }
506    }
507
508    fn next_button_click(&self) {
509        if self.selected_page_index() < self.number_of_pages() - 1 {
510            self.set_current_value(Self::selected_page_index_property(), self.selected_page_index() + 1);
511        }
512    }
513
514    fn update_buttons_state(&self) {
515        if let Some(previous_button) = self.previous_button_part() {
516            previous_button.set_is_enabled(self.selected_page_index() > 0);
517        }
518
519        if let Some(next_button) = self.next_button_part() {
520            next_button.set_is_enabled(self.selected_page_index() < self.number_of_pages() - 1);
521        }
522    }
523
524    fn update_pseudo_classes(&self) {
525        let pseudo_classes = self.pseudo_classes();
526        pseudo_classes.set(PC_FIRST_PAGE, self.selected_page_index() == 0);
527        pseudo_classes
528            .set(PC_LAST_PAGE, self.number_of_pages() > 0 && self.selected_page_index() >= self.number_of_pages() - 1);
529        pseudo_classes.set(PC_VERTICAL, self.orientation() == Orientation::Vertical);
530        pseudo_classes.set(PC_HORIZONTAL, self.orientation() == Orientation::Horizontal);
531    }
532
533    fn on_pips_pager_list_size_changed(&self) {
534        if !self.updating_pager_size.get() {
535            self.update_pager_size();
536        }
537    }
538
539    fn request_scroll_to_selected_pip(&self) {
540        if self.scroll_pending.get() {
541            return;
542        }
543
544        self.scroll_pending.set(true);
545        let this = self.to_ref();
546        Dispatcher::ui_thread().post_local(
547            move || {
548                this.scroll_pending.set(false);
549                this.scroll_to_selected_pip();
550            },
551            DispatcherPriority::INPUT,
552        );
553    }
554
555    fn scroll_to_selected_pip(&self) {
556        let Some(list) = self.pips_pager_list_part() else {
557            return;
558        };
559
560        if self.number_of_pages() <= self.max_visible_pips() {
561            return;
562        }
563
564        let Some(scroll_viewer) = list.scroll().and_then(|scroll| scroll.cast::<ScrollViewer>()) else {
565            return;
566        };
567
568        let Some(container) = list.container_from_index(self.selected_page_index()) else {
569            return;
570        };
571
572        let is_horizontal = self.orientation() == Orientation::Horizontal;
573        let margin = container.margin();
574        let pip_size = if is_horizontal {
575            container.bounds().width + margin.left + margin.right
576        } else {
577            container.bounds().height + margin.top + margin.bottom
578        };
579
580        if pip_size <= 0.0 {
581            return;
582        }
583
584        let max_visible_pips = self.max_visible_pips();
585        let even_offset =
586            if max_visible_pips % 2 == 0 && self.selected_page_index() > self.last_selected_page_index.get() {
587                1
588            } else {
589                0
590            };
591        let offset_elements = self.selected_page_index() + even_offset - max_visible_pips / 2;
592        let mut target_offset = f64::max(0.0, f64::from(offset_elements) * pip_size);
593        let max_offset = if is_horizontal {
594            scroll_viewer.extent().width - scroll_viewer.viewport().width
595        } else {
596            scroll_viewer.extent().height - scroll_viewer.viewport().height
597        };
598        target_offset = f64::min(target_offset, f64::max(0.0, max_offset));
599
600        if self.is_initial_load.get() {
601            self.is_initial_load.set(false);
602            Self::set_scroll_offset(&scroll_viewer, target_offset, is_horizontal);
603            return;
604        }
605
606        self.animate_scroll_offset(&scroll_viewer, target_offset, is_horizontal);
607    }
608
609    fn set_scroll_offset(scroll_viewer: &Ref<ScrollViewer>, offset: f64, is_horizontal: bool) {
610        let current = scroll_viewer.offset();
611        scroll_viewer.set_offset(if is_horizontal {
612            Vector::new(offset, current.y)
613        } else {
614            Vector::new(current.x, offset)
615        });
616    }
617
618    fn animate_scroll_offset(&self, scroll_viewer: &Ref<ScrollViewer>, target_offset: f64, is_horizontal: bool) {
619        if let Some(cts) = self.scroll_animation_cts.take() {
620            cts.cancel();
621        }
622        let cts = CancellationTokenSource::new();
623        let token = cts.token();
624        *self.scroll_animation_cts.borrow_mut() = Some(cts);
625
626        let start_offset = if is_horizontal { scroll_viewer.offset().x } else { scroll_viewer.offset().y };
627        let delta = target_offset - start_offset;
628
629        if delta.abs() < 0.5 {
630            Self::set_scroll_offset(scroll_viewer, target_offset, is_horizontal);
631            return;
632        }
633
634        const DURATION_MS: i32 = 200;
635        const FRAME_MS: u64 = 16;
636        // The tick count of upstream is the clock of the dispatcher here.
637        let start_time = Dispatcher::ui_thread().now();
638
639        let scroll_viewer = scroll_viewer.clone();
640        DispatcherTimer::run_once(
641            move || {
642                Self::animate_step(&scroll_viewer, start_offset, delta, start_time, DURATION_MS, is_horizontal, &token)
643            },
644            Duration::from_millis(FRAME_MS),
645            DispatcherPriority::RENDER,
646        );
647    }
648
649    fn animate_step(
650        scroll_viewer: &Ref<ScrollViewer>,
651        start_offset: f64,
652        delta: f64,
653        start_time: i64,
654        duration_ms: i32,
655        is_horizontal: bool,
656        token: &CancellationToken,
657    ) {
658        if token.is_cancellation_requested() {
659            return;
660        }
661
662        let elapsed = Dispatcher::ui_thread().now() - start_time;
663        let t = f64::min(1.0, elapsed as f64 / f64::from(duration_ms));
664        let eased = 1.0 - (1.0 - t).powi(3);
665        let current = start_offset + (delta * eased);
666
667        Self::set_scroll_offset(scroll_viewer, current, is_horizontal);
668
669        if t < 1.0 {
670            let scroll_viewer = scroll_viewer.clone();
671            let token = token.clone();
672            DispatcherTimer::run_once(
673                move || {
674                    Self::animate_step(
675                        &scroll_viewer,
676                        start_offset,
677                        delta,
678                        start_time,
679                        duration_ms,
680                        is_horizontal,
681                        &token,
682                    )
683                },
684                Duration::from_millis(16),
685                DispatcherPriority::RENDER,
686            );
687        }
688    }
689
690    fn update_pager_size(&self) {
691        let Some(list) = self.pips_pager_list_part() else {
692            return;
693        };
694
695        // `try`/`finally` of the reference: the flag is reset on every path.
696        struct Reset<'a>(&'a Cell<bool>);
697        impl Drop for Reset<'_> {
698            fn drop(&mut self) {
699                self.0.set(false);
700            }
701        }
702
703        self.updating_pager_size.set(true);
704        let _reset = Reset(&self.updating_pager_size);
705        self.update_pager_size_core(&list);
706    }
707
708    fn update_pager_size_core(&self, list: &Ref<ListBox>) {
709        let mut pip_size = 12.0;
710
711        let mut container = list.container_from_index(self.selected_page_index());
712
713        if container.is_none() && list.items().count() > 0 {
714            container = list.container_from_index(0);
715        }
716
717        if let Some(container) = container {
718            let margin = container.margin();
719            let size = if self.orientation() == Orientation::Horizontal {
720                container.bounds().width + margin.left + margin.right
721            } else {
722                container.bounds().height + margin.top + margin.bottom
723            };
724
725            if size > 0.0 {
726                pip_size = size;
727            }
728        }
729
730        let mut spacing = 0.0;
731
732        if let Some(items_panel) = list.items_panel_root().and_then(|panel| panel.cast::<StackPanel>()) {
733            spacing = items_panel.spacing();
734        }
735
736        let visible_count = i32::min(self.number_of_pages(), self.max_visible_pips());
737
738        if visible_count <= 0 {
739            return;
740        }
741
742        let extent = (f64::from(visible_count) * pip_size) + (f64::from(visible_count - 1) * spacing);
743
744        if self.orientation() == Orientation::Horizontal {
745            list.set_width(extent);
746            list.set_height(f64::NAN);
747        } else {
748            list.set_height(extent);
749            list.set_width(f64::NAN);
750        }
751
752        self.request_scroll_to_selected_pip();
753    }
754}