1use ferroui_base::data::BindingMode;
11use ferroui_base::input::{
12 IPointer, InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs,
13 PointerCaptureLostEventArgs, PointerEventArgs, PointerPressedEventArgs,
14 PointerReleasedEventArgs, PointerType,
15};
16use ferroui_base::interactivity::{
17 InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutingStrategies,
18};
19use ferroui_base::layout::{LayoutableImpl, Orientation};
20use ferroui_base::media::{BoxShadows, DrawingContext, FlowDirection, IBrush};
21use ferroui_base::reactive::IDisposable;
22use ferroui_base::{
23 FerroObject, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs,
24 Point, Rect, Ref, RoundedRect, Size, StyledElementImpl, StyledProperty, StyledPropertyOptions,
25 Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs, WeakRef, ferro_class,
26 ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, instantiate,
27};
28use ferroui_controls::automation::peers::{
29 AutomationControlType, AutomationPeer, AutomationPeerImpl, ControlAutomationPeer,
30 ControlAutomationPeerImpl, RangeBaseAutomationPeer, RangeBaseAutomationPeerImpl,
31};
32use ferroui_controls::automation::provider::IRangeValueProvider;
33use ferroui_controls::automation::{AutomationProperties, ElementNotEnabledException};
34use ferroui_controls::primitives::{
35 RangeBase, RangeBaseValueChangedEventArgs, TemplateAppliedEventArgs, TemplatedControl,
36 TemplatedControlImpl, TemplatedControlImplExt,
37};
38use ferroui_controls::{ControlImpl, Panel, PanelImpl};
39use std::cell::{Cell, RefCell};
40use std::rc::Rc;
41
42const HANDLE_WIDTH: f64 = 4.0;
43const TRACK_HEIGHT: f64 = 16.0;
44const TRACK_GAP: f64 = 6.0;
45const STOP_SIZE: f64 = 4.0;
46const TARGET_SIZE: f64 = 48.0;
47const TOUCH_SLOP: f64 = 8.0;
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum MaterialRangeThumb {
51 Start,
52 End,
53}
54
55struct Drag {
56 pointer: Rc<dyn IPointer>,
57 origin: Point,
58 thumb: MaterialRangeThumb,
59 dragging: bool,
60 coincident: bool,
61}
62
63#[repr(C)]
65pub struct MaterialRangeSlider {
66 base: TemplatedControl,
67 layout: RefCell<Option<Ref<MaterialRangeSliderLayout>>>,
68 start_thumb: RefCell<Option<Ref<MaterialRangeSliderThumb>>>,
69 end_thumb: RefCell<Option<Ref<MaterialRangeSliderThumb>>>,
70 subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
71 synchronizing: Cell<bool>,
72 drag: RefCell<Option<Drag>>,
73 edited: Cell<bool>,
74}
75ferro_class!(MaterialRangeSlider: TemplatedControl);
76ferro_class_info!(MaterialRangeSlider {
77 new: MaterialRangeSlider::new
78});
79ferro_impl_classes!(MaterialRangeSlider: StyledElementImpl, LayoutableImpl, InteractiveImpl, ControlImpl);
80
81impl FerroObjectImpl for MaterialRangeSlider {
82 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
83 Self::parent_on_property_changed(this, change);
84 let p = change.property();
85 if p == Self::minimum_property().as_property()
86 || p == Self::maximum_property().as_property()
87 || p == Self::range_start_property().as_property()
88 || p == Self::range_end_property().as_property()
89 || p == Self::steps_property().as_property()
90 || p == Self::orientation_property().as_property()
91 || p == Self::is_direction_reversed_property().as_property()
92 || p == Visual::flow_direction_property().as_property()
93 || p == Self::start_label_property().as_property()
94 || p == Self::end_label_property().as_property()
95 {
96 this.synchronize();
97 }
98 if p == Self::orientation_property().as_property() {
99 this.pseudo_classes()
100 .set(":vertical", this.orientation() == Orientation::Vertical);
101 this.pseudo_classes()
102 .set(":horizontal", this.orientation() == Orientation::Horizontal);
103 }
104 if p == InputElement::is_effectively_enabled_property().as_property()
105 || p == Self::is_read_only_property().as_property()
106 {
107 if !this.is_effectively_enabled() || this.is_read_only() {
108 this.cancel_drag();
109 }
110 }
111 if let Some(layout) = this.layout.borrow().as_ref() {
112 layout.invalidate_visual();
113 }
114 }
115 fn constructed(this: &Self) {
116 Self::parent_constructed(this);
117 this.pseudo_classes().set(":horizontal", true);
118 }
119}
120
121impl VisualImpl for MaterialRangeSlider {
122 fn bypass_flow_direction_policies(_: &Self) -> bool {
123 true
124 }
125 fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
126 this.cancel_drag();
127 Self::parent_on_detached_from_visual_tree(this, e);
128 }
129}
130
131impl TemplatedControlImpl for MaterialRangeSlider {
132 fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
133 this.cancel_drag();
134 for subscription in this.subscriptions.borrow_mut().drain(..) {
135 subscription.dispose();
136 }
137 for thumb in [&this.start_thumb, &this.end_thumb] {
138 if let Some(thumb) = thumb.borrow_mut().take() {
139 *thumb.owner.borrow_mut() = None;
140 }
141 }
142 if let Some(layout) = this.layout.borrow_mut().take() {
143 *layout.owner.borrow_mut() = None;
144 }
145 Self::parent_on_apply_template(this, e);
146 *this.layout.borrow_mut() = e
147 .name_scope()
148 .find_as::<MaterialRangeSliderLayout>("PART_Layout");
149 *this.start_thumb.borrow_mut() = e
150 .name_scope()
151 .find_as::<MaterialRangeSliderThumb>("PART_StartThumb");
152 *this.end_thumb.borrow_mut() = e
153 .name_scope()
154 .find_as::<MaterialRangeSliderThumb>("PART_EndThumb");
155 let weak = this.to_ref().downgrade();
156 if let Some(layout) = this.layout.borrow().as_ref() {
157 *layout.owner.borrow_mut() = Some(weak.clone());
158 }
159 for (slot, role) in [
160 (&this.start_thumb, MaterialRangeThumb::Start),
161 (&this.end_thumb, MaterialRangeThumb::End),
162 ] {
163 if let Some(thumb) = slot.borrow().as_ref() {
164 *thumb.owner.borrow_mut() = Some(weak.clone());
165 thumb.role.set(role);
166 let weak = weak.clone();
167 let subscription = thumb.add_disposable_handler_with(
168 RangeBase::value_changed_event(),
169 move |_, e: &RangeBaseValueChangedEventArgs| {
170 if let Some(owner) = weak.upgrade() {
171 if !owner.synchronizing.get() {
172 owner.set_thumb_value(role, e.new_value());
173 }
174 }
175 },
176 RoutingStrategies::BUBBLE,
177 false,
178 );
179 this.subscriptions.borrow_mut().push(subscription);
180 }
181 }
182 this.synchronize();
183 }
184}
185
186impl InputElementImpl for MaterialRangeSlider {
187 fn on_pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
188 Self::parent_on_pointer_pressed(this, e);
189 if !this.is_effectively_enabled() || this.is_read_only() || this.drag.borrow().is_some() {
190 return;
191 }
192 if e.pointer().type_() == PointerType::Mouse
193 && !e
194 .get_current_point(Some(this))
195 .properties
196 .is_left_button_pressed
197 {
198 return;
199 }
200 let Some(layout) = this.layout.borrow().clone() else {
201 return;
202 };
203 let position = e.get_position(Some(&layout));
204 let geometry = this.geometry(layout.bounds().size());
205 let logical = geometry.fraction_at(position);
206 let start = this.fraction(this.range_start());
207 let end = this.fraction(this.range_end());
208 let first_distance = (start - logical).abs();
209 let second_distance = (end - logical).abs();
210 let thumb = if first_distance < second_distance
211 || (first_distance == second_distance && start > logical)
212 {
213 MaterialRangeThumb::Start
214 } else {
215 MaterialRangeThumb::End
216 };
217 let dragging = e.pointer().type_() == PointerType::Mouse;
218 *this.drag.borrow_mut() = Some(Drag {
219 pointer: e.pointer().clone(),
220 origin: position,
221 thumb,
222 dragging,
223 coincident: (geometry.start - geometry.end).abs() <= TOUCH_SLOP,
224 });
225 this.press_thumb(thumb, true);
226 if let Some(thumb) = this.thumb(thumb) {
227 thumb.focus();
228 }
229 if dragging {
230 e.pointer().capture(Some(&this.to_ref().upcast()));
231 e.prevent_gesture_recognition();
232 this.set_thumb_value(thumb, this.value_at_fraction(logical));
233 e.set_handled(true);
234 }
235 }
236 fn on_pointer_moved(this: &Self, e: &PointerEventArgs) {
237 Self::parent_on_pointer_moved(this, e);
238 let Some(layout) = this.layout.borrow().clone() else {
239 return;
240 };
241 let position = e.get_position(Some(&layout));
242 let geometry = this.geometry(layout.bounds().size());
243 let mut cancel = false;
244 let mut acquired = false;
245 let thumb;
246 {
247 let mut drag = this.drag.borrow_mut();
248 let Some(drag) = drag.as_mut() else {
249 return;
250 };
251 if drag.pointer.id() != e.pointer().id() {
252 return;
253 }
254 if !drag.dragging {
255 let delta = position - drag.origin;
256 let (primary, cross) = if this.orientation() == Orientation::Horizontal {
257 (delta.x, delta.y)
258 } else {
259 (delta.y, delta.x)
260 };
261 if cross.abs() > TOUCH_SLOP && cross.abs() > primary.abs() {
262 cancel = true;
263 } else if primary.abs() > TOUCH_SLOP {
264 if drag.coincident {
265 let direction = if geometry.reversed { -primary } else { primary };
266 drag.thumb = if direction < 0.0 {
267 MaterialRangeThumb::Start
268 } else {
269 MaterialRangeThumb::End
270 };
271 }
272 drag.dragging = true;
273 acquired = true;
274 } else {
275 return;
276 }
277 }
278 thumb = drag.thumb;
279 }
280 if cancel {
281 this.cancel_drag();
282 return;
283 }
284 if acquired {
285 this.press_thumb(
286 MaterialRangeThumb::Start,
287 thumb == MaterialRangeThumb::Start,
288 );
289 this.press_thumb(MaterialRangeThumb::End, thumb == MaterialRangeThumb::End);
290 e.pointer().capture(Some(&this.to_ref().upcast()));
291 e.prevent_gesture_recognition();
292 }
293 this.set_thumb_value(
294 thumb,
295 this.value_at_fraction(geometry.fraction_at(position)),
296 );
297 e.set_handled(true);
298 }
299 fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
300 Self::parent_on_pointer_released(this, e);
301 let role = this
302 .drag
303 .borrow()
304 .as_ref()
305 .filter(|drag| drag.pointer.id() == e.pointer().id())
306 .map(|drag| drag.thumb);
307 let Some(role) = role else {
308 return;
309 };
310 if let Some(layout) = this.layout.borrow().clone() {
311 let geometry = this.geometry(layout.bounds().size());
312 this.set_thumb_value(
313 role,
314 this.value_at_fraction(geometry.fraction_at(e.get_position(Some(&layout)))),
315 );
316 }
317 this.cancel_drag();
318 e.set_handled(true);
319 }
320 fn on_pointer_capture_lost(this: &Self, e: &PointerCaptureLostEventArgs) {
321 if this
322 .drag
323 .borrow()
324 .as_ref()
325 .is_some_and(|drag| drag.pointer.id() == e.pointer().id())
326 {
327 this.cancel_drag();
328 }
329 Self::parent_on_pointer_capture_lost(this, e);
330 }
331}
332
333ferro_properties! { impl MaterialRangeSlider {
334 pub fn minimum_property() -> StyledProperty<f64> { FerroProperty::register_with::<Self, _>("Minimum", StyledPropertyOptions::new(0.0).validate(|v: &f64| v.is_finite())) }
335 pub fn maximum_property() -> StyledProperty<f64> { FerroProperty::register_with::<Self, _>("Maximum", StyledPropertyOptions::new(100.0).validate(|v: &f64| v.is_finite()).coerce(|o, v| v.max(o.get_value(Self::minimum_property())))) }
336 pub fn range_start_property() -> StyledProperty<f64> { FerroProperty::register_with::<Self, _>("RangeStart", StyledPropertyOptions::new(0.0).default_binding_mode(BindingMode::TwoWay).validate(|v: &f64| v.is_finite()).coerce(Self::coerce_start)) }
337 pub fn range_end_property() -> StyledProperty<f64> { FerroProperty::register_with::<Self, _>("RangeEnd", StyledPropertyOptions::new(100.0).default_binding_mode(BindingMode::TwoWay).validate(|v: &f64| v.is_finite()).coerce(Self::coerce_end)) }
338 pub fn steps_property() -> StyledProperty<i32> { FerroProperty::register_with::<Self, _>("Steps", StyledPropertyOptions::new(0).validate(|v: &i32| *v >= 0)) }
339 pub fn orientation_property() -> StyledProperty<Orientation> { FerroProperty::register::<Self, _>("Orientation", Orientation::Horizontal) }
340 pub fn is_direction_reversed_property() -> StyledProperty<bool> { FerroProperty::register::<Self, _>("IsDirectionReversed", false) }
341 pub fn is_read_only_property() -> StyledProperty<bool> { FerroProperty::register::<Self, _>("IsReadOnly", false) }
342 pub fn start_label_property() -> StyledProperty<String> { FerroProperty::register::<Self, _>("StartLabel", "Range start".to_owned()) }
343 pub fn end_label_property() -> StyledProperty<String> { FerroProperty::register::<Self, _>("EndLabel", "Range end".to_owned()) }
344 pub fn active_tick_brush_property() -> StyledProperty<Option<Rc<dyn IBrush>>> { FerroProperty::register::<Self, _>("ActiveTickBrush", None) }
345 pub fn inactive_tick_brush_property() -> StyledProperty<Option<Rc<dyn IBrush>>> { FerroProperty::register::<Self, _>("InactiveTickBrush", None) }
346}}
347
348impl MaterialRangeSlider {
349 ferro_routed_event!(
350 pub fn value_change_finished_event() -> RoutedEvent<RoutedEventArgs> {
351 RoutedEvent::register::<Self, _>("ValueChangeFinished", RoutingStrategies::BUBBLE)
352 }
353 );
354 pub fn construct() -> Self {
355 Self {
356 base: TemplatedControl::construct(),
357 layout: RefCell::new(None),
358 start_thumb: RefCell::new(None),
359 end_thumb: RefCell::new(None),
360 subscriptions: RefCell::new(Vec::new()),
361 synchronizing: Cell::new(false),
362 drag: RefCell::new(None),
363 edited: Cell::new(false),
364 }
365 }
366 pub fn new() -> Ref<Self> {
367 instantiate(Self::construct())
368 }
369 pub fn minimum(&self) -> f64 {
370 self.get_value(Self::minimum_property())
371 }
372 pub fn set_minimum(&self, v: f64) {
373 self.set_value(Self::minimum_property(), v);
374 }
375 pub fn maximum(&self) -> f64 {
376 self.get_value(Self::maximum_property())
377 }
378 pub fn set_maximum(&self, v: f64) {
379 self.set_value(Self::maximum_property(), v);
380 }
381 pub fn range_start(&self) -> f64 {
382 self.get_value(Self::range_start_property())
383 }
384 pub fn set_range_start(&self, v: f64) {
385 self.set_value(Self::range_start_property(), v);
386 }
387 pub fn range_end(&self) -> f64 {
388 self.get_value(Self::range_end_property())
389 }
390 pub fn set_range_end(&self, v: f64) {
391 self.set_value(Self::range_end_property(), v);
392 }
393 pub fn steps(&self) -> i32 {
395 self.get_value(Self::steps_property())
396 }
397 pub fn set_steps(&self, v: i32) {
398 self.set_value(Self::steps_property(), v);
399 }
400 pub fn orientation(&self) -> Orientation {
401 self.get_value(Self::orientation_property())
402 }
403 pub fn set_orientation(&self, v: Orientation) {
404 self.set_value(Self::orientation_property(), v);
405 }
406 pub fn is_direction_reversed(&self) -> bool {
407 self.get_value(Self::is_direction_reversed_property())
408 }
409 pub fn set_is_direction_reversed(&self, v: bool) {
410 self.set_value(Self::is_direction_reversed_property(), v);
411 }
412 pub fn is_read_only(&self) -> bool {
413 self.get_value(Self::is_read_only_property())
414 }
415 pub fn set_is_read_only(&self, v: bool) {
416 self.set_value(Self::is_read_only_property(), v);
417 }
418 pub fn start_label(&self) -> String {
419 self.get_value(Self::start_label_property())
420 }
421 pub fn set_start_label(&self, v: impl Into<String>) {
422 self.set_value(Self::start_label_property(), v.into());
423 }
424 pub fn end_label(&self) -> String {
425 self.get_value(Self::end_label_property())
426 }
427 pub fn set_end_label(&self, v: impl Into<String>) {
428 self.set_value(Self::end_label_property(), v.into());
429 }
430 pub fn active_tick_brush(&self) -> Option<Rc<dyn IBrush>> {
431 self.get_value(Self::active_tick_brush_property())
432 }
433 pub fn set_active_tick_brush(&self, v: Option<Rc<dyn IBrush>>) {
434 self.set_value(Self::active_tick_brush_property(), v);
435 }
436 pub fn inactive_tick_brush(&self) -> Option<Rc<dyn IBrush>> {
437 self.get_value(Self::inactive_tick_brush_property())
438 }
439 pub fn set_inactive_tick_brush(&self, v: Option<Rc<dyn IBrush>>) {
440 self.set_value(Self::inactive_tick_brush_property(), v);
441 }
442
443 fn coerce_start(o: &FerroObject, value: f64) -> f64 {
444 let minimum = o.get_value(Self::minimum_property());
445 let maximum = o.get_value(Self::maximum_property()).max(minimum);
446 snap(value, minimum, maximum, o.get_value(Self::steps_property())).clamp(
447 minimum,
448 o.get_value(Self::range_end_property())
449 .clamp(minimum, maximum),
450 )
451 }
452 fn coerce_end(o: &FerroObject, value: f64) -> f64 {
453 let minimum = o.get_value(Self::minimum_property());
454 let maximum = o.get_value(Self::maximum_property()).max(minimum);
455 snap(value, minimum, maximum, o.get_value(Self::steps_property())).clamp(
456 o.get_value(Self::range_start_property())
457 .clamp(minimum, maximum),
458 maximum,
459 )
460 }
461 fn fraction(&self, value: f64) -> f64 {
462 if self.maximum() <= self.minimum() {
463 0.0
464 } else {
465 ((value - self.minimum()) / (self.maximum() - self.minimum())).clamp(0.0, 1.0)
466 }
467 }
468 fn value_at_fraction(&self, fraction: f64) -> f64 {
469 self.minimum() + fraction * (self.maximum() - self.minimum())
470 }
471 fn reversed(&self) -> bool {
472 match self.orientation() {
473 Orientation::Horizontal => {
474 self.is_direction_reversed() ^ (self.flow_direction() == FlowDirection::RightToLeft)
475 }
476 Orientation::Vertical => !self.is_direction_reversed(),
477 }
478 }
479 fn geometry(&self, size: Size) -> Geometry {
480 Geometry::new(
481 size,
482 self.orientation(),
483 self.reversed(),
484 self.fraction(self.range_start()),
485 self.fraction(self.range_end()),
486 self.steps() > 0,
487 self.thumb(MaterialRangeThumb::Start)
488 .map_or(HANDLE_WIDTH, |thumb| thumb.handle_thickness()),
489 self.thumb(MaterialRangeThumb::End)
490 .map_or(HANDLE_WIDTH, |thumb| thumb.handle_thickness()),
491 )
492 }
493 pub fn thumb(&self, role: MaterialRangeThumb) -> Option<Ref<MaterialRangeSliderThumb>> {
494 match role {
495 MaterialRangeThumb::Start => self.start_thumb.borrow().clone(),
496 MaterialRangeThumb::End => self.end_thumb.borrow().clone(),
497 }
498 }
499 fn synchronize(&self) {
500 if self.synchronizing.replace(true) {
501 return;
502 }
503 self.coerce_value(Self::maximum_property().as_property());
504 self.coerce_value(Self::range_end_property().as_property());
505 self.coerce_value(Self::range_start_property().as_property());
506 let increment = (self.maximum() - self.minimum())
507 / if self.steps() > 0 {
508 self.steps() as f64 + 1.0
509 } else {
510 100.0
511 };
512 let page = (if self.steps() > 0 {
513 self.steps() as f64 + 1.0
514 } else {
515 100.0
516 } / 10.0)
517 .floor()
518 .clamp(1.0, 10.0)
519 * increment;
520 for role in [MaterialRangeThumb::Start, MaterialRangeThumb::End] {
521 if let Some(thumb) = self.thumb(role) {
522 thumb.set_minimum(self.minimum());
525 thumb.set_maximum(self.maximum());
526 match role {
527 MaterialRangeThumb::Start => {
528 thumb.set_maximum(self.range_end());
529 thumb.set_range_value(self.range_start());
530 }
531 MaterialRangeThumb::End => {
532 thumb.set_minimum(self.range_start());
533 thumb.set_range_value(self.range_end());
534 }
535 }
536 thumb.set_small_change(increment);
537 thumb.set_large_change(page);
538 thumb.set_orientation(self.orientation());
539 AutomationProperties::set_name(
540 &thumb,
541 Some(&match role {
542 MaterialRangeThumb::Start => self.start_label(),
543 MaterialRangeThumb::End => self.end_label(),
544 }),
545 );
546 }
547 }
548 self.synchronizing.set(false);
549 if let Some(layout) = self.layout.borrow().as_ref() {
550 layout.invalidate_measure();
551 layout.invalidate_arrange();
552 layout.invalidate_visual();
553 }
554 }
555 fn set_thumb_value(&self, role: MaterialRangeThumb, value: f64) {
556 if !value.is_finite() || !self.is_effectively_enabled() || self.is_read_only() {
557 return;
558 }
559 let before = (self.range_start(), self.range_end());
560 let value = snap(value, self.minimum(), self.maximum(), self.steps());
561 let value = match role {
564 MaterialRangeThumb::Start => value.clamp(self.minimum(), self.range_end()),
565 MaterialRangeThumb::End => value.clamp(self.range_start(), self.maximum()),
566 };
567 match role {
568 MaterialRangeThumb::Start => {
569 self.set_current_value(Self::range_start_property(), value)
570 }
571 MaterialRangeThumb::End => self.set_current_value(Self::range_end_property(), value),
572 }
573 if before != (self.range_start(), self.range_end()) {
574 self.edited.set(true);
575 }
576 self.synchronize();
578 }
579 fn press_thumb(&self, role: MaterialRangeThumb, pressed: bool) {
580 if let Some(thumb) = self.thumb(role) {
581 thumb.pseudo_classes().set(":pressed", pressed);
582 }
583 }
584 fn cancel_drag(&self) {
585 let drag = self.drag.borrow_mut().take();
586 self.press_thumb(MaterialRangeThumb::Start, false);
587 self.press_thumb(MaterialRangeThumb::End, false);
588 if let Some(drag) = drag {
589 if drag
590 .pointer
591 .captured()
592 .is_some_and(|element| element == self.to_ref().upcast::<InputElement>())
593 {
594 drag.pointer.capture(None);
595 }
596 self.finish_edit();
597 }
598 }
599 fn finish_edit(&self) {
600 if self.edited.replace(false) {
601 self.raise_event(&RoutedEventArgs::with_event(
602 Self::value_change_finished_event(),
603 ));
604 }
605 }
606 fn handle_key(&self, role: MaterialRangeThumb, key: Key) -> bool {
607 if !self.is_effectively_enabled() || self.is_read_only() {
608 return false;
609 }
610 let Some(thumb) = self.thumb(role) else {
611 return false;
612 };
613 let increment = thumb.small_change();
614 let directional = match (self.orientation(), key) {
615 (Orientation::Horizontal, Key::Left) | (Orientation::Vertical, Key::Up) => {
616 Some(if self.reversed() {
617 increment
618 } else {
619 -increment
620 })
621 }
622 (Orientation::Horizontal, Key::Right) | (Orientation::Vertical, Key::Down) => {
623 Some(if self.reversed() {
624 -increment
625 } else {
626 increment
627 })
628 }
629 _ => None,
630 };
631 let value = if let Some(delta) = directional {
632 thumb.value() + delta
633 } else {
634 match key {
635 Key::Home => thumb.minimum(),
636 Key::End => thumb.maximum(),
637 Key::PageUp => thumb.value() + thumb.large_change(),
638 Key::PageDown => thumb.value() - thumb.large_change(),
639 _ => return false,
640 }
641 };
642 self.set_thumb_value(role, value);
643 true
644 }
645}
646
647fn snap(value: f64, minimum: f64, maximum: f64, steps: i32) -> f64 {
648 if maximum <= minimum {
649 return minimum;
650 }
651 let value = value.clamp(minimum, maximum);
652 if steps == 0 {
653 value
654 } else {
655 let intervals = steps as f64 + 1.0;
656 minimum
657 + (((value - minimum) / (maximum - minimum) * intervals).round() / intervals)
658 * (maximum - minimum)
659 }
660}
661
662#[repr(C)]
664pub struct MaterialRangeSliderThumb {
665 base: RangeBase,
666 owner: RefCell<Option<WeakRef<MaterialRangeSlider>>>,
667 role: Cell<MaterialRangeThumb>,
668 key_active: Cell<bool>,
669}
670ferro_class!(MaterialRangeSliderThumb: RangeBase);
671ferro_class_info!(MaterialRangeSliderThumb {
672 new: MaterialRangeSliderThumb::new
673});
674ferro_impl_classes!(MaterialRangeSliderThumb: StyledElementImpl, LayoutableImpl, InteractiveImpl, TemplatedControlImpl);
675impl FerroObjectImpl for MaterialRangeSliderThumb {
676 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
677 Self::parent_on_property_changed(this, change);
678 if change.property() == Self::handle_thickness_property().as_property() {
679 if let Some(owner) = this.owner() {
680 if let Some(layout) = owner.layout.borrow().as_ref() {
681 layout.invalidate_arrange();
682 layout.invalidate_visual();
683 }
684 }
685 }
686 }
687}
688impl VisualImpl for MaterialRangeSliderThumb {
689 fn bypass_flow_direction_policies(_: &Self) -> bool {
690 true
691 }
692}
693impl ControlImpl for MaterialRangeSliderThumb {
694 fn on_create_automation_peer(this: &Self) -> Ref<AutomationPeer> {
695 MaterialRangeSliderThumbAutomationPeer::new(this).upcast()
696 }
697}
698impl InputElementImpl for MaterialRangeSliderThumb {
699 fn on_key_down(this: &Self, e: &KeyEventArgs) {
700 Self::parent_on_key_down(this, e);
701 if e.handled() || !e.key_modifiers.is_empty() {
702 return;
703 }
704 if this
705 .owner()
706 .is_some_and(|owner| owner.handle_key(this.role.get(), e.key))
707 {
708 this.key_active.set(true);
709 e.set_handled(true);
710 }
711 }
712 fn on_key_up(this: &Self, e: &KeyEventArgs) {
713 Self::parent_on_key_up(this, e);
714 if this.key_active.replace(false) {
715 if let Some(owner) = this.owner() {
716 owner.finish_edit();
717 }
718 }
719 }
720}
721ferro_properties! { impl MaterialRangeSliderThumb {
722 pub fn handle_thickness_property() -> StyledProperty<f64> { FerroProperty::register_with::<Self, _>("HandleThickness", StyledPropertyOptions::new(HANDLE_WIDTH).validate(|v: &f64| v.is_finite() && *v >= 0.0)) }
723 pub fn orientation_property() -> StyledProperty<Orientation> { FerroProperty::register::<Self, _>("Orientation", Orientation::Horizontal) }
724}}
725impl MaterialRangeSliderThumb {
726 fn static_constructor() {
727 InputElement::focusable_property().override_default_value::<Self>(true);
728 }
729 pub fn construct() -> Self {
730 Self {
731 base: RangeBase::construct(),
732 owner: RefCell::new(None),
733 role: Cell::new(MaterialRangeThumb::Start),
734 key_active: Cell::new(false),
735 }
736 }
737 pub fn new() -> Ref<Self> {
738 instantiate(Self::construct())
739 }
740 fn owner(&self) -> Option<Ref<MaterialRangeSlider>> {
741 self.owner.borrow().as_ref().and_then(WeakRef::upgrade)
742 }
743 pub fn handle_thickness(&self) -> f64 {
744 self.get_value(Self::handle_thickness_property())
745 }
746 pub fn set_handle_thickness(&self, v: f64) {
747 self.set_value(Self::handle_thickness_property(), v);
748 }
749 pub fn orientation(&self) -> Orientation {
750 self.get_value(Self::orientation_property())
751 }
752 pub fn set_orientation(&self, v: Orientation) {
753 self.set_value(Self::orientation_property(), v);
754 }
755}
756
757#[repr(C)]
758pub struct MaterialRangeSliderThumbAutomationPeer {
759 base: RangeBaseAutomationPeer,
760}
761ferro_class!(MaterialRangeSliderThumbAutomationPeer: RangeBaseAutomationPeer);
762ferro_class_info!(MaterialRangeSliderThumbAutomationPeer {
763 interfaces: [Rc<dyn IRangeValueProvider> => MaterialRangeSliderThumbAutomationPeer::provider]
764});
765ferro_impl_classes!(MaterialRangeSliderThumbAutomationPeer: FerroObjectImpl, ControlAutomationPeerImpl, RangeBaseAutomationPeerImpl);
766impl AutomationPeerImpl for MaterialRangeSliderThumbAutomationPeer {
767 fn get_class_name_core(_: &Self) -> String {
768 "MaterialRangeSliderThumb".to_owned()
769 }
770 fn get_automation_control_type_core(_: &Self) -> AutomationControlType {
771 AutomationControlType::Slider
772 }
773}
774impl MaterialRangeSliderThumbAutomationPeer {
775 pub fn new(owner: &MaterialRangeSliderThumb) -> Ref<Self> {
776 instantiate(Self {
777 base: RangeBaseAutomationPeer::construct(owner),
778 })
779 }
780 fn provider(peer: Ref<Self>) -> Rc<dyn IRangeValueProvider> {
781 Rc::new(RangeThumbProvider(peer))
782 }
783 fn thumb(&self) -> Ref<MaterialRangeSliderThumb> {
784 ControlAutomationPeer::owner(self)
785 .cast()
786 .expect("range thumb peer owner")
787 }
788}
789struct RangeThumbProvider(Ref<MaterialRangeSliderThumbAutomationPeer>);
790impl IRangeValueProvider for RangeThumbProvider {
791 fn peer(&self) -> Ref<AutomationPeer> {
792 self.0.clone().upcast()
793 }
794 fn is_read_only(&self) -> bool {
795 self.0
796 .thumb()
797 .owner()
798 .is_none_or(|owner| owner.is_read_only())
799 }
800 fn minimum(&self) -> f64 {
801 self.0.thumb().minimum()
802 }
803 fn maximum(&self) -> f64 {
804 self.0.thumb().maximum()
805 }
806 fn value(&self) -> f64 {
807 self.0.thumb().value()
808 }
809 fn large_change(&self) -> f64 {
810 self.0.thumb().large_change()
811 }
812 fn small_change(&self) -> f64 {
813 self.0.thumb().small_change()
814 }
815 fn set_value(&self, value: f64) -> Result<(), ElementNotEnabledException> {
816 self.0.ensure_enabled()?;
817 let thumb = self.0.thumb();
818 let owner = thumb.owner().ok_or_else(ElementNotEnabledException::new)?;
819 if owner.is_read_only() {
820 return Err(ElementNotEnabledException::with_message(
821 "Range slider is read-only.",
822 ));
823 }
824 if value.is_finite() {
825 owner.set_thumb_value(thumb.role.get(), value);
826 owner.finish_edit();
827 }
828 Ok(())
829 }
830}
831
832#[repr(C)]
834pub struct MaterialRangeSliderLayout {
835 base: Panel,
836 owner: RefCell<Option<WeakRef<MaterialRangeSlider>>>,
837}
838ferro_class!(MaterialRangeSliderLayout: Panel);
839ferro_class_info!(MaterialRangeSliderLayout {
840 new: MaterialRangeSliderLayout::new
841});
842ferro_impl_classes!(MaterialRangeSliderLayout: FerroObjectImpl, StyledElementImpl, InteractiveImpl, InputElementImpl, ControlImpl, PanelImpl);
843impl MaterialRangeSliderLayout {
844 pub fn construct() -> Self {
845 Self {
846 base: Panel::construct(),
847 owner: RefCell::new(None),
848 }
849 }
850 pub fn new() -> Ref<Self> {
851 instantiate(Self::construct())
852 }
853 fn owner(&self) -> Option<Ref<MaterialRangeSlider>> {
854 self.owner.borrow().as_ref().and_then(WeakRef::upgrade)
855 }
856}
857impl LayoutableImpl for MaterialRangeSliderLayout {
858 fn measure_override(this: &Self, available: Size) -> Size {
859 for child in this.children().snapshot().iter() {
860 child.measure(Size::new(TARGET_SIZE, TARGET_SIZE));
861 }
862 let vertical = this
863 .owner()
864 .is_some_and(|owner| owner.orientation() == Orientation::Vertical);
865 if vertical {
866 Size::new(
867 TARGET_SIZE,
868 if available.height.is_finite() {
869 available.height.max(TARGET_SIZE)
870 } else {
871 200.0
872 },
873 )
874 } else {
875 Size::new(
876 if available.width.is_finite() {
877 available.width.max(TARGET_SIZE)
878 } else {
879 200.0
880 },
881 TARGET_SIZE,
882 )
883 }
884 }
885 fn arrange_override(this: &Self, size: Size) -> Size {
886 if let Some(owner) = this.owner() {
887 let geometry = owner.geometry(size);
888 for (index, child) in this.children().snapshot().iter().enumerate() {
889 let position = if index == 0 {
890 geometry.start
891 } else {
892 geometry.end
893 };
894 let rect = if geometry.vertical {
895 Rect::new(
896 (size.width - TARGET_SIZE) / 2.0,
897 position - TARGET_SIZE / 2.0,
898 TARGET_SIZE,
899 TARGET_SIZE,
900 )
901 } else {
902 Rect::new(
903 position - TARGET_SIZE / 2.0,
904 (size.height - TARGET_SIZE) / 2.0,
905 TARGET_SIZE,
906 TARGET_SIZE,
907 )
908 };
909 child.arrange(rect);
910 }
911 }
912 size
913 }
914}
915impl VisualImpl for MaterialRangeSliderLayout {
916 fn bypass_flow_direction_policies(_: &Self) -> bool {
917 true
918 }
919 fn render(this: &Self, dc: &mut DrawingContext) {
920 Self::parent_render(this, dc);
921 let Some(owner) = this.owner() else {
922 return;
923 };
924 let g = owner.geometry(this.bounds().size());
925 if g.extent <= HANDLE_WIDTH {
926 return;
927 }
928 let half_start = owner
929 .thumb(MaterialRangeThumb::Start)
930 .map_or(2.0, |thumb| thumb.handle_thickness() / 2.0);
931 let half_end = owner
932 .thumb(MaterialRangeThumb::End)
933 .map_or(2.0, |thumb| thumb.handle_thickness() / 2.0);
934 let (low, high, low_gap, high_gap) = if g.start <= g.end {
935 (g.start, g.end, half_start + TRACK_GAP, half_end + TRACK_GAP)
936 } else {
937 (g.end, g.start, half_end + TRACK_GAP, half_start + TRACK_GAP)
938 };
939 let active = owner.foreground();
940 let inactive = owner.background();
941 g.segment(dc, inactive.as_ref(), g.inset, low - low_gap, 8.0, 2.0);
942 g.segment(
943 dc,
944 active.as_ref(),
945 low + low_gap,
946 high - high_gap,
947 2.0,
948 2.0,
949 );
950 g.segment(
951 dc,
952 inactive.as_ref(),
953 high + high_gap,
954 g.extent - g.far_inset,
955 2.0,
956 8.0,
957 );
958 if owner.steps() > 0 {
959 let intervals = owner.steps() as usize + 1;
960 let stride =
962 ((intervals as f64 * STOP_SIZE / (g.extent - g.inset - g.far_inset)).ceil()
963 as usize)
964 .max(1);
965 for step in (1..intervals).step_by(stride) {
967 let position = g.coordinate(step as f64 / intervals as f64);
968 if (position - g.start).abs() < half_start + TRACK_GAP
969 || (position - g.end).abs() < half_end + TRACK_GAP
970 {
971 continue;
972 }
973 let brush = if position > low && position < high {
974 owner.active_tick_brush()
975 } else {
976 owner.inactive_tick_brush()
977 };
978 g.dot(dc, brush.as_ref(), position);
979 }
980 }
981 for position in [g.inset + 8.0, g.extent - g.far_inset - 8.0] {
982 if position > 0.0
983 && (position - g.start).abs() >= half_start + TRACK_GAP
984 && (position - g.end).abs() >= half_end + TRACK_GAP
985 {
986 g.dot(dc, owner.inactive_tick_brush().as_ref(), position);
987 }
988 }
989 }
990}
991
992#[derive(Clone, Copy, Debug)]
993struct Geometry {
994 extent: f64,
995 cross: f64,
996 inset: f64,
997 far_inset: f64,
998 raw_min: f64,
999 raw_max: f64,
1000 start: f64,
1001 end: f64,
1002 vertical: bool,
1003 reversed: bool,
1004 discrete: bool,
1005}
1006impl Geometry {
1007 fn new(
1008 size: Size,
1009 orientation: Orientation,
1010 reversed: bool,
1011 start: f64,
1012 end: f64,
1013 discrete: bool,
1014 start_width: f64,
1015 end_width: f64,
1016 ) -> Self {
1017 let vertical = orientation == Orientation::Vertical;
1018 let (extent, cross) = if vertical {
1019 (size.height, size.width / 2.0)
1020 } else {
1021 (size.width, size.height / 2.0)
1022 };
1023 let raw_max = (extent - end_width / 2.0).max(0.0);
1024 let raw_min = (start_width / 2.0).min(raw_max);
1025 let mut result = Self {
1026 extent: extent.max(0.0),
1027 cross,
1028 inset: if reversed { extent - raw_max } else { raw_min },
1029 far_inset: if reversed { raw_min } else { extent - raw_max },
1030 raw_min,
1031 raw_max,
1032 start: 0.0,
1033 end: 0.0,
1034 vertical,
1035 reversed,
1036 discrete,
1037 };
1038 result.start = result.coordinate(start);
1039 result.end = result.coordinate(end);
1040 result
1041 }
1042 fn coordinate(self, fraction: f64) -> f64 {
1043 let fraction = fraction.clamp(0.0, 1.0);
1044 let span = self.raw_max - self.raw_min;
1045 let corner = if self.discrete && fraction > 0.0 && fraction < 1.0 {
1048 8.0_f64.min(span / 2.0)
1049 } else {
1050 0.0
1051 };
1052 let position = self.raw_min + corner + fraction * (span - corner * 2.0);
1053 if self.reversed {
1054 self.extent - position
1055 } else {
1056 position
1057 }
1058 }
1059 fn fraction_at(self, point: Point) -> f64 {
1060 let span = self.raw_max - self.raw_min;
1061 if span <= 0.0 {
1062 return 0.0;
1063 }
1064 let position = if self.vertical { point.y } else { point.x };
1065 let position = if self.reversed {
1066 self.extent - position
1067 } else {
1068 position
1069 };
1070 ((position - self.raw_min) / span).clamp(0.0, 1.0)
1071 }
1072 fn segment(
1073 self,
1074 dc: &mut DrawingContext,
1075 brush: Option<&Rc<dyn IBrush>>,
1076 from: f64,
1077 to: f64,
1078 first_radius: f64,
1079 last_radius: f64,
1080 ) {
1081 if to <= from || brush.is_none() {
1082 return;
1083 }
1084 let rect = if self.vertical {
1085 Rect::new(
1086 self.cross - TRACK_HEIGHT / 2.0,
1087 from,
1088 TRACK_HEIGHT,
1089 to - from,
1090 )
1091 } else {
1092 Rect::new(
1093 from,
1094 self.cross - TRACK_HEIGHT / 2.0,
1095 to - from,
1096 TRACK_HEIGHT,
1097 )
1098 };
1099 let first = first_radius.min((to - from) / 2.0);
1100 let last = last_radius.min((to - from) / 2.0);
1101 let rounded = if self.vertical {
1102 RoundedRect::from_radii(rect, first, first, last, last)
1103 } else {
1104 RoundedRect::from_radii(rect, first, last, last, first)
1105 };
1106 dc.draw_rounded_rectangle(brush, None, rounded, &BoxShadows::default());
1107 }
1108 fn dot(self, dc: &mut DrawingContext, brush: Option<&Rc<dyn IBrush>>, position: f64) {
1109 let point = if self.vertical {
1110 Point::new(self.cross, position)
1111 } else {
1112 Point::new(position, self.cross)
1113 };
1114 dc.draw_ellipse(
1115 brush,
1116 None,
1117 Rect::new(
1118 point.x - STOP_SIZE / 2.0,
1119 point.y - STOP_SIZE / 2.0,
1120 STOP_SIZE,
1121 STOP_SIZE,
1122 ),
1123 );
1124 }
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 use super::*;
1130 use ferroui_base::input::RawInputModifiers;
1131 use ferroui_base::input::{
1132 KeyModifiers, MouseButton, Pointer, PointerPointProperties, PointerUpdateKind,
1133 };
1134 use ferroui_base::layout::{HorizontalAlignment, VerticalAlignment};
1135 use ferroui_controls::{Border, Control, Window};
1136
1137 fn widget() -> (Ref<MaterialRangeSlider>, Ref<Window>) {
1138 let slider = MaterialRangeSlider::new();
1139 slider.set_range_start(20.0);
1140 slider.set_range_end(80.0);
1141 slider.set_width(240.0);
1142 slider.set_height(48.0);
1143 slider.set_horizontal_alignment(HorizontalAlignment::Left);
1144 slider.set_vertical_alignment(VerticalAlignment::Top);
1145 let window = Window::new();
1146 window.set_width(400.0);
1147 window.set_height(320.0);
1148 window.set_content(Some(Control::boxed(&slider)));
1149 window.show();
1150 window.update_layout();
1151 (slider, window)
1152 }
1153
1154 fn key(thumb: &MaterialRangeSliderThumb, key: Key, down: bool) -> bool {
1155 let mut event = KeyEventArgs::new();
1156 event.key = key;
1157 event.set_routed_event(Some(if down {
1158 InputElement::key_down_event()
1159 } else {
1160 InputElement::key_up_event()
1161 }));
1162 thumb.raise_event(&event);
1163 event.handled()
1164 }
1165
1166 fn point(slider: &MaterialRangeSlider, window: &Window, fraction: f64, cross: f64) -> Point {
1167 let layout = slider.layout.borrow().clone().unwrap();
1168 let geometry = slider.geometry(layout.bounds().size());
1169 let p = if geometry.vertical {
1170 Point::new(cross, geometry.coordinate(fraction))
1171 } else {
1172 Point::new(geometry.coordinate(fraction), cross)
1173 };
1174 layout.translate_point(p, window).unwrap()
1175 }
1176
1177 fn press(slider: &MaterialRangeSlider, window: &Window, pointer: &Rc<Pointer>, point: Point) {
1178 let event = PointerPressedEventArgs::new(
1179 slider.to_ref(),
1180 pointer.clone(),
1181 window,
1182 point,
1183 0,
1184 PointerPointProperties::new(
1185 RawInputModifiers::LEFT_MOUSE_BUTTON,
1186 PointerUpdateKind::LeftButtonPressed,
1187 ),
1188 KeyModifiers::NONE,
1189 1,
1190 );
1191 slider.raise_event(&event);
1192 }
1193
1194 fn moved(
1195 slider: &MaterialRangeSlider,
1196 window: &Window,
1197 pointer: &Rc<Pointer>,
1198 point: Point,
1199 ) -> bool {
1200 let event = PointerEventArgs::new(
1201 Some(InputElement::pointer_moved_event()),
1202 slider.to_ref(),
1203 pointer.clone(),
1204 Some(window),
1205 point,
1206 1,
1207 PointerPointProperties::new(
1208 RawInputModifiers::LEFT_MOUSE_BUTTON,
1209 PointerUpdateKind::Other,
1210 ),
1211 KeyModifiers::NONE,
1212 );
1213 slider.raise_event(&event);
1214 event.handled()
1215 }
1216
1217 fn release(slider: &MaterialRangeSlider, window: &Window, pointer: &Rc<Pointer>, point: Point) {
1218 let event = PointerReleasedEventArgs::new(
1219 slider.to_ref(),
1220 pointer.clone(),
1221 window,
1222 point,
1223 2,
1224 PointerPointProperties::new(
1225 RawInputModifiers::NONE,
1226 PointerUpdateKind::LeftButtonReleased,
1227 ),
1228 KeyModifiers::NONE,
1229 MouseButton::Left,
1230 );
1231 slider.raise_event(&event);
1232 }
1233
1234 #[test]
1235 fn native_geometry_insets_and_logical_fraction_round_trip_in_both_directions() {
1236 for orientation in [Orientation::Horizontal, Orientation::Vertical] {
1237 let size = if orientation == Orientation::Horizontal {
1238 Size::new(240.0, 48.0)
1239 } else {
1240 Size::new(48.0, 240.0)
1241 };
1242 for reversed in [false, true] {
1243 let g = Geometry::new(size, orientation, reversed, 0.2, 0.8, false, 4.0, 4.0);
1244 assert_eq!(2.0, g.inset);
1245 assert_eq!(240.0, g.extent);
1246 for fraction in [0.0, 0.2, 0.5, 0.8, 1.0] {
1247 let p = if g.vertical {
1248 Point::new(24.0, g.coordinate(fraction))
1249 } else {
1250 Point::new(g.coordinate(fraction), 24.0)
1251 };
1252 assert!((g.fraction_at(p) - fraction).abs() < 1e-12);
1253 }
1254 }
1255 }
1256 }
1257
1258 #[test]
1259 fn discrete_steps_include_endpoints_and_nonzero_minimum() {
1260 assert_eq!(40.0, snap(41.0, 10.0, 110.0, 9));
1261 assert_eq!(50.0, snap(45.0, 10.0, 110.0, 9));
1262 assert_eq!(10.0, snap(-500.0, 10.0, 110.0, 9));
1263 assert_eq!(110.0, snap(500.0, 10.0, 110.0, 9));
1264 assert_eq!(41.0, snap(41.0, 10.0, 110.0, 0));
1265 assert_eq!(10.0, snap(500.0, 10.0, 10.0, 9));
1266 }
1267
1268 #[test]
1269 fn discrete_corner_inset_is_visual_while_pointer_values_remain_linear() {
1270 let g = Geometry::new(
1271 Size::new(240.0, 48.0),
1272 Orientation::Horizontal,
1273 false,
1274 0.2,
1275 0.8,
1276 true,
1277 4.0,
1278 4.0,
1279 );
1280 assert_eq!(2.0, g.coordinate(0.0));
1281 assert_eq!(238.0, g.coordinate(1.0));
1282 assert_eq!(54.0, g.coordinate(0.2)); assert!((g.fraction_at(Point::new(49.2, 24.0)) - 0.2).abs() < 1e-12);
1284 let pressed = Geometry::new(
1285 Size::new(240.0, 48.0),
1286 Orientation::Horizontal,
1287 true,
1288 0.0,
1289 1.0,
1290 false,
1291 2.0,
1292 4.0,
1293 );
1294 assert_eq!(239.0, pressed.start);
1295 assert_eq!(2.0, pressed.end);
1296 }
1297
1298 #[test]
1299 fn endpoint_properties_enforce_order_and_recoerce_when_range_changes() {
1300 let _app = crate::tests::support::start_themed_application();
1301 let slider = MaterialRangeSlider::new();
1302 slider.set_range_start(20.0);
1303 slider.set_range_end(80.0);
1304 slider.set_range_start(90.0);
1305 assert_eq!(80.0, slider.range_start());
1306 slider.set_range_end(10.0);
1307 assert_eq!(80.0, slider.range_end());
1308 slider.set_maximum(40.0);
1309 assert_eq!((40.0, 40.0), (slider.range_start(), slider.range_end()));
1310 slider.set_minimum(50.0);
1311 assert_eq!(
1312 (50.0, 50.0, 50.0),
1313 (slider.maximum(), slider.range_start(), slider.range_end())
1314 );
1315 }
1316
1317 #[test]
1318 fn compiled_template_positions_two_real_handles_and_keeps_touch_cells() {
1319 let _app = crate::tests::support::start_themed_application();
1320 let (slider, window) = widget();
1321 for role in [MaterialRangeThumb::Start, MaterialRangeThumb::End] {
1322 let thumb = slider.thumb(role).expect("compiled thumb");
1323 assert_eq!(Size::new(48.0, 48.0), thumb.bounds().size());
1324 assert!(thumb.focusable());
1325 let handle = thumb
1326 .get_visual_descendants()
1327 .find(|node| node.name().as_deref() == Some("PART_Handle"))
1328 .unwrap()
1329 .cast::<Border>()
1330 .unwrap();
1331 assert_eq!(Size::new(4.0, 44.0), handle.bounds().size());
1332 }
1333 let a = slider.thumb(MaterialRangeThumb::Start).unwrap();
1334 let b = slider.thumb(MaterialRangeThumb::End).unwrap();
1335 assert!(a.bounds().x < b.bounds().x);
1336 slider.set_flow_direction(FlowDirection::RightToLeft);
1337 window.update_layout();
1338 assert!(a.bounds().x > b.bounds().x);
1339 window.close();
1340 }
1341
1342 #[test]
1343 fn keyboard_steps_rtl_page_and_bounds_match_native_range_contract() {
1344 let _app = crate::tests::support::start_themed_application();
1345 let (slider, window) = widget();
1346 let start = slider.thumb(MaterialRangeThumb::Start).unwrap();
1347 assert!(key(&start, Key::Right, true));
1348 assert_eq!(21.0, slider.range_start());
1349 assert!(
1350 !key(&start, Key::Up, true),
1351 "horizontal range slider must leave vertical arrows to its host"
1352 );
1353 slider.set_flow_direction(FlowDirection::RightToLeft);
1354 assert!(key(&start, Key::Right, true));
1355 assert_eq!(20.0, slider.range_start());
1356 slider.set_steps(9);
1357 assert!(key(&start, Key::Left, true));
1358 assert_eq!(30.0, slider.range_start());
1359 assert!(key(&start, Key::End, true));
1360 assert_eq!(80.0, slider.range_start());
1361 let end = slider.thumb(MaterialRangeThumb::End).unwrap();
1362 assert!(key(&end, Key::Home, true));
1363 assert_eq!(80.0, slider.range_end());
1364 assert!(key(&end, Key::PageUp, true));
1365 assert_eq!(90.0, slider.range_end());
1366 window.close();
1367 }
1368
1369 #[test]
1370 fn each_thumb_has_its_own_named_bounded_range_provider_and_disabled_policy() {
1371 let _app = crate::tests::support::start_themed_application();
1372 let (slider, window) = widget();
1373 slider.set_steps(9);
1374 slider.set_start_label("Minimum price");
1375 slider.set_end_label("Maximum price");
1376 let start = slider.thumb(MaterialRangeThumb::Start).unwrap();
1377 let end = slider.thumb(MaterialRangeThumb::End).unwrap();
1378 let start_peer = ControlAutomationPeer::create_peer_for_element(&start);
1379 let end_peer = ControlAutomationPeer::create_peer_for_element(&end);
1380 assert_eq!(
1381 AutomationControlType::Slider,
1382 start_peer.get_automation_control_type()
1383 );
1384 assert_eq!("Minimum price", start_peer.get_name());
1385 assert_eq!("Maximum price", end_peer.get_name());
1386 let first = start_peer
1387 .get_provider::<dyn IRangeValueProvider>()
1388 .unwrap();
1389 let second = end_peer.get_provider::<dyn IRangeValueProvider>().unwrap();
1390 assert_eq!(
1391 (0.0, 80.0, 20.0),
1392 (first.minimum(), first.maximum(), first.value())
1393 );
1394 assert_eq!(
1395 (20.0, 100.0, 80.0),
1396 (second.minimum(), second.maximum(), second.value())
1397 );
1398 first.set_value(47.0).unwrap();
1399 assert_eq!(50.0, slider.range_start());
1400 assert_eq!(50.0, second.minimum());
1401 second.set_value(30.0).unwrap();
1402 assert_eq!(50.0, slider.range_end());
1403 slider.set_is_read_only(true);
1404 assert!(first.is_read_only());
1405 assert!(first.set_value(10.0).is_err());
1406 slider.set_is_read_only(false);
1407 slider.set_is_enabled(false);
1408 assert!(first.set_value(10.0).is_err());
1409 assert_eq!(50.0, slider.range_start());
1410 window.close();
1411 }
1412
1413 #[test]
1414 fn mouse_drag_selects_nearest_thumb_captures_and_cannot_cross() {
1415 let _app = crate::tests::support::start_themed_application();
1416 let (slider, window) = widget();
1417 let pointer = Pointer::new(Pointer::get_next_free_id(), PointerType::Mouse, true);
1418 let initial = point(&slider, &window, 0.25, 24.0);
1419 press(&slider, &window, &pointer, initial);
1420 assert_eq!(25.0, slider.range_start());
1421 assert!(pointer.captured().is_some());
1422 let end = point(&slider, &window, 0.95, 24.0);
1423 assert!(moved(&slider, &window, &pointer, end));
1424 assert_eq!(80.0, slider.range_start());
1425 release(&slider, &window, &pointer, end);
1426 assert!(pointer.captured().is_none());
1427 assert!(slider.drag.borrow().is_none());
1428 window.close();
1429 }
1430
1431 #[test]
1432 fn touch_cross_axis_drag_yields_to_scroll_and_never_changes_value() {
1433 let _app = crate::tests::support::start_themed_application();
1434 let (slider, window) = widget();
1435 let pointer = Pointer::new(Pointer::get_next_free_id(), PointerType::Touch, true);
1436 press(
1437 &slider,
1438 &window,
1439 &pointer,
1440 point(&slider, &window, 0.25, 24.0),
1441 );
1442 assert_eq!(20.0, slider.range_start());
1443 assert!(!moved(
1444 &slider,
1445 &window,
1446 &pointer,
1447 point(&slider, &window, 0.25, 50.0)
1448 ));
1449 assert!(slider.drag.borrow().is_none());
1450 assert!(pointer.captured().is_none());
1451 assert_eq!(20.0, slider.range_start());
1452 window.close();
1453 }
1454
1455 #[test]
1456 fn coincident_touch_handles_choose_drag_direction_and_release_commits_once() {
1457 let _app = crate::tests::support::start_themed_application();
1458 let (slider, window) = widget();
1459 slider.set_range_start(50.0);
1460 slider.set_range_end(50.0);
1461 let count = Rc::new(Cell::new(0));
1462 let counter = count.clone();
1463 slider.add_handler(
1464 MaterialRangeSlider::value_change_finished_event(),
1465 move |_, _| counter.set(counter.get() + 1),
1466 );
1467 let pointer = Pointer::new(Pointer::get_next_free_id(), PointerType::Touch, true);
1468 press(
1469 &slider,
1470 &window,
1471 &pointer,
1472 point(&slider, &window, 0.5, 24.0),
1473 );
1474 assert!(moved(
1475 &slider,
1476 &window,
1477 &pointer,
1478 point(&slider, &window, 0.3, 24.0)
1479 ));
1480 assert_eq!((30.0, 50.0), (slider.range_start(), slider.range_end()));
1481 release(
1482 &slider,
1483 &window,
1484 &pointer,
1485 point(&slider, &window, 0.3, 24.0),
1486 );
1487 assert_eq!(1, count.get());
1488 window.close();
1489 }
1490
1491 #[test]
1492 fn detaching_captured_range_releases_pointer_and_pressed_state() {
1493 let _app = crate::tests::support::start_themed_application();
1494 let (slider, window) = widget();
1495 let pointer = Pointer::new(Pointer::get_next_free_id(), PointerType::Mouse, true);
1496 press(
1497 &slider,
1498 &window,
1499 &pointer,
1500 point(&slider, &window, 0.3, 24.0),
1501 );
1502 assert!(pointer.captured().is_some());
1503 window.set_content(None);
1504 window.update_layout();
1505 assert!(pointer.captured().is_none());
1506 assert!(slider.drag.borrow().is_none());
1507 assert!(
1508 !slider
1509 .thumb(MaterialRangeThumb::Start)
1510 .unwrap()
1511 .classes()
1512 .contains(":pressed")
1513 );
1514 window.close();
1515 }
1516
1517 #[test]
1518 fn vertical_extension_rotates_handle_and_keyboard_geometry() {
1519 let _app = crate::tests::support::start_themed_application();
1520 let (slider, window) = widget();
1521 slider.set_orientation(Orientation::Vertical);
1522 slider.set_width(48.0);
1523 slider.set_height(240.0);
1524 window.update_layout();
1525 let start = slider.thumb(MaterialRangeThumb::Start).unwrap();
1526 let end = slider.thumb(MaterialRangeThumb::End).unwrap();
1527 assert!(start.bounds().y > end.bounds().y);
1528 let handle = start
1529 .get_visual_descendants()
1530 .find(|node| node.name().as_deref() == Some("PART_Handle"))
1531 .unwrap()
1532 .cast::<Border>()
1533 .unwrap();
1534 assert_eq!(Size::new(44.0, 4.0), handle.bounds().size());
1535 assert!(key(&start, Key::Up, true));
1536 assert_eq!(21.0, slider.range_start());
1537 assert!(!key(&start, Key::Left, true));
1538 window.close();
1539 }
1540}