Skip to main content

ferroui_controls/primitives/
track.rs

1// Portions of this source file are adapted from the Windows Presentation Foundation project.
2// (https://github.com/dotnet/wpf/)
3
4use super::{RangeBase, ScrollBar, Thumb};
5use crate::metadata::PseudoClassesAttribute;
6use crate::{Button, Control, ControlImpl};
7use ferroui_base::input::{InputElementImpl, VectorEventArgs};
8use ferroui_base::interactivity::{InteractiveImpl, RoutedEventHandlerToken};
9use ferroui_base::layout::{Layoutable, LayoutableImpl, Orientation};
10use ferroui_base::utilities::MathUtilities;
11use ferroui_base::{
12    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
13    FerroPropertyChangedEventArgs, Nullable, Point, Rect, Ref, Size, StyledElement, StyledElementImpl, StyledProperty,
14    Vector, VisualImpl,
15};
16use std::cell::Cell;
17
18const PC_VERTICAL: &str = ":vertical";
19const PC_HORIZONTAL: &str = ":horizontal";
20
21/// A control that positions a [`Thumb`] and the buttons before and after it
22/// along a range: the building block of scroll bars and sliders.
23#[repr(C)]
24pub struct Track {
25    base: Control,
26    deferred_thumb_drag: Cell<Option<Vector>>,
27    last_drag: Cell<Vector>,
28    thumb_center_offset: Cell<f64>,
29    density: Cell<f64>,
30    thumb_drag_delta_token: Cell<Option<RoutedEventHandlerToken>>,
31    thumb_drag_completed_token: Cell<Option<RoutedEventHandlerToken>>,
32}
33
34ferro_class! {
35    Track: Control, virtuals TrackImpl: ControlImpl {
36        /// Calculates the distance along the thumb of a specified point
37        /// along the track.
38        ///
39        /// Returns the distance between the thumb and the specified point
40        /// value.
41        fn value_from_point(this, point: Point) -> f64;
42        /// Calculates the change in the value of the track when the thumb
43        /// moves by the given horizontal and vertical displacement.
44        fn value_from_distance(this, horizontal: f64, vertical: f64) -> f64;
45    }
46}
47ferroui_base::ferro_class_info!(Track { new: Track::new });
48
49ferro_impl_classes!(Track: StyledElementImpl, VisualImpl, InteractiveImpl, InputElementImpl, ControlImpl);
50
51impl FerroObjectImpl for Track {
52    fn constructed(this: &Self) {
53        Self::parent_constructed(this);
54        this.update_pseudo_classes(this.orientation());
55    }
56
57    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
58        Self::parent_on_property_changed(this, change);
59
60        if change.property() == Self::orientation_property().as_property() {
61            this.update_pseudo_classes(change.get_new_value::<Orientation>());
62        } else if change.property() == Self::defer_thumb_drag_property().as_property()
63            && !change.get_new_value::<bool>()
64        {
65            this.apply_deferred_thumb_drag();
66        }
67    }
68}
69
70impl LayoutableImpl for Track {
71    fn measure_override(this: &Self, available_size: Size) -> Size {
72        let mut desired_size = Size::new(0.0, 0.0);
73
74        // Only measure the thumb. The repeat buttons will be sized based on
75        // the thumb.
76        if let Some(thumb) = this.thumb() {
77            thumb.measure(available_size);
78            desired_size = thumb.desired_size();
79        }
80
81        if !this.viewport_size().is_nan() {
82            // A scroll bar can shrink to 0 in the direction of scrolling.
83            if this.orientation() == Orientation::Vertical {
84                desired_size = desired_size.with_height(0.0);
85            } else {
86                desired_size = desired_size.with_width(0.0);
87            }
88        }
89
90        desired_size
91    }
92
93    fn arrange_override(this: &Self, arrange_size: Size) -> Size {
94        let is_vertical = this.orientation() == Orientation::Vertical;
95        let viewport_size = f64::max(0.0, this.viewport_size());
96
97        // If the viewport is NaN, compute the thumb's size based on its
98        // desired size, otherwise compute the thumb based on the viewport
99        // and extent properties.
100        let (mut decrease_button_length, mut thumb_length, mut increase_button_length) =
101            if this.viewport_size().is_nan() {
102                this.compute_slider_lengths(arrange_size, is_vertical)
103            } else {
104                // Don't arrange if there's not enough content or the track
105                // is too small.
106                match this.compute_scroll_bar_lengths(arrange_size, viewport_size, is_vertical) {
107                    Some(lengths) => lengths,
108                    None => return arrange_size,
109                }
110            };
111
112        // Layout the pieces of the track.
113        let mut offset = Point::default();
114        let mut piece_size = arrange_size;
115        let is_direction_reversed = this.is_direction_reversed();
116
117        if is_vertical {
118            Self::coerce_length(&mut decrease_button_length, arrange_size.height);
119            Self::coerce_length(&mut increase_button_length, arrange_size.height);
120            Self::coerce_length(&mut thumb_length, arrange_size.height);
121
122            offset = offset.with_y(if is_direction_reversed { decrease_button_length + thumb_length } else { 0.0 });
123            piece_size = piece_size.with_height(increase_button_length);
124
125            if let Some(increase_button) = this.increase_button() {
126                increase_button.arrange(Rect::from_position_size(offset, piece_size));
127            }
128
129            offset = offset.with_y(if is_direction_reversed { 0.0 } else { increase_button_length + thumb_length });
130            piece_size = piece_size.with_height(decrease_button_length);
131
132            if let Some(decrease_button) = this.decrease_button() {
133                decrease_button.arrange(Rect::from_position_size(offset, piece_size));
134            }
135
136            offset = offset.with_y(if is_direction_reversed { decrease_button_length } else { increase_button_length });
137            piece_size = piece_size.with_height(thumb_length);
138
139            if let Some(thumb) = this.thumb() {
140                let bounds = Rect::from_position_size(offset, piece_size);
141                let adjust = this.calculate_thumb_adjustment(&thumb, bounds);
142                thumb.arrange(bounds);
143                thumb.adjust_drag(adjust);
144            }
145
146            this.thumb_center_offset.set(offset.y + (thumb_length * 0.5));
147        } else {
148            Self::coerce_length(&mut decrease_button_length, arrange_size.width);
149            Self::coerce_length(&mut increase_button_length, arrange_size.width);
150            Self::coerce_length(&mut thumb_length, arrange_size.width);
151
152            offset = offset.with_x(if is_direction_reversed { increase_button_length + thumb_length } else { 0.0 });
153            piece_size = piece_size.with_width(decrease_button_length);
154
155            if let Some(decrease_button) = this.decrease_button() {
156                decrease_button.arrange(Rect::from_position_size(offset, piece_size));
157            }
158
159            offset = offset.with_x(if is_direction_reversed { 0.0 } else { decrease_button_length + thumb_length });
160            piece_size = piece_size.with_width(increase_button_length);
161
162            if let Some(increase_button) = this.increase_button() {
163                increase_button.arrange(Rect::from_position_size(offset, piece_size));
164            }
165
166            offset = offset.with_x(if is_direction_reversed { increase_button_length } else { decrease_button_length });
167            piece_size = piece_size.with_width(thumb_length);
168
169            if let Some(thumb) = this.thumb() {
170                let bounds = Rect::from_position_size(offset, piece_size);
171                let adjust = this.calculate_thumb_adjustment(&thumb, bounds);
172                thumb.arrange(bounds);
173                thumb.adjust_drag(adjust);
174            }
175
176            this.thumb_center_offset.set(offset.x + (thumb_length * 0.5));
177        }
178
179        this.last_drag.set(Vector::default());
180        arrange_size
181    }
182}
183
184impl TrackImpl for Track {
185    fn value_from_point(this: &Self, point: Point) -> f64 {
186        // Find the distance from the center of the thumb to the given point.
187        let val = if this.orientation() == Orientation::Horizontal {
188            this.thumb_value()
189                + this.value_from_distance(
190                    point.x - this.thumb_center_offset.get(),
191                    point.y - (this.bounds().height * 0.5),
192                )
193        } else {
194            this.thumb_value()
195                + this.value_from_distance(
196                    point.x - (this.bounds().width * 0.5),
197                    point.y - this.thumb_center_offset.get(),
198                )
199        };
200
201        f64::max(this.minimum(), f64::min(this.maximum(), val))
202    }
203
204    fn value_from_distance(this: &Self, horizontal: f64, vertical: f64) -> f64 {
205        let scale = if this.is_direction_reversed() { -1.0 } else { 1.0 };
206
207        if this.orientation() == Orientation::Horizontal {
208            scale * horizontal * this.density.get()
209        } else {
210            // Increases in y cause decreases in the slider's value.
211            -1.0 * scale * vertical * this.density.get()
212        }
213    }
214}
215
216impl Track {
217    /// The pseudoclasses set by the class.
218    pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_VERTICAL, PC_HORIZONTAL]);
219}
220
221ferroui_base::ferro_properties! { impl Track {
222    ferro_property!(
223        /// Defines the `Minimum` property.
224        pub fn minimum_property() -> StyledProperty<f64> {
225            RangeBase::minimum_property().add_owner::<Track>()
226        }
227    );
228
229    ferro_property!(
230        /// Defines the `Maximum` property.
231        pub fn maximum_property() -> StyledProperty<f64> {
232            RangeBase::maximum_property().add_owner::<Track>()
233        }
234    );
235
236    ferro_property!(
237        /// Defines the `Value` property.
238        pub fn value_property() -> StyledProperty<f64> {
239            RangeBase::value_property().add_owner::<Track>()
240        }
241    );
242
243    ferro_property!(
244        /// Defines the `ViewportSize` property.
245        pub fn viewport_size_property() -> StyledProperty<f64> {
246            ScrollBar::viewport_size_property().add_owner::<Track>()
247        }
248    );
249
250    ferro_property!(
251        /// Defines the `Orientation` property.
252        pub fn orientation_property() -> StyledProperty<Orientation> {
253            ScrollBar::orientation_property().add_owner::<Track>()
254        }
255    );
256
257    ferro_property!(
258        /// Defines the `Thumb` property.
259        pub fn thumb_property() -> StyledProperty<Option<Ref<Thumb>>> {
260            FerroProperty::register::<Track, _>("Thumb", None)
261        }
262    );
263
264    ferro_property!(
265        /// Defines the `IncreaseButton` property.
266        pub fn increase_button_property() -> StyledProperty<Option<Ref<Button>>> {
267            FerroProperty::register::<Track, _>("IncreaseButton", None)
268        }
269    );
270
271    ferro_property!(
272        /// Defines the `DecreaseButton` property.
273        pub fn decrease_button_property() -> StyledProperty<Option<Ref<Button>>> {
274            FerroProperty::register::<Track, _>("DecreaseButton", None)
275        }
276    );
277
278    ferro_property!(
279        /// Defines the `IsDirectionReversed` property.
280        pub fn is_direction_reversed_property() -> StyledProperty<bool> {
281            FerroProperty::register::<Track, _>("IsDirectionReversed", false)
282        }
283    );
284
285    ferro_property!(
286        /// Defines the `IgnoreThumbDrag` property.
287        pub fn ignore_thumb_drag_property() -> StyledProperty<bool> {
288            FerroProperty::register::<Track, _>("IgnoreThumbDrag", false)
289        }
290    );
291
292    ferro_property!(
293        /// Defines the `DeferThumbDrag` property.
294        pub fn defer_thumb_drag_property() -> StyledProperty<bool> {
295            FerroProperty::register::<Track, _>("DeferThumbDrag", false)
296        }
297    );
298} }
299
300impl Track {
301    fn static_constructor() {
302        Self::thumb_property().changed().add_class_handler::<Track>(|x, e| x.thumb_changed(e));
303        Self::increase_button_property().changed().add_class_handler::<Track>(|x, e| x.button_changed(e));
304        Self::decrease_button_property().changed().add_class_handler::<Track>(|x, e| x.button_changed(e));
305        Layoutable::affects_arrange::<Track>(&[
306            Self::is_direction_reversed_property().as_property(),
307            Self::minimum_property().as_property(),
308            Self::maximum_property().as_property(),
309            Self::value_property().as_property(),
310            Self::orientation_property().as_property(),
311        ]);
312    }
313
314    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
315    pub fn construct() -> Self {
316        Self {
317            base: Control::construct(),
318            deferred_thumb_drag: Cell::new(None),
319            last_drag: Cell::new(Vector::default()),
320            thumb_center_offset: Cell::new(0.0),
321            density: Cell::new(0.0),
322            thumb_drag_delta_token: Cell::new(None),
323            thumb_drag_completed_token: Cell::new(None),
324        }
325    }
326
327    pub fn new() -> Ref<Self> {
328        instantiate(Self::construct())
329    }
330
331    /// The minimum possible value.
332    pub fn minimum(&self) -> f64 {
333        self.get_value(Self::minimum_property())
334    }
335
336    pub fn set_minimum(&self, value: f64) {
337        self.set_value(Self::minimum_property(), value)
338    }
339
340    /// The maximum possible value.
341    pub fn maximum(&self) -> f64 {
342        self.get_value(Self::maximum_property())
343    }
344
345    pub fn set_maximum(&self, value: f64) {
346        self.set_value(Self::maximum_property(), value)
347    }
348
349    /// The current value.
350    pub fn value(&self) -> f64 {
351        self.get_value(Self::value_property())
352    }
353
354    pub fn set_range_value(&self, value: f64) {
355        self.set_value(Self::value_property(), value)
356    }
357
358    /// The value of the thumb's current position. This can differ from the
359    /// value while a thumb drag is deferred.
360    fn thumb_value(&self) -> f64 {
361        self.value()
362            + match self.deferred_thumb_drag.get() {
363                None => 0.0,
364                Some(vector) => self.value_from_distance(vector.x, vector.y),
365            }
366    }
367
368    /// The amount of the scrollable content that is currently visible.
369    pub fn viewport_size(&self) -> f64 {
370        self.get_value(Self::viewport_size_property())
371    }
372
373    pub fn set_viewport_size(&self, value: f64) {
374        self.set_value(Self::viewport_size_property(), value)
375    }
376
377    /// The orientation of the track.
378    pub fn orientation(&self) -> Orientation {
379        self.get_value(Self::orientation_property())
380    }
381
382    pub fn set_orientation(&self, value: Orientation) {
383        self.set_value(Self::orientation_property(), value)
384    }
385
386    /// The thumb of the track: its content.
387    pub fn thumb(&self) -> Option<Ref<Thumb>> {
388        self.get_value(Self::thumb_property())
389    }
390
391    pub fn set_thumb(&self, value: impl Into<Nullable<Thumb>>) {
392        self.set_value(Self::thumb_property(), value.into().0)
393    }
394
395    /// The button that increases the value.
396    pub fn increase_button(&self) -> Option<Ref<Button>> {
397        self.get_value(Self::increase_button_property())
398    }
399
400    pub fn set_increase_button(&self, value: impl Into<Nullable<Button>>) {
401        self.set_value(Self::increase_button_property(), value.into().0)
402    }
403
404    /// The button that decreases the value.
405    pub fn decrease_button(&self) -> Option<Ref<Button>> {
406        self.get_value(Self::decrease_button_property())
407    }
408
409    pub fn set_decrease_button(&self, value: impl Into<Nullable<Button>>) {
410        self.set_value(Self::decrease_button_property(), value.into().0)
411    }
412
413    /// Whether the direction of increasing value is reversed.
414    pub fn is_direction_reversed(&self) -> bool {
415        self.get_value(Self::is_direction_reversed_property())
416    }
417
418    pub fn set_is_direction_reversed(&self, value: bool) {
419        self.set_value(Self::is_direction_reversed_property(), value)
420    }
421
422    /// Whether drags of the thumb are ignored.
423    pub fn ignore_thumb_drag(&self) -> bool {
424        self.get_value(Self::ignore_thumb_drag_property())
425    }
426
427    pub fn set_ignore_thumb_drag(&self, value: bool) {
428        self.set_value(Self::ignore_thumb_drag_property(), value)
429    }
430
431    /// Whether the value is only updated when a drag of the thumb completes.
432    pub fn defer_thumb_drag(&self) -> bool {
433        self.get_value(Self::defer_thumb_drag_property())
434    }
435
436    pub fn set_defer_thumb_drag(&self, value: bool) {
437        self.set_value(Self::defer_thumb_drag_property(), value)
438    }
439
440    fn calculate_thumb_adjustment(&self, thumb: &Thumb, new_thumb_bounds: Rect) -> Vector {
441        let thumb_delta: Vector = (new_thumb_bounds.position() - thumb.bounds().position()).into();
442        self.last_drag.get() - thumb_delta
443    }
444
445    fn coerce_length(component_length: &mut f64, track_length: f64) {
446        if *component_length < 0.0 {
447            *component_length = 0.0;
448        } else if *component_length > track_length || component_length.is_nan() {
449            *component_length = track_length;
450        }
451    }
452
453    /// Returns the lengths of the decrease button, the thumb and the
454    /// increase button.
455    fn compute_slider_lengths(&self, arrange_size: Size, is_vertical: bool) -> (f64, f64, f64) {
456        let min = self.minimum();
457        let range = f64::max(0.0, self.maximum() - min);
458        let offset = f64::min(range, self.thumb_value() - min);
459
460        let thumb = self.thumb();
461
462        // Compute the thumb size.
463        let (track_length, mut thumb_length) = if is_vertical {
464            (arrange_size.height, thumb.map_or(0.0, |thumb| thumb.desired_size().height))
465        } else {
466            (arrange_size.width, thumb.map_or(0.0, |thumb| thumb.desired_size().width))
467        };
468
469        Self::coerce_length(&mut thumb_length, track_length);
470
471        let remaining_track_length = track_length - thumb_length;
472
473        let mut decrease_button_length = remaining_track_length * offset / range;
474        Self::coerce_length(&mut decrease_button_length, remaining_track_length);
475
476        let mut increase_button_length = remaining_track_length - decrease_button_length;
477        Self::coerce_length(&mut increase_button_length, remaining_track_length);
478
479        self.density.set(range / remaining_track_length);
480
481        (decrease_button_length, thumb_length, increase_button_length)
482    }
483
484    /// Returns the lengths of the decrease button, the thumb and the
485    /// increase button, or `None` when the pieces must not be arranged.
486    fn compute_scroll_bar_lengths(
487        &self,
488        arrange_size: Size,
489        viewport_size: f64,
490        is_vertical: bool,
491    ) -> Option<(f64, f64, f64)> {
492        let min = self.minimum();
493        let range = f64::max(0.0, self.maximum() - min);
494        let offset = f64::min(range, self.thumb_value() - min);
495        let extent = f64::max(0.0, range) + viewport_size;
496        let track_length = if is_vertical { arrange_size.height } else { arrange_size.width };
497        let mut thumb_min_length = 10.0;
498
499        let min_length_property =
500            if is_vertical { Layoutable::min_height_property() } else { Layoutable::min_width_property() };
501
502        if let Some(thumb) = self.thumb() {
503            if thumb.is_set(min_length_property.as_property()) {
504                thumb_min_length = thumb.get_value(min_length_property);
505            }
506        }
507
508        let mut thumb_length = track_length * viewport_size / extent;
509        Self::coerce_length(&mut thumb_length, track_length);
510        thumb_length = f64::max(thumb_min_length, thumb_length);
511
512        // If we don't have enough content to scroll, disable the track.
513        let not_enough_content_to_scroll = MathUtilities::less_than_or_close(range, 0.0);
514        let thumb_longer_than_track = thumb_length > track_length;
515
516        // If there's not enough content or the thumb is longer than the
517        // track, hide the track and don't arrange the pieces.
518        if not_enough_content_to_scroll || thumb_longer_than_track {
519            self.show_children(false);
520            self.thumb_center_offset.set(f64::NAN);
521            self.density.set(f64::NAN);
522            return None; // don't arrange
523        }
524
525        self.show_children(true);
526
527        // Compute the lengths of the increase and decrease buttons.
528        let remaining_track_length = track_length - thumb_length;
529        let mut decrease_button_length = remaining_track_length * offset / range;
530        Self::coerce_length(&mut decrease_button_length, remaining_track_length);
531
532        let mut increase_button_length = remaining_track_length - decrease_button_length;
533        Self::coerce_length(&mut increase_button_length, remaining_track_length);
534
535        self.density.set(range / remaining_track_length);
536
537        Some((decrease_button_length, thumb_length, increase_button_length))
538    }
539
540    fn thumb_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
541        let (old_thumb, new_thumb) = e.get_old_and_new_value::<Option<Ref<Thumb>>>();
542
543        if let Some(old_thumb) = old_thumb {
544            if let Some(token) = self.thumb_drag_delta_token.take() {
545                old_thumb.remove_handler(Thumb::drag_delta_event(), token);
546            }
547            if let Some(token) = self.thumb_drag_completed_token.take() {
548                old_thumb.remove_handler(Thumb::drag_completed_event(), token);
549            }
550            StyledElement::logical_children(self).remove(&old_thumb.clone().upcast());
551            self.visual_children().remove(&old_thumb.upcast());
552        }
553
554        if let Some(new_thumb) = new_thumb {
555            let weak = self.to_ref().downgrade();
556            self.thumb_drag_delta_token.set(Some(new_thumb.drag_delta(move |_, e| {
557                if let Some(this) = weak.upgrade() {
558                    this.thumb_dragged(e);
559                }
560            })));
561            let weak = self.to_ref().downgrade();
562            self.thumb_drag_completed_token.set(Some(new_thumb.drag_completed(move |_, _| {
563                if let Some(this) = weak.upgrade() {
564                    this.apply_deferred_thumb_drag();
565                }
566            })));
567            StyledElement::logical_children(self).add(new_thumb.clone().upcast());
568            self.visual_children().add(new_thumb.upcast());
569        }
570    }
571
572    fn button_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
573        let (old_button, new_button) = e.get_old_and_new_value::<Option<Ref<Button>>>();
574
575        if let Some(old_button) = old_button {
576            StyledElement::logical_children(self).remove(&old_button.clone().upcast());
577            self.visual_children().remove(&old_button.upcast());
578        }
579
580        if let Some(new_button) = new_button {
581            StyledElement::logical_children(self).add(new_button.clone().upcast());
582            self.visual_children().add(new_button.upcast());
583        }
584    }
585
586    fn thumb_dragged(&self, e: &VectorEventArgs) {
587        if self.ignore_thumb_drag() {
588            return;
589        }
590
591        if self.defer_thumb_drag() {
592            self.deferred_thumb_drag.set(Some(e.vector));
593            self.invalidate_arrange();
594        } else {
595            self.apply_thumb_drag(e.vector);
596        }
597    }
598
599    fn apply_thumb_drag(&self, vector: Vector) {
600        let delta = self.value_from_distance(vector.x, vector.y);
601        let factor = vector / delta;
602        let old_value = self.value();
603
604        self.set_current_value(
605            Self::value_property(),
606            MathUtilities::clamp(self.value() + delta, self.minimum(), self.maximum()),
607        );
608
609        // Record the part of the drag that actually had effect as the last
610        // drag delta. Due to clamping, we need to compare the two values
611        // instead of using the drag delta.
612        self.last_drag.set(factor * (self.value() - old_value));
613    }
614
615    fn apply_deferred_thumb_drag(&self) {
616        if let Some(vector) = self.deferred_thumb_drag.take() {
617            self.apply_thumb_drag(vector);
618        }
619    }
620
621    fn show_children(&self, visible: bool) {
622        // Setting IsVisible = false on the track would cause it to stop
623        // being laid out. Instead show/hide the child controls.
624        if let Some(thumb) = self.thumb() {
625            thumb.set_is_visible(visible);
626        }
627
628        if let Some(increase_button) = self.increase_button() {
629            increase_button.set_is_visible(visible);
630        }
631
632        if let Some(decrease_button) = self.decrease_button() {
633            decrease_button.set_is_visible(visible);
634        }
635    }
636
637    fn update_pseudo_classes(&self, o: Orientation) {
638        self.pseudo_classes().set(PC_VERTICAL, o == Orientation::Vertical);
639        self.pseudo_classes().set(PC_HORIZONTAL, o == Orientation::Horizontal);
640    }
641}