Skip to main content

ferroui_controls/
slider.rs

1use crate::metadata::{PseudoClassesAttribute, TemplatePartAttribute};
2use crate::primitives::{
3    RangeBase, ScrollBar, TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt, Thumb, Track,
4};
5use crate::{Button, ControlImpl, TickBar, TickList};
6use ferroui_base::input::navigation::XYFocusHelpers;
7use ferroui_base::input::{
8    InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, PointerEventArgs, PointerPoint,
9    PointerPressedEventArgs, PointerReleasedEventArgs, VectorEventArgs,
10};
11use ferroui_base::interactivity::{InteractiveImpl, RoutingStrategies};
12use ferroui_base::layout::{LayoutableImpl, Orientation};
13use ferroui_base::reactive::IDisposable;
14use ferroui_base::utilities::MathUtilities;
15use ferroui_base::{
16    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
17    FerroPropertyChangedEventArgs, Ref, StaticType, StyledElementImpl, StyledProperty, StyledPropertyMetadata, Visual,
18    VisualImpl,
19};
20use std::cell::{Cell, RefCell};
21use std::rc::Rc;
22
23const PC_VERTICAL: &str = ":vertical";
24const PC_HORIZONTAL: &str = ":horizontal";
25const PC_PRESSED: &str = ":pressed";
26
27const TOLERANCE: f64 = 0.0001;
28
29/// Describes how to position the ticks in a [`Slider`].
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31#[repr(i32)]
32pub enum TickPlacement {
33    /// No tick marks will appear.
34    None = 0,
35    /// Tick marks will appear above the track for a horizontal slider, or to
36    /// the left of the track for a vertical slider.
37    TopLeft = 1,
38    /// Tick marks will appear below the track for a horizontal slider, or to
39    /// the right of the track for a vertical slider.
40    BottomRight = 2,
41    /// Tick marks appear on both sides of either a horizontal or vertical
42    /// slider.
43    Outside = 3,
44}
45
46/// A control that lets the user select from a range of values by moving a
47/// thumb control along a track.
48#[repr(C)]
49pub struct Slider {
50    base: RangeBase,
51    // Slider required parts
52    is_dragging: Cell<bool>,
53    is_focus_engaged: Cell<bool>,
54    track: RefCell<Option<Ref<Track>>>,
55    decrease_button: RefCell<Option<Ref<Button>>>,
56    increase_button: RefCell<Option<Ref<Button>>>,
57    decrease_button_press_dispose: RefCell<Option<Rc<dyn IDisposable>>>,
58    decrease_button_release_dispose: RefCell<Option<Rc<dyn IDisposable>>>,
59    increase_button_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
60    increase_button_release_dispose: RefCell<Option<Rc<dyn IDisposable>>>,
61    pointer_moved_dispose: RefCell<Option<Rc<dyn IDisposable>>>,
62}
63
64ferro_class! {
65    Slider: RangeBase, virtuals SliderImpl: TemplatedControlImpl {
66        /// Called when the user starts dragging the thumb.
67        fn on_thumb_drag_started(this, e: &VectorEventArgs);
68        /// Called when the user stops dragging the thumb.
69        fn on_thumb_drag_completed(this, e: &VectorEventArgs);
70    }
71}
72ferroui_base::ferro_class_info!(Slider { new: Slider::new });
73
74ferro_impl_classes!(Slider: StyledElementImpl, VisualImpl, LayoutableImpl, InteractiveImpl);
75
76impl ControlImpl for Slider {
77    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
78        crate::automation::peers::SliderAutomationPeer::new(this).upcast()
79    }
80}
81
82impl FerroObjectImpl for Slider {
83    fn constructed(this: &Self) {
84        Self::parent_constructed(this);
85        this.update_pseudo_classes(this.orientation());
86    }
87
88    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
89        Self::parent_on_property_changed(this, change);
90
91        if change.property() == Self::orientation_property().as_property() {
92            this.update_pseudo_classes(change.get_new_value::<Orientation>());
93        }
94    }
95}
96
97impl TemplatedControlImpl for Slider {
98    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
99        Self::parent_on_apply_template(this, e);
100
101        for slot in [
102            &this.decrease_button_press_dispose,
103            &this.decrease_button_release_dispose,
104            &this.increase_button_subscription,
105            &this.increase_button_release_dispose,
106            &this.pointer_moved_dispose,
107        ] {
108            let previous = slot.borrow_mut().take();
109            if let Some(previous) = previous {
110                previous.dispose();
111            }
112        }
113
114        let track = e.name_scope().find_as::<Track>("PART_Track");
115        *this.track.borrow_mut() = track.clone();
116
117        let weak = this.to_ref().downgrade();
118
119        if let Some(track) = track {
120            track.set_ignore_thumb_drag(true);
121
122            let decrease_button = e.name_scope().find_as::<Button>("PART_DecreaseButton");
123            let increase_button = e.name_scope().find_as::<Button>("PART_IncreaseButton");
124            *this.decrease_button.borrow_mut() = decrease_button.clone();
125            *this.increase_button.borrow_mut() = increase_button.clone();
126
127            let subscribe = |button: &Button| {
128                let pressed_weak = weak.clone();
129                let press = button.add_disposable_handler_with(
130                    InputElement::pointer_pressed_event(),
131                    move |_, e: &PointerPressedEventArgs| {
132                        if let Some(this) = pressed_weak.upgrade() {
133                            this.track_pressed(e);
134                        }
135                    },
136                    RoutingStrategies::TUNNEL,
137                    false,
138                );
139                let released_weak = weak.clone();
140                let release = button.add_disposable_handler_with(
141                    InputElement::pointer_released_event(),
142                    move |_, e: &PointerReleasedEventArgs| {
143                        if let Some(this) = released_weak.upgrade() {
144                            this.track_released(e);
145                        }
146                    },
147                    RoutingStrategies::TUNNEL,
148                    false,
149                );
150                (press, release)
151            };
152
153            if let Some(decrease_button) = &decrease_button {
154                let (press, release) = subscribe(decrease_button);
155                *this.decrease_button_press_dispose.borrow_mut() = Some(press);
156                *this.decrease_button_release_dispose.borrow_mut() = Some(release);
157            }
158
159            if let Some(increase_button) = &increase_button {
160                let (press, release) = subscribe(increase_button);
161                *this.increase_button_subscription.borrow_mut() = Some(press);
162                *this.increase_button_release_dispose.borrow_mut() = Some(release);
163            }
164        }
165
166        let pointer_moved = this.add_disposable_handler_with(
167            InputElement::pointer_moved_event(),
168            move |_, e: &PointerEventArgs| {
169                if let Some(this) = weak.upgrade() {
170                    this.track_moved(e);
171                }
172            },
173            RoutingStrategies::TUNNEL,
174            false,
175        );
176        *this.pointer_moved_dispose.borrow_mut() = Some(pointer_moved);
177    }
178}
179
180impl InputElementImpl for Slider {
181    fn on_key_down(this: &Self, e: &KeyEventArgs) {
182        Self::parent_on_key_down(this, e);
183
184        if e.handled() || !e.key_modifiers.is_empty() {
185            return;
186        }
187
188        let using_xy_navigation = XYFocusHelpers::is_allowed_xy_navigation_mode(this, Some(e.key_device_type));
189        let allow_arrow_keys = this.is_focus_engaged.get() || !using_xy_navigation;
190
191        let mut handled = true;
192
193        match e.key {
194            Key::Enter if using_xy_navigation => {
195                this.is_focus_engaged.set(!this.is_focus_engaged.get());
196                handled = true;
197            }
198            Key::Escape if using_xy_navigation => {
199                this.is_focus_engaged.set(false);
200                handled = true;
201            }
202            Key::Down | Key::Left if allow_arrow_keys => {
203                this.move_to_next_tick(if this.is_direction_reversed() {
204                    this.small_change()
205                } else {
206                    -this.small_change()
207                });
208            }
209            Key::Up | Key::Right if allow_arrow_keys => {
210                this.move_to_next_tick(if this.is_direction_reversed() {
211                    -this.small_change()
212                } else {
213                    this.small_change()
214                });
215            }
216            Key::PageUp => {
217                this.move_to_next_tick(if this.is_direction_reversed() {
218                    -this.large_change()
219                } else {
220                    this.large_change()
221                });
222            }
223            Key::PageDown => {
224                this.move_to_next_tick(if this.is_direction_reversed() {
225                    this.large_change()
226                } else {
227                    -this.large_change()
228                });
229            }
230            Key::Home => {
231                this.set_current_value(RangeBase::value_property(), this.minimum());
232            }
233            Key::End => {
234                this.set_current_value(RangeBase::value_property(), this.maximum());
235            }
236            _ => {
237                handled = false;
238            }
239        }
240
241        e.set_handled(handled);
242    }
243}
244
245impl SliderImpl for Slider {
246    fn on_thumb_drag_started(this: &Self, _e: &VectorEventArgs) {
247        this.is_dragging.set(true);
248    }
249
250    fn on_thumb_drag_completed(this: &Self, _e: &VectorEventArgs) {
251        this.is_dragging.set(false);
252    }
253}
254
255impl Slider {
256    /// The named parts expected in the control template.
257    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] = &[
258        TemplatePartAttribute::new("PART_DecreaseButton", <Button as StaticType>::TYPE),
259        TemplatePartAttribute::new("PART_IncreaseButton", <Button as StaticType>::TYPE),
260        TemplatePartAttribute::new("PART_Track", <Track as StaticType>::TYPE),
261    ];
262
263    /// The pseudoclasses set by the class.
264    pub const PSEUDO_CLASSES: PseudoClassesAttribute =
265        PseudoClassesAttribute::new(&[PC_VERTICAL, PC_HORIZONTAL, PC_PRESSED]);
266}
267
268ferroui_base::ferro_properties! { impl Slider {
269    ferro_property!(
270        /// Defines the `Orientation` property.
271        pub fn orientation_property() -> StyledProperty<Orientation> {
272            ScrollBar::orientation_property().add_owner::<Slider>()
273        }
274    );
275
276    ferro_property!(
277        /// Defines the `IsDirectionReversed` property.
278        pub fn is_direction_reversed_property() -> StyledProperty<bool> {
279            Track::is_direction_reversed_property().add_owner::<Slider>()
280        }
281    );
282
283    ferro_property!(
284        /// Defines the `IsSnapToTickEnabled` property.
285        pub fn is_snap_to_tick_enabled_property() -> StyledProperty<bool> {
286            FerroProperty::register::<Slider, _>("IsSnapToTickEnabled", false)
287        }
288    );
289
290    ferro_property!(
291        /// Defines the `TickFrequency` property.
292        pub fn tick_frequency_property() -> StyledProperty<f64> {
293            FerroProperty::register::<Slider, _>("TickFrequency", 0.0)
294        }
295    );
296
297    ferro_property!(
298        /// Defines the `TickPlacement` property.
299        pub fn tick_placement_property() -> StyledProperty<TickPlacement> {
300            FerroProperty::register::<Slider, _>("TickPlacement", TickPlacement::None)
301        }
302    );
303
304    ferro_property!(
305        /// Defines the `Ticks` property.
306        pub fn ticks_property() -> StyledProperty<Option<TickList>> {
307            TickBar::ticks_property().add_owner::<Slider>()
308        }
309    );
310} }
311
312impl Slider {
313    fn static_constructor() {
314        crate::mixins::PressedMixin::attach::<Slider>();
315
316        InputElement::focusable_property().override_default_value::<Slider>(true);
317        Self::orientation_property().override_default_value::<Slider>(Orientation::Horizontal);
318        Thumb::drag_started_event().add_class_handler_with::<Slider>(
319            |x, e| x.on_thumb_drag_started(e),
320            RoutingStrategies::BUBBLE,
321            false,
322        );
323        Thumb::drag_completed_event().add_class_handler_with::<Slider>(
324            |x, e| x.on_thumb_drag_completed(e),
325            RoutingStrategies::BUBBLE,
326            false,
327        );
328
329        RangeBase::value_property()
330            .override_metadata::<Slider>(StyledPropertyMetadata::new(None).with_enable_data_validation(true));
331        crate::automation::AutomationProperties::control_type_override_property()
332            .override_default_value::<Slider>(Some(crate::automation::peers::AutomationControlType::Slider));
333    }
334
335    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
336    pub fn construct() -> Self {
337        Self {
338            base: RangeBase::construct(),
339            is_dragging: Cell::new(false),
340            is_focus_engaged: Cell::new(false),
341            track: RefCell::new(None),
342            decrease_button: RefCell::new(None),
343            increase_button: RefCell::new(None),
344            decrease_button_press_dispose: RefCell::new(None),
345            decrease_button_release_dispose: RefCell::new(None),
346            increase_button_subscription: RefCell::new(None),
347            increase_button_release_dispose: RefCell::new(None),
348            pointer_moved_dispose: RefCell::new(None),
349        }
350    }
351
352    pub fn new() -> Ref<Self> {
353        instantiate(Self::construct())
354    }
355
356    /// Defines the ticks to be drawn on the tick bar.
357    pub fn ticks(&self) -> Option<TickList> {
358        self.get_value(Self::ticks_property())
359    }
360
361    pub fn set_ticks(&self, value: Option<TickList>) {
362        self.set_value(Self::ticks_property(), value)
363    }
364
365    /// The orientation of the slider.
366    pub fn orientation(&self) -> Orientation {
367        self.get_value(Self::orientation_property())
368    }
369
370    pub fn set_orientation(&self, value: Orientation) {
371        self.set_value(Self::orientation_property(), value)
372    }
373
374    /// The direction of increasing value.
375    ///
376    /// True if the direction of increasing value is to the left for a
377    /// horizontal slider or down for a vertical slider; otherwise, false.
378    /// The default is false.
379    pub fn is_direction_reversed(&self) -> bool {
380        self.get_value(Self::is_direction_reversed_property())
381    }
382
383    pub fn set_is_direction_reversed(&self, value: bool) {
384        self.set_value(Self::is_direction_reversed_property(), value)
385    }
386
387    /// Indicates whether the slider automatically moves the thumb to the
388    /// closest tick mark.
389    pub fn is_snap_to_tick_enabled(&self) -> bool {
390        self.get_value(Self::is_snap_to_tick_enabled_property())
391    }
392
393    pub fn set_is_snap_to_tick_enabled(&self, value: bool) {
394        self.set_value(Self::is_snap_to_tick_enabled_property(), value)
395    }
396
397    /// The interval between tick marks.
398    pub fn tick_frequency(&self) -> f64 {
399        self.get_value(Self::tick_frequency_property())
400    }
401
402    pub fn set_tick_frequency(&self, value: f64) {
403        self.set_value(Self::tick_frequency_property(), value)
404    }
405
406    /// Indicates where to draw tick marks in relation to the track.
407    pub fn tick_placement(&self) -> TickPlacement {
408        self.get_value(Self::tick_placement_property())
409    }
410
411    pub fn set_tick_placement(&self, value: TickPlacement) {
412        self.set_value(Self::tick_placement_property(), value)
413    }
414
415    /// Whether the slider is currently being dragged.
416    pub fn is_dragging(&self) -> bool {
417        self.is_dragging.get()
418    }
419
420    /// The track part of the slider.
421    pub fn track(&self) -> Option<Ref<Track>> {
422        self.track.borrow().clone()
423    }
424
425
426    fn move_to_next_tick(&self, direction: f64) {
427        if direction == 0.0 {
428            return;
429        }
430
431        let value = self.value();
432        let minimum = self.minimum();
433        let maximum = self.maximum();
434
435        // Find the next value by snapping.
436        let mut next = self.snap_to_tick(f64::max(minimum, f64::min(maximum, value + direction)));
437
438        // Search for the next tick greater than value?
439        let greater_than = direction > 0.0;
440
441        // If the snapping brought us back to value, find the next tick
442        // point.
443        if (next - value).abs() < TOLERANCE
444            // Stop if searching up if already at Max.
445            && !(greater_than && (value - maximum).abs() < TOLERANCE)
446            // Stop if searching down if already at Min.
447            && !(!greater_than && (value - minimum).abs() < TOLERANCE)
448        {
449            let ticks = self.ticks().filter(|ticks| ticks.count() > 0);
450            let tick_frequency = self.tick_frequency();
451
452            // If the ticks collection is available, use it. Note that the
453            // ticks may be unsorted.
454            if let Some(ticks) = ticks {
455                for &tick in ticks.snapshot().iter() {
456                    // Find the smallest tick greater than value or the
457                    // largest tick less than value.
458                    if greater_than
459                        && MathUtilities::greater_than(tick, value)
460                        && (MathUtilities::less_than(tick, next) || (next - value).abs() < TOLERANCE)
461                        || !greater_than
462                            && MathUtilities::less_than(tick, value)
463                            && (MathUtilities::greater_than(tick, next) || (next - value).abs() < TOLERANCE)
464                    {
465                        next = tick;
466                    }
467                }
468            } else if MathUtilities::greater_than(tick_frequency, 0.0) {
469                // Find the current tick we are at.
470                let mut tick_number = ((value - minimum) / tick_frequency).round_ties_even();
471
472                if greater_than {
473                    tick_number += 1.0;
474                } else {
475                    tick_number -= 1.0;
476                }
477
478                next = minimum + tick_number * tick_frequency;
479            }
480        }
481
482        // Update if we've found a better value.
483        if (next - value).abs() > TOLERANCE {
484            self.set_current_value(RangeBase::value_property(), next);
485        }
486    }
487
488    fn track_visual(&self) -> Option<Ref<Visual>> {
489        self.track.borrow().clone().map(Ref::upcast)
490    }
491
492    fn track_moved(&self, e: &PointerEventArgs) {
493        if !self.is_enabled() {
494            self.is_dragging.set(false);
495            return;
496        }
497
498        if self.is_dragging.get() {
499            self.move_to_point(e.get_current_point(self.track_visual().as_deref()));
500        }
501    }
502
503    fn track_released(&self, _e: &PointerReleasedEventArgs) {
504        self.is_dragging.set(false);
505    }
506
507    fn track_pressed(&self, e: &PointerPressedEventArgs) {
508        if e.get_current_point(Some(self)).properties.is_left_button_pressed {
509            self.move_to_point(e.get_current_point(self.track_visual().as_deref()));
510            self.is_dragging.set(true);
511        }
512    }
513
514    fn move_to_point(&self, pos_on_track: PointerPoint) {
515        let Some(track) = self.track.borrow().clone() else {
516            return;
517        };
518
519        let orient = self.orientation() == Orientation::Horizontal;
520        let thumb_bounds = track.thumb().map(|thumb| thumb.bounds());
521        // The smallest positive value is added to avoid a division by zero.
522        let thumb_length =
523            thumb_bounds.map_or(0.0, |bounds| if orient { bounds.width } else { bounds.height }) + 5e-324;
524        let track_length = (if orient { track.bounds().width } else { track.bounds().height }) - thumb_length;
525        let track_pos = if orient { pos_on_track.position.x } else { pos_on_track.position.y };
526        let logical_pos = MathUtilities::clamp((track_pos - thumb_length * 0.5) / track_length, 0.0, 1.0);
527        let invert = if orient == self.is_direction_reversed() { 1.0 } else { 0.0 };
528        let calc_val = f64::abs(invert - logical_pos);
529        let range = self.maximum() - self.minimum();
530        let final_value = calc_val * range + self.minimum();
531
532        self.set_current_value(
533            RangeBase::value_property(),
534            if self.is_snap_to_tick_enabled() { self.snap_to_tick(final_value) } else { final_value },
535        );
536    }
537
538    /// Snaps the input value to the closest tick.
539    fn snap_to_tick(&self, mut value: f64) -> f64 {
540        if self.is_snap_to_tick_enabled() {
541            let mut previous = self.minimum();
542            let mut next = self.maximum();
543
544            let ticks = self.ticks().filter(|ticks| ticks.count() > 0);
545            let tick_frequency = self.tick_frequency();
546
547            // If the ticks collection is available, use it. Note that the
548            // ticks may be unsorted.
549            if let Some(ticks) = ticks {
550                for &tick in ticks.snapshot().iter() {
551                    if MathUtilities::are_close(tick, value) {
552                        return value;
553                    }
554
555                    if MathUtilities::less_than(tick, value) && MathUtilities::greater_than(tick, previous) {
556                        previous = tick;
557                    } else if MathUtilities::greater_than(tick, value) && MathUtilities::less_than(tick, next) {
558                        next = tick;
559                    }
560                }
561            } else if MathUtilities::greater_than(tick_frequency, 0.0) {
562                previous =
563                    self.minimum() + ((value - self.minimum()) / tick_frequency).round_ties_even() * tick_frequency;
564                next = f64::min(self.maximum(), previous + tick_frequency);
565            }
566
567            // Choose the closest value between previous and next. If tie,
568            // snap to 'next'.
569            value = if MathUtilities::greater_than_or_close(value, (previous + next) * 0.5) { next } else { previous };
570        }
571
572        value
573    }
574
575    fn update_pseudo_classes(&self, o: Orientation) {
576        self.pseudo_classes().set(PC_VERTICAL, o == Orientation::Vertical);
577        self.pseudo_classes().set(PC_HORIZONTAL, o == Orientation::Horizontal);
578    }
579}