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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31#[repr(i32)]
32pub enum TickPlacement {
33 None = 0,
35 TopLeft = 1,
38 BottomRight = 2,
41 Outside = 3,
44}
45
46#[repr(C)]
49pub struct Slider {
50 base: RangeBase,
51 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 fn on_thumb_drag_started(this, e: &VectorEventArgs);
68 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 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 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 pub fn orientation_property() -> StyledProperty<Orientation> {
272 ScrollBar::orientation_property().add_owner::<Slider>()
273 }
274 );
275
276 ferro_property!(
277 pub fn is_direction_reversed_property() -> StyledProperty<bool> {
279 Track::is_direction_reversed_property().add_owner::<Slider>()
280 }
281 );
282
283 ferro_property!(
284 pub fn is_snap_to_tick_enabled_property() -> StyledProperty<bool> {
286 FerroProperty::register::<Slider, _>("IsSnapToTickEnabled", false)
287 }
288 );
289
290 ferro_property!(
291 pub fn tick_frequency_property() -> StyledProperty<f64> {
293 FerroProperty::register::<Slider, _>("TickFrequency", 0.0)
294 }
295 );
296
297 ferro_property!(
298 pub fn tick_placement_property() -> StyledProperty<TickPlacement> {
300 FerroProperty::register::<Slider, _>("TickPlacement", TickPlacement::None)
301 }
302 );
303
304 ferro_property!(
305 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 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 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 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 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 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 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 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 pub fn is_dragging(&self) -> bool {
417 self.is_dragging.get()
418 }
419
420 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 let mut next = self.snap_to_tick(f64::max(minimum, f64::min(maximum, value + direction)));
437
438 let greater_than = direction > 0.0;
440
441 if (next - value).abs() < TOLERANCE
444 && !(greater_than && (value - maximum).abs() < TOLERANCE)
446 && !(!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 let Some(ticks) = ticks {
455 for &tick in ticks.snapshot().iter() {
456 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 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 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 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 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 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 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}