Skip to main content

ferroui_controls/primitives/
scroll_bar.rs

1use super::{
2    RangeBase, ScrollBarVisibility, ScrollEventType, TemplateAppliedEventArgs, TemplatedControlImpl, Thumb, Track,
3};
4use crate::metadata::{PseudoClassesAttribute, TemplatePartAttribute};
5use crate::{Button, ControlImpl, ScrollViewer};
6use ferroui_base::animation::TimeSpan;
7use ferroui_base::data::BindingPriority;
8use ferroui_base::input::{
9    ContextRequestedEventArgs, InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs,
10    PointerEventArgs, PointerPointProperties, PointerPressedEventArgs, PointerType, PointerUpdateKind,
11    PointerWheelEventArgs,
12    RawInputModifiers, VectorEventArgs,
13};
14use ferroui_base::interactivity::{InteractiveImpl, RoutedEventHandlerToken, RoutingStrategies};
15use ferroui_base::layout::{LayoutableImpl, Orientation};
16use ferroui_base::reactive::{Disposable, IDisposable};
17use ferroui_base::threading::{DispatcherPriority, DispatcherTimer};
18use ferroui_base::utilities::HandlerList;
19use ferroui_base::{
20    ferro_class, ferro_impl_classes, ferro_property, instantiate, DirectProperty, FerroObject, FerroObjectExtensions,
21    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Point, Ref, Size, StaticType,
22    StyledElementImpl, StyledProperty, Vector, Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
23    WeakRef,
24};
25use std::cell::{Cell, RefCell};
26use std::rc::Rc;
27use std::time::Duration;
28
29const PC_VERTICAL: &str = ":vertical";
30const PC_HORIZONTAL: &str = ":horizontal";
31
32/// Provides data for the scroll event of a [`ScrollBar`].
33#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct ScrollEventArgs {
35    new_value: f64,
36    scroll_event_type: ScrollEventType,
37}
38
39impl ScrollEventArgs {
40    pub fn new(event_type: ScrollEventType, new_value: f64) -> Self {
41        Self { new_value, scroll_event_type: event_type }
42    }
43
44    /// The new value of the scroll bar.
45    pub fn new_value(&self) -> f64 {
46        self.new_value
47    }
48
49    /// The type of the scroll event.
50    pub fn scroll_event_type(&self) -> ScrollEventType {
51        self.scroll_event_type
52    }
53}
54
55/// The action invoked when the expand/collapse timer elapses.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57enum DelayedAction {
58    Collapse,
59    Expand,
60}
61
62/// A scrollbar control.
63#[repr(C)]
64pub struct ScrollBar {
65    base: RangeBase,
66    line_up_button: RefCell<Option<(Ref<Button>, RoutedEventHandlerToken)>>,
67    line_down_button: RefCell<Option<(Ref<Button>, RoutedEventHandlerToken)>>,
68    page_up_button: RefCell<Option<(Ref<Button>, RoutedEventHandlerToken)>>,
69    page_down_button: RefCell<Option<(Ref<Button>, RoutedEventHandlerToken)>>,
70    timer: RefCell<Option<Rc<DispatcherTimer>>>,
71    timer_action: Cell<Option<DelayedAction>>,
72    is_expanded: Cell<bool>,
73    owner_subscriptions: RefCell<Option<Vec<Rc<dyn IDisposable>>>>,
74    owner: RefCell<Option<WeakRef<ScrollViewer>>>,
75    is_dragging: Cell<bool>,
76    last_right_click_position: Cell<Point>,
77    scroll: HandlerList<dyn Fn(&ScrollEventArgs)>,
78}
79
80ferro_class!(ScrollBar: RangeBase);
81// The `markup:` part is declared here and not generated: the public methods of the class are
82// declared for markup, the commands of the context menu of the scroll bar in the control themes
83// (`Command="{Binding $parent[ScrollBar].ScrollHere}"`).
84ferroui_base::ferro_class_info!(ScrollBar {
85    new: ScrollBar::new,
86    markup: {
87        methods: [
88            fn ScrollHere() => ScrollBar::scroll_here,
89            fn ScrollToHome() => ScrollBar::scroll_to_home,
90            fn ScrollToEnd() => ScrollBar::scroll_to_end,
91            fn PageUp() => ScrollBar::page_up,
92            fn PageDown() => ScrollBar::page_down,
93            fn PageLeft() => ScrollBar::page_left,
94            fn PageRight() => ScrollBar::page_right,
95            fn LineUp() => ScrollBar::line_up,
96            fn LineDown() => ScrollBar::line_down,
97            fn LineLeft() => ScrollBar::line_left,
98            fn LineRight() => ScrollBar::line_right,
99        ],
100        events: [
101            Scroll(Option<ferroui_base::BoxedValue>, crate::primitives::ScrollEventArgs) => |this: &ferroui_base::Ref<ScrollBar>, handler: ferroui_base::metadata::MarkupDelegate| {
102                let sender = this.downgrade();
103                this.scroll(move |e: &crate::primitives::ScrollEventArgs| {
104                    handler.invoke(&[ferroui_base::metadata::into_markup_value(sender.upgrade()), ferroui_base::metadata::into_markup_value(e.clone())]);
105                })
106            },
107        ],
108        attributes: [
109            TemplatePart("PART_LineDownButton", type(ferroui_base::Ref<crate::Button>)),
110            TemplatePart("PART_LineUpButton", type(ferroui_base::Ref<crate::Button>)),
111            TemplatePart("PART_PageDownButton", type(ferroui_base::Ref<crate::Button>)),
112            TemplatePart("PART_PageUpButton", type(ferroui_base::Ref<crate::Button>)),
113            PseudoClasses(":vertical", ":horizontal"),
114        ],
115    },
116});
117ferro_impl_classes!(ScrollBar: StyledElementImpl, LayoutableImpl, InteractiveImpl);
118
119impl ControlImpl for ScrollBar {
120    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
121        crate::automation::peers::ScrollBarAutomationPeer::new(this).upcast()
122    }
123}
124
125impl VisualImpl for ScrollBar {
126    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
127        Self::parent_on_attached_to_visual_tree(this, e);
128        this.attach_to_scroll_viewer();
129    }
130}
131
132impl FerroObjectImpl for ScrollBar {
133    fn constructed(this: &Self) {
134        Self::parent_constructed(this);
135        this.update_pseudo_classes(this.orientation());
136        this.add_handler(InputElement::context_requested_event(), |sender, e| {
137            if let Some(this) = sender.downcast_ref::<ScrollBar>() {
138                this.on_context_requested(e);
139            }
140        });
141    }
142
143    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
144        Self::parent_on_property_changed(this, change);
145
146        let property = change.property();
147
148        if property == Self::orientation_property().as_property() {
149            this.update_pseudo_classes(change.get_new_value::<Orientation>());
150            if this.is_attached_to_visual_tree() {
151                // There's no way to manually refresh bindings, so reapply
152                // them.
153                this.attach_to_scroll_viewer();
154            }
155        } else if property == Self::allow_auto_hide_property().as_property() {
156            this.update_is_expanded_state();
157        } else if property == RangeBase::value_property().as_property() {
158            let value = change.get_new_value::<f64>();
159            if let Some(owner) = this.owner() {
160                let offset = owner.offset();
161                owner.set_current_value(
162                    ScrollViewer::offset_property(),
163                    if this.orientation() == Orientation::Horizontal {
164                        offset.with_x(value)
165                    } else {
166                        offset.with_y(value)
167                    },
168                );
169            }
170        } else if property == RangeBase::minimum_property().as_property()
171            || property == RangeBase::maximum_property().as_property()
172            || property == Self::viewport_size_property().as_property()
173            || property == Self::visibility_property().as_property()
174        {
175            this.update_is_visible();
176        }
177    }
178}
179
180impl InputElementImpl for ScrollBar {
181    fn on_key_down(this: &Self, e: &KeyEventArgs) {
182        if this.orientation() == Orientation::Vertical {
183            if e.key == Key::PageUp {
184                this.large_decrement();
185                e.set_handled(true);
186            } else if e.key == Key::PageDown {
187                this.large_increment();
188                e.set_handled(true);
189            } else if e.key == Key::Up {
190                this.small_decrement();
191                e.set_handled(true);
192            } else if e.key == Key::Down {
193                this.small_increment();
194                e.set_handled(true);
195            }
196        } else if this.orientation() == Orientation::Horizontal {
197            if e.key == Key::Left {
198                this.small_decrement();
199                e.set_handled(true);
200            } else if e.key == Key::Right {
201                this.small_increment();
202                e.set_handled(true);
203            }
204        }
205    }
206
207    fn on_pointer_wheel_changed(this: &Self, e: &PointerWheelEventArgs) {
208        Self::parent_on_pointer_wheel_changed(this, e);
209
210        // We need to handle the pointer wheel event to allow scrolling with
211        // the pointer wheel. So we raise the event on the scroll viewer's
212        // presenter.
213        if e.handled() {
214            return;
215        }
216
217        let presenter = this.owner().and_then(|owner| owner.presenter());
218        if let (Some(presenter), Some(root)) = (presenter, this.visual_root()) {
219            e.set_handled(true);
220            let key_modifiers = e.key_modifiers();
221            let e = PointerWheelEventArgs::new(
222                this.to_ref(),
223                e.pointer().clone(),
224                &root,
225                e.get_position(Some(&root)),
226                e.timestamp(),
227                PointerPointProperties::new(
228                    RawInputModifiers::from_bits_truncate(key_modifiers.bits()),
229                    PointerUpdateKind::Other,
230                ),
231                key_modifiers,
232                e.delta(),
233            );
234            presenter.raise_event(&e);
235        }
236    }
237
238    fn on_pointer_entered(this: &Self, e: &PointerEventArgs) {
239        Self::parent_on_pointer_entered(this, e);
240
241        if this.allow_auto_hide() {
242            this.expand_after_delay();
243        }
244    }
245
246    fn on_pointer_exited(this: &Self, e: &PointerEventArgs) {
247        Self::parent_on_pointer_exited(this, e);
248
249        if this.allow_auto_hide() && !this.is_dragging.get() {
250            this.collapse_after_delay();
251        }
252    }
253
254    fn on_pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
255        Self::parent_on_pointer_pressed(this, e);
256
257        if e.get_current_point(Some(this)).properties.is_right_button_pressed {
258            this.last_right_click_position.set(e.get_position(Some(this)));
259        }
260    }
261}
262
263impl TemplatedControlImpl for ScrollBar {
264    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
265        for slot in [&this.line_up_button, &this.line_down_button, &this.page_up_button, &this.page_down_button] {
266            let previous = slot.borrow_mut().take();
267            if let Some((button, token)) = previous {
268                button.remove_handler(Button::click_event(), token);
269            }
270        }
271
272        let attach = |slot: &RefCell<Option<(Ref<Button>, RoutedEventHandlerToken)>>,
273                      name: &str,
274                      action: fn(&ScrollBar)| {
275            if let Some(button) = e.name_scope().find_as::<Button>(name) {
276                let weak = this.to_ref().downgrade();
277                let token = button.click(move |_, _| {
278                    if let Some(this) = weak.upgrade() {
279                        action(&this);
280                    }
281                });
282                *slot.borrow_mut() = Some((button, token));
283            }
284        };
285
286        attach(&this.line_up_button, "PART_LineUpButton", Self::small_decrement);
287        attach(&this.line_down_button, "PART_LineDownButton", Self::small_increment);
288        attach(&this.page_up_button, "PART_PageUpButton", Self::large_decrement);
289        attach(&this.page_down_button, "PART_PageDownButton", Self::large_increment);
290    }
291}
292
293impl ScrollBar {
294    /// The named parts expected in the control template.
295    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] = &[
296        TemplatePartAttribute::new("PART_LineDownButton", <Button as StaticType>::TYPE),
297        TemplatePartAttribute::new("PART_LineUpButton", <Button as StaticType>::TYPE),
298        TemplatePartAttribute::new("PART_PageDownButton", <Button as StaticType>::TYPE),
299        TemplatePartAttribute::new("PART_PageUpButton", <Button as StaticType>::TYPE),
300    ];
301
302    /// The pseudoclasses set by the class.
303    pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_VERTICAL, PC_HORIZONTAL]);
304}
305
306ferroui_base::ferro_properties! { impl ScrollBar {
307    ferro_property!(
308        /// Defines the `ViewportSize` property.
309        pub fn viewport_size_property() -> StyledProperty<f64> {
310            FerroProperty::register::<ScrollBar, _>("ViewportSize", f64::NAN)
311        }
312    );
313
314    ferro_property!(
315        /// Defines the `Visibility` property.
316        pub fn visibility_property() -> StyledProperty<ScrollBarVisibility> {
317            FerroProperty::register::<ScrollBar, _>("Visibility", ScrollBarVisibility::Visible)
318        }
319    );
320
321    ferro_property!(
322        /// Defines the `Orientation` property.
323        pub fn orientation_property() -> StyledProperty<Orientation> {
324            FerroProperty::register::<ScrollBar, _>("Orientation", Orientation::Vertical)
325        }
326    );
327
328    ferro_property!(
329        /// Defines the `IsExpanded` property.
330        pub fn is_expanded_property() -> DirectProperty<ScrollBar, bool> {
331            FerroProperty::register_direct::<ScrollBar, _>("IsExpanded", |o| o.is_expanded(), None, false)
332        }
333    );
334
335    ferro_property!(
336        /// Defines the `AllowAutoHide` property.
337        pub fn allow_auto_hide_property() -> StyledProperty<bool> {
338            FerroProperty::register::<ScrollBar, _>("AllowAutoHide", true)
339        }
340    );
341
342    ferro_property!(
343        /// Defines the `HideDelay` property.
344        pub fn hide_delay_property() -> StyledProperty<TimeSpan> {
345            FerroProperty::register::<ScrollBar, _>("HideDelay", TimeSpan::from_seconds(2.0))
346        }
347    );
348
349    ferro_property!(
350        /// Defines the `ShowDelay` property.
351        pub fn show_delay_property() -> StyledProperty<TimeSpan> {
352            FerroProperty::register::<ScrollBar, _>("ShowDelay", TimeSpan::from_seconds(0.5))
353        }
354    );
355} }
356
357impl ScrollBar {
358    fn static_constructor() {
359        Thumb::drag_delta_event().add_class_handler_with::<ScrollBar>(
360            |x, e| x.on_thumb_drag_delta(e),
361            RoutingStrategies::BUBBLE,
362            false,
363        );
364        Thumb::drag_started_event().add_class_handler_with::<ScrollBar>(
365            |x, e| x.on_thumb_drag_start(e),
366            RoutingStrategies::BUBBLE,
367            false,
368        );
369        Thumb::drag_completed_event().add_class_handler_with::<ScrollBar>(
370            |x, e| x.on_thumb_drag_complete(e),
371            RoutingStrategies::BUBBLE,
372            false,
373        );
374
375        InputElement::focusable_property().override_default_value::<ScrollBar>(false);
376    }
377
378    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
379    pub fn construct() -> Self {
380        Self {
381            base: RangeBase::construct(),
382            line_up_button: RefCell::new(None),
383            line_down_button: RefCell::new(None),
384            page_up_button: RefCell::new(None),
385            page_down_button: RefCell::new(None),
386            timer: RefCell::new(None),
387            timer_action: Cell::new(None),
388            is_expanded: Cell::new(false),
389            owner_subscriptions: RefCell::new(None),
390            owner: RefCell::new(None),
391            is_dragging: Cell::new(false),
392            last_right_click_position: Cell::new(Point::default()),
393            scroll: HandlerList::new(),
394        }
395    }
396
397    pub fn new() -> Ref<Self> {
398        instantiate(Self::construct())
399    }
400
401    /// The amount of the scrollable content that is currently visible.
402    pub fn viewport_size(&self) -> f64 {
403        self.get_value(Self::viewport_size_property())
404    }
405
406    pub fn set_viewport_size(&self, value: f64) {
407        self.set_value(Self::viewport_size_property(), value)
408    }
409
410    /// Indicates whether the scrollbar should hide itself when it is not
411    /// needed.
412    pub fn visibility(&self) -> ScrollBarVisibility {
413        self.get_value(Self::visibility_property())
414    }
415
416    pub fn set_visibility(&self, value: ScrollBarVisibility) {
417        self.set_value(Self::visibility_property(), value)
418    }
419
420    /// The orientation of the scrollbar.
421    pub fn orientation(&self) -> Orientation {
422        self.get_value(Self::orientation_property())
423    }
424
425    pub fn set_orientation(&self, value: Orientation) {
426        self.set_value(Self::orientation_property(), value)
427    }
428
429    /// Indicates whether the scrollbar is expanded.
430    pub fn is_expanded(&self) -> bool {
431        self.is_expanded.get()
432    }
433
434    fn set_is_expanded(&self, value: bool) {
435        self.set_and_raise_cell(Self::is_expanded_property(), &self.is_expanded, value);
436    }
437
438    /// Indicates whether the scrollbar can hide itself when the user is not
439    /// interacting with it.
440    pub fn allow_auto_hide(&self) -> bool {
441        self.get_value(Self::allow_auto_hide_property())
442    }
443
444    pub fn set_allow_auto_hide(&self, value: bool) {
445        self.set_value(Self::allow_auto_hide_property(), value)
446    }
447
448    /// Determines how long the hide delay will be after the user stops
449    /// interacting with the scrollbar.
450    pub fn hide_delay(&self) -> TimeSpan {
451        self.get_value(Self::hide_delay_property())
452    }
453
454    pub fn set_hide_delay(&self, value: TimeSpan) {
455        self.set_value(Self::hide_delay_property(), value)
456    }
457
458    /// Determines how long the show delay will be when the user starts
459    /// interacting with the scrollbar.
460    pub fn show_delay(&self) -> TimeSpan {
461        self.get_value(Self::show_delay_property())
462    }
463
464    pub fn set_show_delay(&self, value: TimeSpan) {
465        self.set_value(Self::show_delay_property(), value)
466    }
467
468    /// Raised when the scrollbar scrolls. The handler receives the event
469    /// data, as the handlers of the other plain events do.
470    pub fn scroll(&self, handler: impl Fn(&ScrollEventArgs) + 'static) -> Rc<dyn IDisposable> {
471        let token = self.scroll.add(Rc::new(handler));
472        let weak = self.to_ref().downgrade();
473        Disposable::create(move || {
474            if let Some(this) = weak.upgrade() {
475                this.scroll.remove(token);
476            }
477        })
478    }
479
480    /// Calculates and updates whether the scrollbar should be visible.
481    fn update_is_visible(&self) {
482        let is_visible = match self.visibility() {
483            ScrollBarVisibility::Visible => true,
484            ScrollBarVisibility::Disabled => false,
485            ScrollBarVisibility::Hidden => false,
486            ScrollBarVisibility::Auto => self.viewport_size().is_nan() || self.maximum() > 0.0,
487        };
488
489        self.set_current_value(Visual::is_visible_property(), is_visible);
490    }
491
492    fn owner(&self) -> Option<Ref<ScrollViewer>> {
493        self.owner.borrow().as_ref().and_then(WeakRef::upgrade)
494    }
495
496    fn dispose_owner_subscriptions(&self) {
497        let subscriptions = self.owner_subscriptions.borrow_mut().take();
498        if let Some(subscriptions) = subscriptions {
499            for subscription in subscriptions {
500                subscription.dispose();
501            }
502        }
503    }
504
505    /// Tries to attach to the templated parent if it is a [`ScrollViewer`]
506    /// and binds to its properties. Properties which have been set through
507    /// other means are not bound.
508    ///
509    /// This method is automatically called when the control is attached to
510    /// a visual tree.
511    pub(crate) fn attach_to_scroll_viewer(&self) {
512        let owner = self.templated_parent().and_then(|parent| parent.cast::<ScrollViewer>());
513
514        let Some(owner) = owner else {
515            *self.owner.borrow_mut() = None;
516            self.dispose_owner_subscriptions();
517            return;
518        };
519
520        if self.owner().as_ref() == Some(&owner) {
521            return;
522        }
523
524        self.dispose_owner_subscriptions();
525
526        let is_horizontal = self.orientation() == Orientation::Horizontal;
527
528        let weak = self.to_ref().downgrade();
529        let is_horizontal_now = move || {
530            weak.upgrade().map_or(is_horizontal, |this| this.orientation() == Orientation::Horizontal)
531        };
532        let extract_vector = {
533            let is_horizontal_now = is_horizontal_now.clone();
534            move |v: Vector| if is_horizontal_now() { v.x } else { v.y }
535        };
536        let extract_size = move |v: Size| if is_horizontal_now() { v.width } else { v.height };
537
538        let visibility_source = if is_horizontal {
539            ScrollViewer::horizontal_scroll_bar_visibility_property()
540        } else {
541            ScrollViewer::vertical_scroll_bar_visibility_property()
542        };
543
544        let priority = BindingPriority::Template;
545        let source: &FerroObject = &owner;
546        let mut subscriptions: Vec<Rc<dyn IDisposable>> = Vec::new();
547
548        if !self.is_set(RangeBase::maximum_property().as_property()) {
549            subscriptions.push(self.bind_typed(
550                RangeBase::maximum_property(),
551                FerroObjectExtensions::get_observable_with(source, ScrollViewer::scroll_bar_maximum_property(), extract_vector.clone()),
552                priority,
553            ));
554        }
555        if !self.is_set(RangeBase::value_property().as_property()) {
556            subscriptions.push(self.bind_typed(
557                RangeBase::value_property(),
558                FerroObjectExtensions::get_observable_with(source, ScrollViewer::offset_property(), extract_vector),
559                priority,
560            ));
561        }
562        if !self.is_set(ScrollViewer::is_deferred_scrolling_enabled_property().as_property()) {
563            subscriptions.push(self.bind_typed(
564                ScrollViewer::is_deferred_scrolling_enabled_property(),
565                FerroObjectExtensions::get_observable(source, ScrollViewer::is_deferred_scrolling_enabled_property()),
566                priority,
567            ));
568        }
569        if !self.is_set(Self::viewport_size_property().as_property()) {
570            subscriptions.push(self.bind_typed(
571                Self::viewport_size_property(),
572                FerroObjectExtensions::get_observable_with(source, ScrollViewer::viewport_property(), extract_size.clone()),
573                priority,
574            ));
575        }
576        if !self.is_set(Self::visibility_property().as_property()) {
577            subscriptions.push(self.bind_typed(
578                Self::visibility_property(),
579                FerroObjectExtensions::get_observable(source, visibility_source),
580                priority,
581            ));
582        }
583        if !self.is_set(Self::allow_auto_hide_property().as_property()) {
584            subscriptions.push(self.bind_typed(
585                Self::allow_auto_hide_property(),
586                FerroObjectExtensions::get_observable(source, ScrollViewer::allow_auto_hide_property()),
587                priority,
588            ));
589        }
590        if !self.is_set(RangeBase::large_change_property().as_property()) {
591            subscriptions.push(self.bind_typed(
592                RangeBase::large_change_property(),
593                FerroObjectExtensions::get_observable_with(source, ScrollViewer::large_change_property(), extract_size.clone()),
594                priority,
595            ));
596        }
597        if !self.is_set(RangeBase::small_change_property().as_property()) {
598            subscriptions.push(self.bind_typed(
599                RangeBase::small_change_property(),
600                FerroObjectExtensions::get_observable_with(source, ScrollViewer::small_change_property(), extract_size),
601                priority,
602            ));
603        }
604
605        *self.owner.borrow_mut() = Some(owner.downgrade());
606        *self.owner_subscriptions.borrow_mut() = Some(subscriptions);
607    }
608
609    fn invoke_after_delay(&self, handler: DelayedAction, delay: TimeSpan) {
610        let existing = self.timer.borrow().clone();
611        let timer = match existing {
612            Some(timer) => {
613                timer.stop();
614                timer
615            }
616            None => {
617                let timer = DispatcherTimer::with_priority(DispatcherPriority::NORMAL);
618                let weak = self.to_ref().downgrade();
619                // The subscription lives as long as the timer.
620                let _ = timer.tick(move |sender_timer| {
621                    if let Some(this) = weak.upgrade() {
622                        match this.timer_action.get() {
623                            Some(DelayedAction::Collapse) => this.collapse(),
624                            Some(DelayedAction::Expand) => this.expand(),
625                            None => {}
626                        }
627                    }
628
629                    sender_timer.stop();
630                });
631                *self.timer.borrow_mut() = Some(timer.clone());
632                timer
633            }
634        };
635
636        self.timer_action.set(Some(handler));
637        timer.set_interval(delay.to_duration().unwrap_or(Duration::ZERO));
638
639        timer.start();
640    }
641
642    fn update_is_expanded_state(&self) {
643        if !self.allow_auto_hide() {
644            let timer = self.timer.borrow().clone();
645            if let Some(timer) = timer {
646                timer.stop();
647            }
648
649            self.set_is_expanded(true);
650        } else {
651            self.set_is_expanded(self.is_pointer_over());
652        }
653    }
654
655    fn collapse_after_delay(&self) {
656        self.invoke_after_delay(DelayedAction::Collapse, self.hide_delay());
657    }
658
659    fn expand_after_delay(&self) {
660        self.invoke_after_delay(DelayedAction::Expand, self.show_delay());
661    }
662
663    fn collapse(&self) {
664        self.set_is_expanded(false);
665    }
666
667    fn expand(&self) {
668        self.set_is_expanded(true);
669    }
670
671    fn small_decrement(&self) {
672        self.set_current_value(
673            RangeBase::value_property(),
674            f64::max(self.value() - self.small_change(), self.minimum()),
675        );
676        self.on_scroll(ScrollEventType::SmallDecrement);
677    }
678
679    fn small_increment(&self) {
680        self.set_current_value(
681            RangeBase::value_property(),
682            f64::min(self.value() + self.small_change(), self.maximum()),
683        );
684        self.on_scroll(ScrollEventType::SmallIncrement);
685    }
686
687    fn large_decrement(&self) {
688        self.set_current_value(
689            RangeBase::value_property(),
690            f64::max(self.value() - self.large_change(), self.minimum()),
691        );
692        self.on_scroll(ScrollEventType::LargeDecrement);
693    }
694
695    fn large_increment(&self) {
696        self.set_current_value(
697            RangeBase::value_property(),
698            f64::min(self.value() + self.large_change(), self.maximum()),
699        );
700        self.on_scroll(ScrollEventType::LargeIncrement);
701    }
702
703    fn on_thumb_drag_delta(&self, _e: &VectorEventArgs) {
704        self.on_scroll(ScrollEventType::ThumbTrack);
705    }
706
707    fn on_thumb_drag_start(&self, _e: &VectorEventArgs) {
708        self.is_dragging.set(true);
709    }
710
711    fn on_thumb_drag_complete(&self, _e: &VectorEventArgs) {
712        self.is_dragging.set(false);
713
714        if self.allow_auto_hide() && !self.is_pointer_over() {
715            self.collapse_after_delay();
716        }
717
718        self.on_scroll(ScrollEventType::EndScroll);
719    }
720
721    /// Raises the scroll event.
722    pub fn on_scroll(&self, scroll_event_type: ScrollEventType) {
723        if self.scroll.is_empty() {
724            return;
725        }
726
727        let e = ScrollEventArgs::new(scroll_event_type, self.value());
728        for (_, handler) in self.scroll.snapshot().iter() {
729            handler(&e);
730        }
731    }
732
733    fn update_pseudo_classes(&self, o: Orientation) {
734        self.pseudo_classes().set(PC_VERTICAL, o == Orientation::Vertical);
735        self.pseudo_classes().set(PC_HORIZONTAL, o == Orientation::Horizontal);
736    }
737
738    fn on_context_requested(&self, e: &ContextRequestedEventArgs) {
739        if matches!(e.pointer_type(), Some(PointerType::Touch | PointerType::Pen)) {
740            e.set_handled(true);
741            return;
742        }
743
744        if let Some(position) = e.try_get_position(Some(self)) {
745            self.last_right_click_position.set(position);
746        }
747    }
748
749    /// Scrolls to the location at which the context menu was most recently
750    /// requested.
751    pub fn scroll_here(&self) {
752        let Some(track) = self.track() else {
753            return;
754        };
755
756        let is_vertical = self.orientation() == Orientation::Vertical;
757        let last_right_click_position = self.last_right_click_position.get();
758        let thumb_bounds = track.thumb().map(|thumb| thumb.bounds());
759
760        let track_length = if is_vertical { track.bounds().height } else { track.bounds().width };
761        let thumb_length =
762            thumb_bounds.map_or(0.0, |bounds| if is_vertical { bounds.height } else { bounds.width });
763        let click_position = if is_vertical { last_right_click_position.y } else { last_right_click_position.x };
764
765        if track_length > thumb_length {
766            let ratio = click_position / track_length;
767            let range = self.maximum() - self.minimum();
768            let value = self.minimum() + (ratio * range);
769            self.set_current_value(
770                RangeBase::value_property(),
771                f64::max(self.minimum(), f64::min(self.maximum(), value)),
772            );
773            self.on_scroll(ScrollEventType::ThumbTrack);
774        }
775    }
776
777    /// Scrolls to the top (or left edge) of the scrollbar by setting the
778    /// value to the minimum.
779    pub fn scroll_to_home(&self) {
780        self.set_current_value(RangeBase::value_property(), self.minimum());
781        self.on_scroll(ScrollEventType::LargeDecrement);
782    }
783
784    /// Scrolls to the bottom (or right edge) of the scrollbar by setting the
785    /// value to the maximum.
786    pub fn scroll_to_end(&self) {
787        self.set_current_value(RangeBase::value_property(), self.maximum());
788        self.on_scroll(ScrollEventType::LargeIncrement);
789    }
790
791    /// Scrolls up by one large change (a page) on a vertical scrollbar.
792    pub fn page_up(&self) {
793        self.large_decrement()
794    }
795
796    /// Scrolls down by one large change (a page) on a vertical scrollbar.
797    pub fn page_down(&self) {
798        self.large_increment()
799    }
800
801    /// Scrolls left by one large change (a page) on a horizontal scrollbar.
802    pub fn page_left(&self) {
803        self.large_decrement()
804    }
805
806    /// Scrolls right by one large change (a page) on a horizontal scrollbar.
807    pub fn page_right(&self) {
808        self.large_increment()
809    }
810
811    /// Scrolls up by one small change on a vertical scrollbar.
812    pub fn line_up(&self) {
813        self.small_decrement()
814    }
815
816    /// Scrolls down by one small change on a vertical scrollbar.
817    pub fn line_down(&self) {
818        self.small_increment()
819    }
820
821    /// Scrolls left by one small change on a horizontal scrollbar.
822    pub fn line_left(&self) {
823        self.small_decrement()
824    }
825
826    /// Scrolls right by one small change on a horizontal scrollbar.
827    pub fn line_right(&self) {
828        self.small_increment()
829    }
830
831    fn track(&self) -> Option<Ref<Track>> {
832        self.get_template_descendants().into_iter().find_map(|visual| visual.cast::<Track>())
833    }
834}