1use azul_core::{
15 callbacks::{CoreCallbackData, Update},
16 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
17 refany::RefAny,
18};
19use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
20use azul_css::{
21 props::{
22 basic::{color::ColorU, *},
23 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutWidth, LayoutHeight, LayoutMarginLeft},
24 property::{CssProperty, *},
25 style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleCursor},
26 },
27 impl_option_inner, AzString,
28};
29
30use crate::callbacks::{Callback, CallbackInfo};
31
32static SLIDER_TRACK_CLASS: &[IdOrClass] =
33 &[Class(AzString::from_const_str("__azul-native-slider"))];
34static SLIDER_THUMB_CLASS: &[IdOrClass] =
35 &[Class(AzString::from_const_str("__azul-native-slider-thumb"))];
36
37pub type SliderOnValueChangeCallbackType =
39 extern "C" fn(RefAny, CallbackInfo, SliderState) -> Update;
40impl_widget_callback!(
41 SliderOnValueChange,
42 OptionSliderOnValueChange,
43 SliderOnValueChangeCallback,
44 SliderOnValueChangeCallbackType
45);
46
47azul_core::impl_managed_callback! {
48 wrapper: SliderOnValueChangeCallback,
49 info_ty: CallbackInfo,
50 return_ty: Update,
51 default_ret: Update::DoNothing,
52 invoker_static: SLIDER_ON_VALUE_CHANGE_INVOKER,
53 invoker_ty: AzSliderOnValueChangeCallbackInvoker,
54 thunk_fn: az_slider_on_value_change_callback_thunk,
55 setter_fn: AzApp_setSliderOnValueChangeCallbackInvoker,
56 from_handle_fn: AzSliderOnValueChangeCallback_createFromHostHandle,
57 extra_args: [ state: SliderState ],
58}
59
60#[derive(Debug, Clone, PartialEq)]
62#[repr(C)]
63pub struct Slider {
64 pub slider_state: SliderStateWrapper,
65 pub track_style: CssPropertyWithConditionsVec,
67 pub thumb_style: CssPropertyWithConditionsVec,
69}
70
71#[derive(Debug, Default, Clone, PartialEq)]
72#[repr(C)]
73pub struct SliderStateWrapper {
74 pub on_value_change: OptionSliderOnValueChange,
76 pub inner: SliderState,
78 pub dragging: bool,
81}
82
83#[derive(Debug, Copy, Clone, PartialEq)]
85#[repr(C)]
86pub struct SliderState {
87 pub value: f32,
89 pub min: f32,
91 pub max: f32,
93}
94
95impl Default for SliderState {
96 fn default() -> Self {
97 Self {
98 value: 0.0,
99 min: 0.0,
100 max: 100.0,
101 }
102 }
103}
104
105const TRACK_WIDTH: isize = 200;
107const TRACK_HEIGHT: isize = 16;
108const TRACK_RADIUS: isize = 8;
109const THUMB_SIZE: isize = 16;
110const THUMB_RADIUS: isize = 8;
111
112const RAIL_COLOR: ColorU = ColorU {
115 r: 204,
116 g: 204,
117 b: 204,
118 a: 255,
119};
120const THUMB_COLOR: ColorU = ColorU {
122 r: 13,
123 g: 110,
124 b: 253,
125 a: 255,
126};
127
128const RAIL_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(RAIL_COLOR)];
129const RAIL_BG: StyleBackgroundContentVec =
130 StyleBackgroundContentVec::from_const_slice(RAIL_BG_ITEMS);
131const THUMB_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(THUMB_COLOR)];
132const THUMB_BG: StyleBackgroundContentVec =
133 StyleBackgroundContentVec::from_const_slice(THUMB_BG_ITEMS);
134
135static SLIDER_TRACK_STYLE: &[CssPropertyWithConditions] = &[
137 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
138 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
139 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
140 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Center)),
141 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
142 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(TRACK_WIDTH))),
143 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
144 TRACK_HEIGHT,
145 ))),
146 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
147 StyleBorderTopLeftRadius::const_px(TRACK_RADIUS),
148 )),
149 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
150 StyleBorderTopRightRadius::const_px(TRACK_RADIUS),
151 )),
152 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
153 StyleBorderBottomLeftRadius::const_px(TRACK_RADIUS),
154 )),
155 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
156 StyleBorderBottomRightRadius::const_px(TRACK_RADIUS),
157 )),
158 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
159 CssPropertyWithConditions::simple(CssProperty::const_background_content(RAIL_BG)),
160];
161
162fn value_to_fraction(value: f32, min: f32, max: f32) -> f32 {
164 if max <= min {
165 0.0
166 } else {
167 ((value - min) / (max - min)).clamp(0.0, 1.0)
168 }
169}
170
171#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn build_thumb_style(fraction: f32) -> CssPropertyWithConditionsVec {
175 const MAX_ENCODABLE_PX: isize = isize::MAX / 1000;
186 let margin = ((fraction * (TRACK_WIDTH - THUMB_SIZE) as f32).round() as isize)
187 .clamp(-MAX_ENCODABLE_PX, MAX_ENCODABLE_PX);
188 CssPropertyWithConditionsVec::from_vec(alloc::vec![
189 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
190 THUMB_SIZE,
191 ))),
192 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
193 THUMB_SIZE,
194 ))),
195 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
196 0,
197 ))),
198 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
199 StyleBorderTopLeftRadius::const_px(THUMB_RADIUS),
200 )),
201 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
202 StyleBorderTopRightRadius::const_px(THUMB_RADIUS),
203 )),
204 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
205 StyleBorderBottomLeftRadius::const_px(THUMB_RADIUS),
206 )),
207 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
208 StyleBorderBottomRightRadius::const_px(THUMB_RADIUS),
209 )),
210 CssPropertyWithConditions::simple(CssProperty::const_background_content(THUMB_BG)),
211 CssPropertyWithConditions::simple(CssProperty::const_margin_left(
212 LayoutMarginLeft::const_px(margin),
213 )),
214 ])
215}
216
217fn clamp_to_range(value: f32, min: f32, max: f32) -> f32 {
223 let (lo, hi) = match (min.is_nan(), max.is_nan()) {
224 (true, true) => return value,
225 (true, false) => (max, max),
226 (false, true) => (min, min),
227 (false, false) if min <= max => (min, max),
228 (false, false) => (max, min),
229 };
230 value.clamp(lo, hi)
231}
232
233impl Slider {
234 #[must_use] pub fn create(value: f32, min: f32, max: f32) -> Self {
236 let value = clamp_to_range(value, min, max);
237 Self {
238 slider_state: SliderStateWrapper {
239 inner: SliderState { value, min, max },
240 ..Default::default()
241 },
242 track_style: CssPropertyWithConditionsVec::from_const_slice(SLIDER_TRACK_STYLE),
243 thumb_style: build_thumb_style(value_to_fraction(value, min, max)),
244 }
245 }
246
247 #[inline]
249 pub fn set_value(&mut self, value: f32) {
250 let min = self.slider_state.inner.min;
251 let max = self.slider_state.inner.max;
252 let value = clamp_to_range(value, min, max);
253 self.slider_state.inner.value = value;
254 self.thumb_style = build_thumb_style(value_to_fraction(value, min, max));
255 }
256
257 #[inline]
259 #[must_use] pub fn with_value(mut self, value: f32) -> Self {
260 self.set_value(value);
261 self
262 }
263
264 #[inline]
265 #[must_use] pub fn swap_with_default(&mut self) -> Self {
266 let mut s = Self::create(0.0, 0.0, 100.0);
267 core::mem::swap(&mut s, self);
268 s
269 }
270
271 #[inline]
272 pub fn set_on_value_change<C: Into<SliderOnValueChangeCallback>>(
273 &mut self,
274 data: RefAny,
275 on_value_change: C,
276 ) {
277 self.slider_state.on_value_change = Some(SliderOnValueChange {
278 callback: on_value_change.into(),
279 refany: data,
280 })
281 .into();
282 }
283
284 #[inline]
285 #[must_use] pub fn with_on_value_change<C: Into<SliderOnValueChangeCallback>>(
286 mut self,
287 data: RefAny,
288 on_value_change: C,
289 ) -> Self {
290 self.set_on_value_change(data, on_value_change);
291 self
292 }
293
294 #[inline]
295 #[must_use] pub fn dom(self) -> Dom {
296 use azul_core::{
297 callbacks::CoreCallback,
298 dom::{EventFilter, HoverEventFilter},
299 refany::OptionRefAny,
300 };
301
302 let state = RefAny::new(self.slider_state);
306 let mk = |event: EventFilter, cb: usize| CoreCallbackData {
307 event,
308 callback: CoreCallback {
309 cb,
310 ctx: OptionRefAny::None,
311 },
312 refany: state.clone(),
313 };
314 let callbacks = vec![
315 mk(
316 EventFilter::Hover(HoverEventFilter::MouseDown),
317 on_slider_pointer_down as usize,
318 ),
319 mk(
320 EventFilter::Hover(HoverEventFilter::MouseOver),
321 on_slider_pointer_move as usize,
322 ),
323 mk(
324 EventFilter::Hover(HoverEventFilter::MouseUp),
325 on_slider_pointer_up as usize,
326 ),
327 mk(
328 EventFilter::Hover(HoverEventFilter::MouseLeave),
329 on_slider_pointer_up as usize,
330 ),
331 mk(
332 EventFilter::Hover(HoverEventFilter::TouchStart),
333 on_slider_pointer_down as usize,
334 ),
335 mk(
336 EventFilter::Hover(HoverEventFilter::TouchMove),
337 on_slider_pointer_move as usize,
338 ),
339 mk(
340 EventFilter::Hover(HoverEventFilter::TouchEnd),
341 on_slider_pointer_up as usize,
342 ),
343 ];
344
345 Dom::create_div()
346 .with_ids_and_classes(IdOrClassVec::from_const_slice(SLIDER_TRACK_CLASS))
347 .with_css_props(self.track_style)
348 .with_callbacks(callbacks.into())
349 .with_tab_index(TabIndex::Auto)
350 .with_children(
351 vec![Dom::create_div()
352 .with_ids_and_classes(IdOrClassVec::from_const_slice(SLIDER_THUMB_CLASS))
353 .with_css_props(self.thumb_style)]
354 .into(),
355 )
356 }
357}
358
359impl Default for Slider {
360 fn default() -> Self {
361 Self::create(0.0, 0.0, 100.0)
362 }
363}
364
365#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn apply_cursor_value(slider: &mut SliderStateWrapper, info: &mut CallbackInfo) -> Update {
369 let Some(pos) = info.get_cursor_relative_to_node().into_option() else {
370 return Update::DoNothing;
371 };
372 let width = info
374 .get_hit_node_rect()
375 .map(|r| r.size.width)
376 .filter(|w| *w > 0.0)
377 .unwrap_or(TRACK_WIDTH as f32);
378
379 let fraction = (pos.x / width).clamp(0.0, 1.0);
380 let min = slider.inner.min;
381 let max = slider.inner.max;
382 slider.inner.value = fraction.mul_add(max - min, min);
383
384 let track_id = info.get_hit_node();
386 if let Some(thumb_id) = info.get_first_child(track_id) {
387 let margin = (fraction * (width - THUMB_SIZE as f32)).round() as isize;
388 info.set_css_property(
389 thumb_id,
390 CssProperty::const_margin_left(LayoutMarginLeft::const_px(margin)),
391 );
392 }
393
394 let inner = slider.inner;
395 match slider.on_value_change.as_mut() {
396 Some(SliderOnValueChange { callback, refany }) => (callback.cb)(refany.clone(), *info, inner),
397 None => Update::DoNothing,
398 }
399}
400
401extern "C" fn on_slider_pointer_down(mut data: RefAny, mut info: CallbackInfo) -> Update {
403 let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() else {
404 return Update::DoNothing;
405 };
406 slider.dragging = true;
407 apply_cursor_value(&mut slider, &mut info)
408}
409
410extern "C" fn on_slider_pointer_move(mut data: RefAny, mut info: CallbackInfo) -> Update {
412 let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() else {
413 return Update::DoNothing;
414 };
415 if !slider.dragging {
416 return Update::DoNothing;
417 }
418 apply_cursor_value(&mut slider, &mut info)
419}
420
421extern "C" fn on_slider_pointer_up(mut data: RefAny, _info: CallbackInfo) -> Update {
423 if let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() {
424 slider.dragging = false;
425 }
426 Update::DoNothing
427}
428
429impl From<Slider> for Dom {
430 fn from(s: Slider) -> Self {
431 s.dom()
432 }
433}
434
435#[cfg(all(test, feature = "std"))]
444#[allow(
445 clippy::float_cmp,
446 clippy::cast_precision_loss,
447 clippy::cast_possible_truncation,
448 clippy::unreadable_literal,
449 clippy::too_many_lines
450)]
451mod autotest_generated {
452 use std::{
453 collections::{BTreeMap, HashMap},
454 mem::discriminant,
455 panic::{catch_unwind, AssertUnwindSafe},
456 sync::{Arc, Mutex},
457 };
458
459 use azul_core::{
460 dom::{
461 DomId, DomNodeId, EventFilter, FormattingContext, HoverEventFilter, NodeId, NodeType,
462 },
463 geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition},
464 gl::OptionGlContextPtr,
465 hit_test::ScrollPosition,
466 refany::OptionRefAny,
467 resources::RendererResources,
468 styled_dom::{NodeHierarchyItemId, StyledDom},
469 window::{MonitorVec, RawWindowHandle},
470 };
471 use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
472 use rust_fontconfig::FcFontCache;
473
474 use super::*;
475 #[cfg(feature = "icu")]
476 use crate::icu::IcuLocalizerHandle;
477 use crate::{
478 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
479 solver3::{
480 display_list::DisplayList,
481 geometry::PackedBoxProps,
482 layout_tree::{LayoutNodeHot, LayoutTree},
483 },
484 window::{DomLayoutResult, LayoutWindow},
485 window_state::FullWindowState,
486 };
487
488 const TRAVEL: f32 = (TRACK_WIDTH - THUMB_SIZE) as f32; const DESIGN_WIDTH: f32 = TRACK_WIDTH as f32; const REACHABLE_FRACTIONS: [f32; 10] = [
505 0.0,
506 -0.0,
507 f32::MIN_POSITIVE,
508 f32::EPSILON,
509 0.001,
510 0.25,
511 0.5,
512 0.75,
513 1.0,
514 f32::NAN,
515 ];
516
517 const SANE_RANGES: [(f32, f32); 8] = [
519 (0.0, 100.0),
520 (0.0, 1.0),
521 (-100.0, -50.0),
522 (-50.0, 50.0),
523 (0.0, 0.0),
524 (-7.5, 7.5),
525 (1.0, 1.0e9),
526 (-1.0e9, 1.0e9),
527 ];
528
529 const DEGENERATE_RANGES: [(f32, f32); 6] = [
533 (100.0, 0.0),
534 (1.0, -1.0),
535 (f32::NAN, 100.0),
536 (0.0, f32::NAN),
537 (f32::NAN, f32::NAN),
538 (f32::INFINITY, f32::NEG_INFINITY),
539 ];
540
541 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
546 v.as_ref().iter().map(|p| p.property.clone()).collect()
547 }
548
549 fn px(pv: PixelValue) -> f32 {
553 assert_eq!(
554 pv.metric,
555 SizeMetric::Px,
556 "slider geometry must be absolute px, got {:?}",
557 pv.metric,
558 );
559 pv.number.get()
560 }
561
562 fn margin_left(v: &CssPropertyWithConditionsVec) -> Option<f32> {
564 v.as_ref().iter().find_map(|p| match &p.property {
565 CssProperty::MarginLeft(m) => m.get_property().map(|m| px(m.inner)),
566 _ => None,
567 })
568 }
569
570 fn width_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
571 v.as_ref().iter().find_map(|p| match &p.property {
572 CssProperty::Width(w) => match w.get_property() {
573 Some(LayoutWidth::Px(pv)) => Some(px(*pv)),
574 _ => None,
575 },
576 _ => None,
577 })
578 }
579
580 fn height_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
581 v.as_ref().iter().find_map(|p| match &p.property {
582 CssProperty::Height(h) => match h.get_property() {
583 Some(LayoutHeight::Px(pv)) => Some(px(*pv)),
584 _ => None,
585 },
586 _ => None,
587 })
588 }
589
590 fn background(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
591 v.as_ref().iter().find_map(|p| match &p.property {
592 CssProperty::BackgroundContent(b) => match b.get_property()?.as_ref().first()? {
593 StyleBackgroundContent::Color(c) => Some(*c),
594 _ => None,
595 },
596 _ => None,
597 })
598 }
599
600 fn thumb_margin(s: &Slider) -> f32 {
602 margin_left(&s.thumb_style).expect("the thumb style must declare a margin-left")
603 }
604
605 fn classes(dom: &Dom) -> Vec<String> {
606 dom.root
607 .get_ids_and_classes()
608 .as_ref()
609 .iter()
610 .filter_map(|c| match c {
611 IdOrClass::Class(s) => Some(s.as_str().to_string()),
612 IdOrClass::Id(_) => None,
613 })
614 .collect()
615 }
616
617 fn inline_properties(dom: &Dom) -> Vec<CssProperty> {
618 dom.root
619 .style
620 .iter_inline_properties()
621 .map(|(p, _)| p.clone())
622 .collect()
623 }
624
625 fn node(idx: usize) -> DomNodeId {
630 DomNodeId {
631 dom: DomId::ROOT_ID,
632 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx))),
633 }
634 }
635
636 fn node_none() -> DomNodeId {
637 DomNodeId {
638 dom: DomId::ROOT_ID,
639 node: NodeHierarchyItemId::NONE,
640 }
641 }
642
643 fn unlaid(styled_dom: StyledDom) -> DomLayoutResult {
648 DomLayoutResult {
649 styled_dom,
650 layout_tree: LayoutTree {
651 nodes: Vec::new(),
652 warm: Vec::new(),
653 cold: Vec::new(),
654 root: 0,
655 dom_to_layout: BTreeMap::new(),
656 children_arena: Vec::new(),
657 children_offsets: Vec::new(),
658 subtree_needs_intrinsic: Vec::new(),
659 },
660 calculated_positions: Vec::new(),
661 viewport: LogicalRect::zero(),
662 display_list: DisplayList::default(),
663 scroll_ids: HashMap::new(),
664 scroll_id_to_node_id: HashMap::new(),
665 }
666 }
667
668 fn laid_out_at(styled_dom: StyledDom, track: LogicalSize) -> DomLayoutResult {
671 let hot = |dom_node: usize, size: LogicalSize, parent: Option<usize>| LayoutNodeHot {
672 box_props: PackedBoxProps::default(),
673 dom_node_id: Some(NodeId::new(dom_node)),
674 used_size: Some(size),
675 formatting_context: FormattingContext::Flex,
676 parent,
677 };
678 let mut dom_to_layout = BTreeMap::new();
679 dom_to_layout.insert(NodeId::new(0), vec![0usize]);
680 dom_to_layout.insert(NodeId::new(1), vec![1usize]);
681
682 let mut result = unlaid(styled_dom);
683 result.layout_tree.nodes = vec![
684 hot(0, track, None),
685 hot(1, LogicalSize::new(THUMB_SIZE as f32, THUMB_SIZE as f32), Some(0)),
686 ];
687 result.layout_tree.dom_to_layout = dom_to_layout;
688 result.calculated_positions = vec![LogicalPosition::zero(), LogicalPosition::zero()];
689 result
690 }
691
692 fn with_info<R>(
696 layout: DomLayoutResult,
697 hit: DomNodeId,
698 cursor: OptionLogicalPosition,
699 f: impl FnOnce(&mut CallbackInfo) -> R,
700 ) -> (R, Vec<CallbackChange>) {
701 let mut layout_window =
702 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
703 layout_window.layout_results.insert(DomId::ROOT_ID, layout);
704
705 let renderer_resources = RendererResources::default();
706 let previous_window_state: Option<FullWindowState> = None;
707 let current_window_state = FullWindowState::default();
708 let gl_context = OptionGlContextPtr::None;
709 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
710 BTreeMap::new();
711 let window_handle = RawWindowHandle::Unsupported;
712 let system_callbacks = ExternalSystemCallbacks::rust_internal();
713
714 let ref_data = CallbackInfoRefData {
715 layout_window: &layout_window,
716 renderer_resources: &renderer_resources,
717 previous_window_state: &previous_window_state,
718 current_window_state: ¤t_window_state,
719 gl_context: &gl_context,
720 current_scroll_manager: &scroll_states,
721 current_window_handle: &window_handle,
722 system_callbacks: &system_callbacks,
723 system_style: Arc::new(azul_css::system::SystemStyle::default()),
724 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
725 #[cfg(feature = "icu")]
726 icu_localizer: IcuLocalizerHandle::default(),
727 ctx: OptionRefAny::None,
728 };
729
730 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
731 let mut info = CallbackInfo::new(
732 &ref_data,
733 &changes,
734 hit,
735 cursor,
736 OptionLogicalPosition::None,
737 );
738
739 let out = f(&mut info);
740 let pushed = info.take_changes();
741 (out, pushed)
742 }
743
744 fn wired(slider: Slider) -> (StyledDom, RefAny) {
749 let dom = slider.dom();
750 let state = dom.root.callbacks.as_ref()[0].refany.clone();
751 (StyledDom::create_from_dom(dom), state)
752 }
753
754 fn cursor(x: f32, y: f32) -> OptionLogicalPosition {
755 OptionLogicalPosition::Some(LogicalPosition::new(x, y))
756 }
757
758 fn deliver(
760 slider: Slider,
761 state: &RefAny,
762 hit: DomNodeId,
763 at: OptionLogicalPosition,
764 track: Option<LogicalSize>,
765 kind: extern "C" fn(RefAny, CallbackInfo) -> Update,
766 ) -> (Update, Vec<CallbackChange>) {
767 let styled = StyledDom::create_from_dom(slider.dom());
768 let layout = match track {
769 Some(t) => laid_out_at(styled, t),
770 None => unlaid(styled),
771 };
772 with_info(layout, hit, at, |info| kind(state.clone(), *info))
773 }
774
775 fn press_at(slider: Slider, x: f32) -> (Update, Vec<CallbackChange>, SliderStateWrapper) {
777 let (styled, state) = wired(slider);
778 let (update, changes) = with_info(unlaid(styled), node(0), cursor(x, 8.0), |info| {
779 on_slider_pointer_down(state.clone(), *info)
780 });
781 (update, changes, read_state(&state))
782 }
783
784 fn read_state(state: &RefAny) -> SliderStateWrapper {
785 let mut state = state.clone();
786 let wrapper = state
787 .downcast_ref::<SliderStateWrapper>()
788 .expect("the widget state changed type");
789 wrapper.clone()
790 }
791
792 fn pushed_margins(changes: &[CallbackChange]) -> Vec<(NodeId, f32)> {
794 changes
795 .iter()
796 .filter_map(|c| match c {
797 CallbackChange::ChangeNodeCssProperties {
798 node_id, properties, ..
799 } => properties
800 .as_ref()
801 .iter()
802 .find_map(|p| match p {
803 CssProperty::MarginLeft(m) => m.get_property().map(|m| px(m.inner)),
804 _ => None,
805 })
806 .map(|m| (*node_id, m)),
807 _ => None,
808 })
809 .collect()
810 }
811
812 fn expected_value(x: f32, width: f32, min: f32, max: f32) -> f32 {
816 (x / width).clamp(0.0, 1.0).mul_add(max - min, min)
817 }
818
819 #[derive(Debug, Clone, Default, PartialEq)]
827 struct ValueLog {
828 seen: Vec<SliderState>,
829 }
830
831 extern "C" fn record_value(mut data: RefAny, _: CallbackInfo, state: SliderState) -> Update {
832 if let Some(mut log) = data.downcast_mut::<ValueLog>() {
833 log.seen.push(state);
834 }
835 Update::RefreshDom
836 }
837
838 extern "C" fn value_do_nothing(_: RefAny, _: CallbackInfo, _: SliderState) -> Update {
839 Update::DoNothing
840 }
841
842 extern "C" fn value_refresh_all(_: RefAny, _: CallbackInfo, _: SliderState) -> Update {
843 Update::RefreshDomAllWindows
844 }
845
846 extern "C" fn generic_shaped(_: RefAny, _: CallbackInfo) -> Update {
849 Update::DoNothing
850 }
851
852 fn log_refany() -> RefAny {
853 RefAny::new(ValueLog::default())
854 }
855
856 fn read_log(probe: &RefAny) -> ValueLog {
857 let mut probe = probe.clone();
858 let log = probe
859 .downcast_ref::<ValueLog>()
860 .expect("the user payload changed type");
861 log.clone()
862 }
863
864 fn hook_ptr(s: &Slider) -> Option<usize> {
865 s.slider_state
866 .on_value_change
867 .as_ref()
868 .map(|h| h.callback.cb as *const () as usize)
869 }
870
871 #[test]
876 fn value_to_fraction_maps_the_endpoints_and_the_midpoint_exactly() {
877 assert_eq!(value_to_fraction(0.0, 0.0, 100.0), 0.0);
878 assert_eq!(value_to_fraction(100.0, 0.0, 100.0), 1.0);
879 assert_eq!(value_to_fraction(50.0, 0.0, 100.0), 0.5);
880 assert_eq!(value_to_fraction(25.0, 0.0, 100.0), 0.25);
881 assert_eq!(value_to_fraction(150.0, 100.0, 200.0), 0.5);
883 assert_eq!(value_to_fraction(-75.0, -100.0, -50.0), 0.5);
884 }
885
886 #[test]
887 fn value_to_fraction_clamps_instead_of_letting_the_thumb_leave_the_rail() {
888 for (value, min, max) in [
889 (200.0_f32, 0.0_f32, 100.0_f32),
890 (-1.0, 0.0, 100.0),
891 (1.0e30, 0.0, 1.0),
892 (-1.0e30, 0.0, 1.0),
893 (f32::MAX, -1.0, 1.0),
894 (f32::MIN, -1.0, 1.0),
895 ] {
896 let f = value_to_fraction(value, min, max);
897 assert!(
898 (0.0..=1.0).contains(&f),
899 "value_to_fraction({value}, {min}, {max}) = {f} is outside [0, 1]",
900 );
901 }
902 assert_eq!(value_to_fraction(200.0, 0.0, 100.0), 1.0);
903 assert_eq!(value_to_fraction(-1.0, 0.0, 100.0), 0.0);
904 }
905
906 #[test]
907 fn value_to_fraction_guards_the_zero_width_and_inverted_range() {
908 for (value, min, max) in [
912 (0.0_f32, 0.0_f32, 0.0_f32),
913 (5.0, 5.0, 5.0),
914 (-5.0, -5.0, -5.0),
915 (1.0e9, 7.0, 7.0),
916 (50.0, 100.0, 0.0),
917 (50.0, 1.0, -1.0),
918 ] {
919 assert_eq!(
920 value_to_fraction(value, min, max),
921 0.0,
922 "a degenerate range [{min}, {max}] must pin the thumb left",
923 );
924 }
925 }
926
927 #[test]
928 fn value_to_fraction_is_monotonic_across_the_whole_range() {
929 let mut previous = f32::NEG_INFINITY;
932 for i in -50..=150 {
933 let f = value_to_fraction(i as f32, 0.0, 100.0);
934 assert!(
935 f >= previous,
936 "the fraction went backwards at value = {i} ({f} < {previous})",
937 );
938 previous = f;
939 }
940 assert_eq!(previous, 1.0);
941 }
942
943 #[test]
944 fn value_to_fraction_nan_inputs_do_not_panic_and_stay_nan() {
945 assert!(value_to_fraction(f32::NAN, 0.0, 100.0).is_nan());
949 assert!(value_to_fraction(50.0, f32::NAN, 100.0).is_nan());
952 assert!(value_to_fraction(50.0, 0.0, f32::NAN).is_nan());
953 assert!(value_to_fraction(f32::NAN, f32::NAN, f32::NAN).is_nan());
954 }
955
956 #[test]
957 fn value_to_fraction_never_escapes_the_unit_interval_for_any_input() {
958 let interesting = [
962 0.0_f32,
963 -0.0,
964 1.0,
965 -1.0,
966 50.0,
967 f32::MIN_POSITIVE,
968 f32::EPSILON,
969 f32::MAX,
970 f32::MIN,
971 f32::INFINITY,
972 f32::NEG_INFINITY,
973 f32::NAN,
974 ];
975 for value in interesting {
976 for min in interesting {
977 for max in interesting {
978 let f = value_to_fraction(value, min, max);
979 assert!(
980 f.is_nan() || (0.0..=1.0).contains(&f),
981 "value_to_fraction({value}, {min}, {max}) = {f} escaped [0, 1]",
982 );
983 }
984 }
985 }
986 }
987
988 #[test]
989 fn value_to_fraction_infinite_bounds_produce_a_defined_result() {
990 assert!(value_to_fraction(0.0, f32::NEG_INFINITY, f32::INFINITY).is_nan());
993 assert!(value_to_fraction(-50.0, f32::NEG_INFINITY, 0.0).is_nan());
994 assert_eq!(value_to_fraction(50.0, 0.0, f32::INFINITY), 0.0);
996 assert_eq!(value_to_fraction(f32::INFINITY, 0.0, 100.0), 1.0);
998 assert_eq!(value_to_fraction(f32::NEG_INFINITY, 0.0, 100.0), 0.0);
999 }
1000
1001 #[test]
1002 fn value_to_fraction_survives_a_range_whose_width_overflows_f32() {
1003 for value in [f32::MIN, -1.0e30, 0.0, 1.0e30] {
1009 assert_eq!(
1010 value_to_fraction(value, f32::MIN, f32::MAX),
1011 0.0,
1012 "an f32-wide range should collapse {value} onto the left end",
1013 );
1014 }
1015 assert!(value_to_fraction(f32::MAX, f32::MIN, f32::MAX).is_nan());
1016 assert_eq!(margin_left(&build_thumb_style(f32::NAN)), Some(0.0));
1017 }
1018
1019 #[test]
1020 fn value_to_fraction_is_pure() {
1021 for (min, max) in SANE_RANGES {
1022 for value in [min, max, 0.0, -1.0, 1.0e9] {
1023 let a = value_to_fraction(value, min, max);
1024 let b = value_to_fraction(value, min, max);
1025 assert!(
1026 a == b || (a.is_nan() && b.is_nan()),
1027 "value_to_fraction({value}, {min}, {max}) is not deterministic",
1028 );
1029 }
1030 }
1031 }
1032
1033 #[test]
1038 fn build_thumb_style_endpoints_keep_the_thumb_inside_the_track() {
1039 assert_eq!(margin_left(&build_thumb_style(0.0)), Some(0.0));
1040 assert_eq!(margin_left(&build_thumb_style(1.0)), Some(TRAVEL));
1041 assert_eq!(TRAVEL + THUMB_SIZE as f32, DESIGN_WIDTH);
1044 }
1045
1046 #[test]
1047 fn build_thumb_style_rounds_to_whole_pixels() {
1048 for (fraction, expected) in [
1051 (0.5_f32, 92.0_f32),
1052 (0.25, 46.0),
1053 (0.75, 138.0),
1054 (0.1, 18.0), (0.9, 166.0), (0.001, 0.0), ] {
1058 assert_eq!(
1059 margin_left(&build_thumb_style(fraction)),
1060 Some(expected),
1061 "fraction {fraction} landed on the wrong pixel",
1062 );
1063 }
1064 }
1065
1066 #[test]
1067 fn build_thumb_style_is_monotonic_and_bounded_over_the_unit_interval() {
1068 let mut previous = f32::NEG_INFINITY;
1069 for i in 0..=1000 {
1070 let m = margin_left(&build_thumb_style(i as f32 / 1000.0))
1071 .expect("every thumb style declares a margin-left");
1072 assert!(m >= previous, "the thumb moved backwards at {i}/1000");
1073 assert!(
1074 (0.0..=TRAVEL).contains(&m),
1075 "the thumb left the rail at {i}/1000 (margin {m})",
1076 );
1077 previous = m;
1078 }
1079 }
1080
1081 #[test]
1082 fn build_thumb_style_nan_fraction_pins_the_thumb_left_instead_of_panicking() {
1083 assert_eq!(margin_left(&build_thumb_style(f32::NAN)), Some(0.0));
1087 }
1088
1089 #[test]
1090 fn build_thumb_style_negative_and_signed_zero_fractions_are_deterministic() {
1091 assert_eq!(margin_left(&build_thumb_style(-0.0)), Some(0.0));
1092 assert_eq!(margin_left(&build_thumb_style(-1.0)), Some(-TRAVEL));
1093 assert_eq!(margin_left(&build_thumb_style(-0.5)), Some(-92.0));
1094 }
1095
1096 #[test]
1097 fn build_thumb_style_declares_the_full_thumb_geometry_exactly_once() {
1098 let props = properties(&build_thumb_style(0.5));
1099 assert_eq!(props.len(), 9, "the thumb style changed shape: {props:?}");
1100 let mut seen = Vec::new();
1101 for p in &props {
1102 let d = discriminant(p);
1103 assert!(!seen.contains(&d), "the thumb style declares {p:?} twice");
1104 seen.push(d);
1105 }
1106 assert!(
1109 matches!(props.last(), Some(CssProperty::MarginLeft(_))),
1110 "margin-left must be the last (position-dependent) declaration",
1111 );
1112 }
1113
1114 #[test]
1115 fn build_thumb_style_geometry_is_absolute_px_and_independent_of_the_fraction() {
1116 for fraction in REACHABLE_FRACTIONS {
1117 let v = build_thumb_style(fraction);
1118 assert_eq!(width_px(&v), Some(THUMB_SIZE as f32), "fraction {fraction}");
1119 assert_eq!(height_px(&v), Some(THUMB_SIZE as f32), "fraction {fraction}");
1120 assert_eq!(background(&v), Some(THUMB_COLOR), "fraction {fraction}");
1121 }
1122 }
1123
1124 #[test]
1125 fn build_thumb_style_only_the_margin_depends_on_the_fraction() {
1126 let strip = |f: f32| -> Vec<CssProperty> {
1129 properties(&build_thumb_style(f))
1130 .into_iter()
1131 .filter(|p| !matches!(p, CssProperty::MarginLeft(_)))
1132 .collect()
1133 };
1134 let reference = strip(0.0);
1135 for fraction in REACHABLE_FRACTIONS {
1136 assert_eq!(strip(fraction), reference, "fraction {fraction} restyled the thumb");
1137 }
1138 }
1139
1140 #[test]
1141 fn build_thumb_style_is_pure_for_every_reachable_fraction() {
1142 for fraction in REACHABLE_FRACTIONS {
1143 assert_eq!(
1144 build_thumb_style(fraction),
1145 build_thumb_style(fraction),
1146 "build_thumb_style({fraction}) is not deterministic",
1147 );
1148 }
1149 }
1150
1151 #[test]
1152 fn build_thumb_style_handles_every_fraction_value_to_fraction_can_produce() {
1153 for fraction in REACHABLE_FRACTIONS {
1156 let m = margin_left(&build_thumb_style(fraction))
1157 .expect("every thumb style declares a margin-left");
1158 assert!(
1159 (0.0..=TRAVEL).contains(&m),
1160 "fraction {fraction} put the thumb at {m}, off a {TRAVEL}px rail",
1161 );
1162 }
1163 }
1164
1165 #[test]
1166 fn build_thumb_style_out_of_contract_fractions_must_not_panic() {
1167 let hostile: [f32; 6] = [
1175 1.0e14,
1176 -1.0e14,
1177 1.0e30,
1178 f32::MAX,
1179 f32::INFINITY,
1180 f32::NEG_INFINITY,
1181 ];
1182 let panicked: Vec<f32> = hostile
1183 .iter()
1184 .copied()
1185 .filter(|&f| catch_unwind(AssertUnwindSafe(|| build_thumb_style(f))).is_err())
1186 .collect();
1187 assert!(
1188 panicked.is_empty(),
1189 "build_thumb_style overflows the isize fixed-point encoding and panics \
1190 instead of saturating for: {panicked:?}",
1191 );
1192 }
1193
1194 #[test]
1199 fn create_clamps_the_value_into_the_range() {
1200 assert_eq!(Slider::create(150.0, 0.0, 100.0).slider_state.inner.value, 100.0);
1201 assert_eq!(Slider::create(-50.0, 0.0, 100.0).slider_state.inner.value, 0.0);
1202 assert_eq!(Slider::create(50.0, 0.0, 100.0).slider_state.inner.value, 50.0);
1203 assert_eq!(Slider::create(f32::MAX, -1.0, 1.0).slider_state.inner.value, 1.0);
1204 assert_eq!(Slider::create(f32::MIN, -1.0, 1.0).slider_state.inner.value, -1.0);
1205 }
1206
1207 #[test]
1208 fn create_stores_the_bounds_verbatim() {
1209 for (min, max) in SANE_RANGES {
1210 let s = Slider::create(min, min, max);
1211 assert_eq!(s.slider_state.inner.min, min, "min was rewritten");
1212 assert_eq!(s.slider_state.inner.max, max, "max was rewritten");
1213 }
1214 }
1215
1216 #[test]
1217 fn create_places_the_thumb_where_the_pure_helpers_say_it_belongs() {
1218 for (min, max) in SANE_RANGES {
1222 for value in [min, max, (min + max) / 2.0, min - 1.0, max + 1.0, 0.0] {
1223 let s = Slider::create(value, min, max);
1224 let expected =
1225 margin_left(&build_thumb_style(value_to_fraction(value.clamp(min, max), min, max)));
1226 assert_eq!(
1227 margin_left(&s.thumb_style),
1228 expected,
1229 "create({value}, {min}, {max}) put the thumb in the wrong place",
1230 );
1231 }
1232 }
1233 }
1234
1235 #[test]
1236 fn create_leaves_the_thumb_on_the_rail_for_every_sane_range() {
1237 for (min, max) in SANE_RANGES {
1238 for value in [min, max, (min + max) / 2.0, min - 1.0e9, max + 1.0e9] {
1239 let m = thumb_margin(&Slider::create(value, min, max));
1240 assert!(
1241 (0.0..=TRAVEL).contains(&m),
1242 "create({value}, {min}, {max}) parked the thumb at {m}",
1243 );
1244 }
1245 }
1246 }
1247
1248 #[test]
1249 fn create_with_a_zero_width_range_pins_the_thumb_left() {
1250 for bound in [0.0_f32, 5.0, -5.0, 1.0e9] {
1251 let s = Slider::create(bound, bound, bound);
1252 assert_eq!(s.slider_state.inner.value, bound);
1253 assert_eq!(thumb_margin(&s), 0.0, "a [{bound}, {bound}] range must pin left");
1254 }
1255 }
1256
1257 #[test]
1258 fn create_with_a_nan_value_keeps_the_nan_but_still_parks_the_thumb() {
1259 let s = Slider::create(f32::NAN, 0.0, 100.0);
1263 assert!(s.slider_state.inner.value.is_nan(), "the NaN value was silently rewritten");
1264 assert_eq!(thumb_margin(&s), 0.0);
1265 }
1266
1267 #[test]
1268 fn create_with_infinite_bounds_does_not_panic() {
1269 for (value, min, max) in [
1272 (0.0_f32, f32::NEG_INFINITY, f32::INFINITY),
1273 (f32::INFINITY, 0.0, f32::INFINITY),
1274 (f32::NEG_INFINITY, f32::NEG_INFINITY, 0.0),
1275 (50.0, 0.0, f32::INFINITY),
1276 ] {
1277 let s = Slider::create(value, min, max);
1278 let m = thumb_margin(&s);
1279 assert!(
1280 m.is_finite() && (0.0..=TRAVEL).contains(&m),
1281 "create({value}, {min}, {max}) put the thumb at {m}",
1282 );
1283 }
1284 }
1285
1286 #[test]
1287 fn create_with_a_degenerate_range_must_not_panic() {
1288 let panicked: Vec<(f32, f32)> = DEGENERATE_RANGES
1297 .iter()
1298 .copied()
1299 .filter(|&(min, max)| {
1300 catch_unwind(AssertUnwindSafe(|| Slider::create(0.0, min, max))).is_err()
1301 })
1302 .collect();
1303 assert!(
1304 panicked.is_empty(),
1305 "Slider::create panics (f32::clamp asserts min <= max) instead of \
1306 normalising these ranges: {panicked:?}",
1307 );
1308 }
1309
1310 #[test]
1311 fn create_is_deterministic_and_distinguishes_distinct_values() {
1312 for (min, max) in SANE_RANGES {
1313 assert_eq!(Slider::create(min, min, max), Slider::create(min, min, max));
1314 }
1315 assert_ne!(Slider::create(0.0, 0.0, 100.0), Slider::create(100.0, 0.0, 100.0));
1316 assert_ne!(Slider::create(0.0, 0.0, 100.0), Slider::create(0.0, 0.0, 200.0));
1319 }
1320
1321 #[test]
1322 fn create_installs_no_hook_and_starts_undragged() {
1323 let s = Slider::create(50.0, 0.0, 100.0);
1324 assert!(
1325 s.slider_state.on_value_change.as_ref().is_none(),
1326 "create invented a value-change hook out of nowhere",
1327 );
1328 assert!(!s.slider_state.dragging, "a fresh slider must not be mid-drag");
1329 }
1330
1331 #[test]
1332 fn create_track_style_is_shared_and_value_independent() {
1333 let reference = properties(&Slider::create(0.0, 0.0, 100.0).track_style);
1336 for (min, max) in SANE_RANGES {
1337 assert_eq!(
1338 properties(&Slider::create(max, min, max).track_style),
1339 reference,
1340 "the track style leaked a dependency on [{min}, {max}]",
1341 );
1342 }
1343 assert_eq!(reference.len(), 13, "the track style changed shape");
1344 }
1345
1346 #[test]
1347 fn create_track_geometry_is_absolute_px_and_declared_once() {
1348 let s = Slider::create(50.0, 0.0, 100.0);
1349 assert_eq!(width_px(&s.track_style), Some(DESIGN_WIDTH));
1350 assert_eq!(height_px(&s.track_style), Some(TRACK_HEIGHT as f32));
1351 assert_eq!(background(&s.track_style), Some(RAIL_COLOR));
1352
1353 let props = properties(&s.track_style);
1354 let mut seen = Vec::new();
1355 for p in &props {
1356 let d = discriminant(p);
1357 assert!(!seen.contains(&d), "the track style declares {p:?} twice");
1358 seen.push(d);
1359 }
1360 assert!(props.contains(&CssProperty::const_cursor(StyleCursor::Pointer)));
1364 assert!(props.contains(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))));
1365 }
1366
1367 #[test]
1368 fn default_is_exactly_create_0_0_100() {
1369 assert_eq!(Slider::default(), Slider::create(0.0, 0.0, 100.0));
1370 assert_eq!(Slider::default().slider_state.inner, SliderState::default());
1371 assert_eq!(SliderState::default().min, 0.0);
1372 assert_eq!(SliderState::default().max, 100.0);
1373 assert_eq!(SliderState::default().value, 0.0);
1374 }
1375
1376 #[test]
1381 fn set_value_clamps_and_moves_the_thumb_together() {
1382 let mut s = Slider::create(0.0, 0.0, 100.0);
1383 for (input, expected_value) in [
1384 (50.0_f32, 50.0_f32),
1385 (100.0, 100.0),
1386 (150.0, 100.0),
1387 (-1.0, 0.0),
1388 (f32::INFINITY, 100.0),
1389 (f32::NEG_INFINITY, 0.0),
1390 (0.0, 0.0),
1391 ] {
1392 s.set_value(input);
1393 assert_eq!(s.slider_state.inner.value, expected_value, "set_value({input})");
1394 assert_eq!(
1395 thumb_margin(&s),
1396 margin_left(&build_thumb_style(value_to_fraction(expected_value, 0.0, 100.0)))
1397 .expect("margin"),
1398 "set_value({input}) did not move the thumb with the value",
1399 );
1400 }
1401 }
1402
1403 #[test]
1404 fn set_value_never_touches_the_bounds() {
1405 for (min, max) in SANE_RANGES {
1406 let mut s = Slider::create(min, min, max);
1407 s.set_value(1.0e9);
1408 s.set_value(-1.0e9);
1409 assert_eq!(s.slider_state.inner.min, min);
1410 assert_eq!(s.slider_state.inner.max, max);
1411 }
1412 }
1413
1414 #[test]
1415 fn set_value_round_trips_every_value_inside_the_range() {
1416 let mut s = Slider::create(0.0, 0.0, 100.0);
1419 for i in 0..=100 {
1420 let v = i as f32;
1421 s.set_value(v);
1422 assert_eq!(s.slider_state.inner.value, v, "{v} did not survive set_value");
1423 assert_eq!(
1424 thumb_margin(&s),
1425 (v / 100.0 * TRAVEL).round(),
1426 "{v} landed on the wrong pixel",
1427 );
1428 }
1429 }
1430
1431 #[test]
1432 fn set_value_is_idempotent() {
1433 let mut s = Slider::create(0.0, 0.0, 100.0);
1434 s.set_value(37.5);
1435 let once = s.clone();
1436 s.set_value(37.5);
1437 assert_eq!(s, once, "re-setting the same value changed the widget");
1438 }
1439
1440 #[test]
1441 fn set_value_with_nan_parks_the_thumb_without_panicking() {
1442 let mut s = Slider::create(50.0, 0.0, 100.0);
1443 s.set_value(f32::NAN);
1444 assert!(s.slider_state.inner.value.is_nan());
1445 assert_eq!(thumb_margin(&s), 0.0);
1446 s.set_value(25.0);
1448 assert_eq!(s.slider_state.inner.value, 25.0);
1449 assert_eq!(thumb_margin(&s), 46.0);
1450 }
1451
1452 #[test]
1453 fn set_value_on_a_degenerate_range_must_not_panic() {
1454 let panicked: Vec<(f32, f32)> = DEGENERATE_RANGES
1459 .iter()
1460 .copied()
1461 .filter(|&(min, max)| {
1462 let mut s = Slider::create(0.0, 0.0, 100.0);
1463 s.slider_state.inner.min = min;
1464 s.slider_state.inner.max = max;
1465 catch_unwind(AssertUnwindSafe(move || s.set_value(1.0))).is_err()
1466 })
1467 .collect();
1468 assert!(
1469 panicked.is_empty(),
1470 "Slider::set_value panics (f32::clamp asserts min <= max) on these \
1471 externally-set ranges: {panicked:?}",
1472 );
1473 }
1474
1475 #[test]
1476 fn with_value_is_exactly_set_value() {
1477 for v in [0.0_f32, 50.0, 100.0, 150.0, -1.0, f32::INFINITY] {
1478 let mut expected = Slider::create(0.0, 0.0, 100.0);
1479 expected.set_value(v);
1480 assert_eq!(
1481 Slider::create(0.0, 0.0, 100.0).with_value(v),
1482 expected,
1483 "with_value({v}) diverged from set_value({v})",
1484 );
1485 }
1486 }
1487
1488 #[test]
1489 fn with_value_keeps_the_hook_it_was_handed() {
1490 let s = Slider::create(0.0, 0.0, 100.0)
1493 .with_on_value_change(log_refany(), record_value as SliderOnValueChangeCallbackType)
1494 .with_value(80.0);
1495 assert_eq!(hook_ptr(&s), Some(record_value as *const () as usize));
1496 assert_eq!(s.slider_state.inner.value, 80.0);
1497 }
1498
1499 #[test]
1500 fn with_value_chains_to_the_last_write() {
1501 let s = Slider::create(0.0, 0.0, 100.0)
1502 .with_value(10.0)
1503 .with_value(90.0)
1504 .with_value(45.0);
1505 assert_eq!(s.slider_state.inner.value, 45.0);
1506 assert_eq!(thumb_margin(&s), (0.45 * TRAVEL).round());
1507 }
1508
1509 #[test]
1514 fn swap_with_default_returns_the_old_widget_and_leaves_a_default_behind() {
1515 let mut s = Slider::create(75.0, 50.0, 200.0);
1516 let old = s.swap_with_default();
1517 assert_eq!(old.slider_state.inner.value, 75.0);
1518 assert_eq!(old.slider_state.inner.min, 50.0);
1519 assert_eq!(old.slider_state.inner.max, 200.0);
1520 assert_eq!(s, Slider::default(), "the hole was not filled with a default");
1521 }
1522
1523 #[test]
1524 fn swap_with_default_moves_the_hook_out_with_the_old_widget() {
1525 let mut s = Slider::create(10.0, 0.0, 100.0)
1526 .with_on_value_change(log_refany(), record_value as SliderOnValueChangeCallbackType);
1527 let old = s.swap_with_default();
1528 assert_eq!(hook_ptr(&old), Some(record_value as *const () as usize));
1529 assert_eq!(hook_ptr(&s), None, "the hook survived the swap");
1530 }
1531
1532 #[test]
1533 fn swap_with_default_is_an_involution_when_chained() {
1534 let original = Slider::create(33.0, 0.0, 100.0);
1535 let mut s = original.clone();
1536 let first = s.swap_with_default();
1537 s = first;
1538 assert_eq!(s, original, "swap-then-restore lost state");
1539 }
1540
1541 #[test]
1542 fn swap_with_default_does_not_panic_for_hostile_widgets() {
1543 for (value, min, max) in [
1546 (f32::NAN, 0.0_f32, 100.0_f32),
1547 (0.0, f32::NEG_INFINITY, f32::INFINITY),
1548 (5.0, 5.0, 5.0),
1549 ] {
1550 let mut s = Slider::create(value, min, max);
1551 let old = s.swap_with_default();
1552 assert_eq!(s, Slider::default());
1553 assert_eq!(old.slider_state.inner.min, min);
1554 }
1555 }
1556
1557 #[test]
1562 fn set_on_value_change_stores_the_pointer_and_the_payload() {
1563 let probe = RefAny::new(0xDEAD_BEEF_u32);
1564 let mut s = Slider::create(0.0, 0.0, 100.0);
1565 s.set_on_value_change(probe.clone(), record_value as SliderOnValueChangeCallbackType);
1566
1567 let hook = s
1568 .slider_state
1569 .on_value_change
1570 .as_ref()
1571 .expect("the hook was dropped");
1572 assert_eq!(hook.callback.cb as *const () as usize, record_value as *const () as usize);
1573 let mut stored = hook.refany.clone();
1574 assert_eq!(
1575 *stored.downcast_ref::<u32>().expect("the payload changed type"),
1576 0xDEAD_BEEF,
1577 );
1578 }
1579
1580 #[test]
1581 fn set_on_value_change_replaces_a_previous_hook() {
1582 let mut s = Slider::create(0.0, 0.0, 100.0);
1585 s.set_on_value_change(RefAny::new(1u8), record_value as SliderOnValueChangeCallbackType);
1586 s.set_on_value_change(RefAny::new(2u8), value_refresh_all as SliderOnValueChangeCallbackType);
1587 assert_eq!(hook_ptr(&s), Some(value_refresh_all as *const () as usize));
1588 let mut stored = s
1589 .slider_state
1590 .on_value_change
1591 .as_ref()
1592 .expect("hook")
1593 .refany
1594 .clone();
1595 assert_eq!(*stored.downcast_ref::<u8>().expect("payload"), 2);
1596 }
1597
1598 #[test]
1599 fn set_on_value_change_leaves_the_value_and_both_styles_untouched() {
1600 let before = Slider::create(60.0, 0.0, 100.0);
1601 let mut after = before.clone();
1602 after.set_on_value_change(log_refany(), record_value as SliderOnValueChangeCallbackType);
1603 assert_eq!(after.slider_state.inner, before.slider_state.inner);
1604 assert_eq!(after.track_style, before.track_style);
1605 assert_eq!(after.thumb_style, before.thumb_style);
1606 }
1607
1608 #[test]
1609 fn with_on_value_change_matches_the_setter() {
1610 let a = Slider::create(60.0, 0.0, 100.0)
1611 .with_on_value_change(RefAny::new(9u8), record_value as SliderOnValueChangeCallbackType);
1612 let mut b = Slider::create(60.0, 0.0, 100.0);
1613 b.set_on_value_change(RefAny::new(9u8), record_value as SliderOnValueChangeCallbackType);
1614 assert_eq!(hook_ptr(&a), hook_ptr(&b));
1615 assert_eq!(a.slider_state.inner, b.slider_state.inner);
1616 }
1617
1618 #[test]
1619 fn with_on_value_change_accepts_a_generic_callback_without_mangling_the_pointer() {
1620 let generic = Callback {
1624 cb: generic_shaped,
1625 ctx: OptionRefAny::None,
1626 };
1627 let s = Slider::create(0.0, 0.0, 100.0).with_on_value_change(RefAny::new(0u8), generic);
1628 assert_eq!(
1629 hook_ptr(&s),
1630 Some(generic_shaped as *const () as usize),
1631 "the Callback -> SliderOnValueChangeCallback transmute mangled the pointer",
1632 );
1633 }
1634
1635 #[test]
1636 fn with_on_value_change_does_not_panic_for_hostile_widgets() {
1637 for (value, min, max) in [
1638 (f32::NAN, 0.0_f32, 100.0_f32),
1639 (0.0, f32::NEG_INFINITY, f32::INFINITY),
1640 (7.0, 7.0, 7.0),
1641 ] {
1642 let s = Slider::create(value, min, max).with_on_value_change(
1643 log_refany(),
1644 record_value as SliderOnValueChangeCallbackType,
1645 );
1646 assert!(s.slider_state.on_value_change.as_ref().is_some());
1647 }
1648 }
1649
1650 #[test]
1655 fn dom_renders_a_classed_track_with_exactly_one_thumb_child() {
1656 let dom = Slider::create(50.0, 0.0, 100.0).dom();
1657 assert_eq!(classes(&dom), vec!["__azul-native-slider".to_string()]);
1658 assert!(matches!(dom.root.get_node_type(), NodeType::Div));
1659 let kids = dom.children.as_ref();
1660 assert_eq!(kids.len(), 1, "the track must have exactly one child (the thumb)");
1661 assert_eq!(classes(&kids[0]), vec!["__azul-native-slider-thumb".to_string()]);
1662 assert!(kids[0].children.as_ref().is_empty(), "the thumb must be a leaf");
1663 }
1664
1665 #[test]
1666 fn dom_is_keyboard_reachable() {
1667 let dom = Slider::create(0.0, 0.0, 100.0).dom();
1669 assert_eq!(dom.root.get_tab_index(), Some(TabIndex::Auto));
1670 }
1671
1672 #[test]
1673 fn dom_registers_every_pointer_filter_exactly_once_in_order() {
1674 let dom = Slider::create(0.0, 0.0, 100.0).dom();
1675 let expected: [(EventFilter, usize); 7] = [
1676 (EventFilter::Hover(HoverEventFilter::MouseDown), on_slider_pointer_down as usize),
1677 (EventFilter::Hover(HoverEventFilter::MouseOver), on_slider_pointer_move as usize),
1678 (EventFilter::Hover(HoverEventFilter::MouseUp), on_slider_pointer_up as usize),
1679 (EventFilter::Hover(HoverEventFilter::MouseLeave), on_slider_pointer_up as usize),
1680 (EventFilter::Hover(HoverEventFilter::TouchStart), on_slider_pointer_down as usize),
1681 (EventFilter::Hover(HoverEventFilter::TouchMove), on_slider_pointer_move as usize),
1682 (EventFilter::Hover(HoverEventFilter::TouchEnd), on_slider_pointer_up as usize),
1683 ];
1684 let got: Vec<(EventFilter, usize)> = dom
1685 .root
1686 .callbacks
1687 .as_ref()
1688 .iter()
1689 .map(|c| (c.event, c.callback.cb))
1690 .collect();
1691 assert_eq!(got, expected.to_vec(), "the pointer wiring changed");
1692 assert_eq!(got.len(), 7);
1695 }
1696
1697 #[test]
1698 fn dom_shares_one_state_refany_across_all_seven_handlers() {
1699 let dom = Slider::create(0.0, 0.0, 100.0).dom();
1703 let cbs = dom.root.callbacks.as_ref();
1704 {
1705 let mut first = cbs[0].refany.clone();
1706 let mut guard = first
1707 .downcast_mut::<SliderStateWrapper>()
1708 .expect("the state changed type");
1709 guard.dragging = true;
1710 }
1711 for (i, cb) in cbs.iter().enumerate() {
1712 let mut other = cb.refany.clone();
1713 let guard = other
1714 .downcast_ref::<SliderStateWrapper>()
1715 .expect("the state changed type");
1716 assert!(guard.dragging, "handler {i} does not share the drag state");
1717 }
1718 }
1719
1720 #[test]
1721 fn dom_carries_the_widgets_own_state_not_a_default() {
1722 let mut state = Slider::create(42.0, -10.0, 90.0).dom().root.callbacks.as_ref()[0]
1723 .refany
1724 .clone();
1725 let wrapper = state
1726 .downcast_ref::<SliderStateWrapper>()
1727 .expect("the state changed type");
1728 assert_eq!(wrapper.inner.value, 42.0);
1729 assert_eq!(wrapper.inner.min, -10.0);
1730 assert_eq!(wrapper.inner.max, 90.0);
1731 assert!(!wrapper.dragging, "a freshly rendered slider must not be mid-drag");
1732 }
1733
1734 #[test]
1735 fn dom_inlines_the_track_and_thumb_styles_verbatim() {
1736 let s = Slider::create(75.0, 0.0, 100.0);
1737 let (track_props, thumb_props) = (properties(&s.track_style), properties(&s.thumb_style));
1738 let dom = s.dom();
1739 assert_eq!(inline_properties(&dom), track_props);
1740 assert_eq!(inline_properties(&dom.children.as_ref()[0]), thumb_props);
1741 }
1742
1743 #[test]
1744 fn dom_does_not_panic_for_hostile_widgets() {
1745 for (value, min, max) in [
1746 (f32::NAN, 0.0_f32, 100.0_f32),
1747 (0.0, f32::NEG_INFINITY, f32::INFINITY),
1748 (3.0, 3.0, 3.0),
1749 (f32::MAX, f32::MIN, f32::MAX),
1750 ] {
1751 let dom = Slider::create(value, min, max).dom();
1752 assert_eq!(dom.children.as_ref().len(), 1, "({value}, {min}, {max})");
1753 }
1754 }
1755
1756 #[test]
1757 fn from_slider_for_dom_is_exactly_dom() {
1758 let s = Slider::create(25.0, 0.0, 100.0);
1759 let via_impl: Dom = s.clone().into();
1760 assert_eq!(inline_properties(&via_impl), inline_properties(&s.dom()));
1761 }
1762
1763 #[test]
1768 fn a_press_maps_the_cursor_x_onto_the_range_using_the_design_width() {
1769 for (x, expected) in [
1772 (0.0_f32, 0.0_f32),
1773 (50.0, 25.0),
1774 (100.0, 50.0),
1775 (200.0, 100.0),
1776 ] {
1777 let (_, _, state) = press_at(Slider::create(0.0, 0.0, 100.0), x);
1778 assert_eq!(
1779 state.inner.value,
1780 expected,
1781 "a press at x = {x} produced the wrong value",
1782 );
1783 assert_eq!(state.inner.value, expected_value(x, DESIGN_WIDTH, 0.0, 100.0));
1784 }
1785 }
1786
1787 #[test]
1788 fn a_press_outside_the_track_clamps_instead_of_overshooting() {
1789 for (x, expected) in [
1790 (-1.0_f32, 0.0_f32),
1791 (-1.0e9, 0.0),
1792 (201.0, 100.0),
1793 (1.0e9, 100.0),
1794 (f32::INFINITY, 100.0),
1795 (f32::NEG_INFINITY, 0.0),
1796 ] {
1797 let (_, _, state) = press_at(Slider::create(50.0, 0.0, 100.0), x);
1798 assert_eq!(state.inner.value, expected, "a press at x = {x} escaped the range");
1799 }
1800 }
1801
1802 #[test]
1803 fn a_press_maps_onto_a_negative_range_too() {
1804 let (_, _, state) = press_at(Slider::create(-100.0, -100.0, -50.0), 100.0);
1805 assert_eq!(state.inner.value, -75.0, "the midpoint of [-100, -50] is -75");
1806 }
1807
1808 #[test]
1809 fn a_press_slides_the_thumb_by_writing_margin_left_on_the_first_child() {
1810 let (update, changes, _) = press_at(Slider::create(0.0, 0.0, 100.0), 100.0);
1811 assert_eq!(update, Update::DoNothing, "no hook is installed, so nothing to redraw");
1812 let margins = pushed_margins(&changes);
1813 assert_eq!(
1814 margins.len(),
1815 1,
1816 "exactly one thumb move per press, got {changes:?}",
1817 );
1818 let (node_id, margin) = margins[0];
1819 assert_eq!(node_id, NodeId::new(1), "the margin landed on the track, not the thumb");
1820 assert_eq!(margin, (0.5 * TRAVEL).round(), "the thumb went to the wrong pixel");
1821 }
1822
1823 #[test]
1824 fn a_press_never_slides_the_thumb_off_the_rail() {
1825 for x in [-1.0e9_f32, -1.0, 0.0, 37.0, 199.0, 200.0, 1.0e9, f32::INFINITY] {
1826 let (_, changes, _) = press_at(Slider::create(0.0, 0.0, 100.0), x);
1827 for (_, margin) in pushed_margins(&changes) {
1828 assert!(
1829 (0.0..=TRAVEL).contains(&margin),
1830 "a press at x = {x} put the thumb at {margin}, off a {TRAVEL}px rail",
1831 );
1832 }
1833 }
1834 }
1835
1836 #[test]
1837 fn a_press_uses_the_real_track_width_once_the_node_is_laid_out() {
1838 for (width, x, expected) in [
1842 (400.0_f32, 100.0_f32, 25.0_f32),
1843 (400.0, 400.0, 100.0),
1844 (100.0, 50.0, 50.0),
1845 (50.0, 200.0, 100.0), ] {
1847 let (styled, state) = wired(Slider::create(0.0, 0.0, 100.0));
1848 let layout = laid_out_at(styled, LogicalSize::new(width, TRACK_HEIGHT as f32));
1849 let (_, _) = with_info(layout, node(0), cursor(x, 8.0), |info| {
1850 on_slider_pointer_down(state.clone(), *info)
1851 });
1852 assert_eq!(
1853 read_state(&state).inner.value,
1854 expected,
1855 "a {width}px-wide track mapped x = {x} wrongly",
1856 );
1857 }
1858 }
1859
1860 #[test]
1861 fn a_zero_width_track_falls_back_to_the_design_width_instead_of_dividing_by_zero() {
1862 let (styled, state) = wired(Slider::create(0.0, 0.0, 100.0));
1865 let layout = laid_out_at(styled, LogicalSize::new(0.0, TRACK_HEIGHT as f32));
1866 with_info(layout, node(0), cursor(100.0, 8.0), |info| {
1867 on_slider_pointer_down(state.clone(), *info)
1868 });
1869 let value = read_state(&state).inner.value;
1870 assert!(value.is_finite(), "a zero-width track produced {value}");
1871 assert_eq!(value, 50.0, "the fallback must be the 200px design width");
1872 }
1873
1874 #[test]
1875 fn a_press_without_a_cursor_latches_the_drag_but_changes_nothing() {
1876 let (styled, state) = wired(Slider::create(60.0, 0.0, 100.0));
1878 let (update, changes) = with_info(
1879 unlaid(styled),
1880 node(0),
1881 OptionLogicalPosition::None,
1882 |info| on_slider_pointer_down(state.clone(), *info),
1883 );
1884 assert_eq!(update, Update::DoNothing);
1885 assert!(changes.is_empty(), "the thumb moved without a cursor: {changes:?}");
1886 let s = read_state(&state);
1887 assert_eq!(s.inner.value, 60.0, "the value changed without a cursor");
1888 assert!(s.dragging, "the press must still arm the drag");
1889 }
1890
1891 #[test]
1892 fn a_nan_cursor_does_not_panic_and_leaves_the_thumb_parked() {
1893 let (styled, state) = wired(Slider::create(10.0, 0.0, 100.0));
1894 let (update, changes) = with_info(
1895 unlaid(styled),
1896 node(0),
1897 cursor(f32::NAN, f32::NAN),
1898 |info| on_slider_pointer_down(state.clone(), *info),
1899 );
1900 assert_eq!(update, Update::DoNothing);
1901 assert!(read_state(&state).inner.value.is_nan());
1904 assert_eq!(pushed_margins(&changes), vec![(NodeId::new(1), 0.0)]);
1905 }
1906
1907 #[test]
1908 fn a_press_on_an_unknown_node_still_updates_the_value_but_moves_no_thumb() {
1909 for hit in [node_none(), node(99), node(usize::MAX - 1)] {
1912 let (styled, state) = wired(Slider::create(0.0, 0.0, 100.0));
1913 let (update, changes) = with_info(unlaid(styled), hit, cursor(100.0, 8.0), |info| {
1914 on_slider_pointer_down(state.clone(), *info)
1915 });
1916 assert_eq!(update, Update::DoNothing);
1917 assert_eq!(read_state(&state).inner.value, 50.0, "hit {hit:?}");
1918 assert!(
1919 pushed_margins(&changes).is_empty(),
1920 "a thumb was moved for a node that does not exist: {changes:?}",
1921 );
1922 }
1923 }
1924
1925 #[test]
1926 fn a_move_is_ignored_until_a_press_starts_the_drag() {
1927 let (styled, state) = wired(Slider::create(10.0, 0.0, 100.0));
1930 let (update, changes) = with_info(unlaid(styled), node(0), cursor(200.0, 8.0), |info| {
1931 on_slider_pointer_move(state.clone(), *info)
1932 });
1933 assert_eq!(update, Update::DoNothing);
1934 assert!(changes.is_empty(), "a hover moved the thumb: {changes:?}");
1935 assert_eq!(read_state(&state).inner.value, 10.0, "a hover changed the value");
1936 }
1937
1938 #[test]
1939 fn a_move_tracks_the_cursor_once_the_press_armed_the_drag() {
1940 let slider = Slider::create(0.0, 0.0, 100.0);
1941 let (styled, state) = wired(slider.clone());
1942 with_info(unlaid(styled), node(0), cursor(0.0, 8.0), |info| {
1943 on_slider_pointer_down(state.clone(), *info)
1944 });
1945 assert!(read_state(&state).dragging, "the press did not arm the drag");
1946
1947 let styled2 = StyledDom::create_from_dom(slider.dom());
1948 let (update, changes) = with_info(unlaid(styled2), node(0), cursor(150.0, 8.0), |info| {
1949 on_slider_pointer_move(state.clone(), *info)
1950 });
1951 assert_eq!(update, Update::DoNothing);
1952 assert_eq!(read_state(&state).inner.value, 75.0, "the drag did not track the cursor");
1953 assert_eq!(pushed_margins(&changes), vec![(NodeId::new(1), (0.75 * TRAVEL).round())]);
1954 }
1955
1956 #[test]
1957 fn a_release_ends_the_drag_and_records_nothing() {
1958 let (styled, state) = wired(Slider::create(0.0, 0.0, 100.0));
1959 with_info(unlaid(styled), node(0), cursor(100.0, 8.0), |info| {
1960 on_slider_pointer_down(state.clone(), *info)
1961 });
1962 assert!(read_state(&state).dragging);
1963
1964 let styled2 = StyledDom::create_from_dom(Slider::create(0.0, 0.0, 100.0).dom());
1965 let (update, changes) = with_info(unlaid(styled2), node(0), cursor(0.0, 8.0), |info| {
1966 on_slider_pointer_up(state.clone(), *info)
1967 });
1968 assert_eq!(update, Update::DoNothing);
1969 assert!(changes.is_empty(), "the release moved the thumb: {changes:?}");
1970 let s = read_state(&state);
1971 assert!(!s.dragging, "the drag outlived the release");
1972 assert_eq!(s.inner.value, 50.0, "the release rewrote the value");
1973 }
1974
1975 #[test]
1976 fn a_release_is_idempotent_and_safe_before_any_press() {
1977 let (styled, state) = wired(Slider::create(0.0, 0.0, 100.0));
1978 with_info(unlaid(styled), node(0), OptionLogicalPosition::None, |info| {
1979 on_slider_pointer_up(state.clone(), *info)
1980 });
1981 assert!(!read_state(&state).dragging);
1982 let styled2 = StyledDom::create_from_dom(Slider::create(0.0, 0.0, 100.0).dom());
1983 with_info(unlaid(styled2), node(0), OptionLogicalPosition::None, |info| {
1984 on_slider_pointer_up(state.clone(), *info)
1985 });
1986 assert!(!read_state(&state).dragging);
1987 }
1988
1989 #[test]
1990 fn every_pointer_handler_ignores_a_foreign_payload() {
1991 for handler in [
1994 on_slider_pointer_down as extern "C" fn(RefAny, CallbackInfo) -> Update,
1995 on_slider_pointer_move,
1996 on_slider_pointer_up,
1997 ] {
1998 let foreign = RefAny::new(0xABCD_u32);
1999 let styled = StyledDom::create_from_dom(Slider::create(0.0, 0.0, 100.0).dom());
2000 let (update, changes) =
2001 with_info(unlaid(styled), node(0), cursor(100.0, 8.0), |info| {
2002 handler(foreign.clone(), *info)
2003 });
2004 assert_eq!(update, Update::DoNothing);
2005 assert!(changes.is_empty(), "a foreign payload still mutated the DOM: {changes:?}");
2006 let mut foreign = foreign;
2007 assert_eq!(
2008 *foreign.downcast_ref::<u32>().expect("the payload was replaced"),
2009 0xABCD,
2010 );
2011 }
2012 }
2013
2014 #[test]
2015 fn a_press_forwards_the_hooks_update_verbatim() {
2016 for (cb, expected) in [
2017 (value_do_nothing as SliderOnValueChangeCallbackType, Update::DoNothing),
2018 (record_value as SliderOnValueChangeCallbackType, Update::RefreshDom),
2019 (value_refresh_all as SliderOnValueChangeCallbackType, Update::RefreshDomAllWindows),
2020 ] {
2021 let (styled, state) =
2022 wired(Slider::create(0.0, 0.0, 100.0).with_on_value_change(log_refany(), cb));
2023 let (update, _) = with_info(unlaid(styled), node(0), cursor(100.0, 8.0), |info| {
2024 on_slider_pointer_down(state.clone(), *info)
2025 });
2026 assert_eq!(update, expected, "the handler swallowed {expected:?}");
2027 }
2028 }
2029
2030 #[test]
2031 fn the_hook_is_told_the_new_value_not_the_old_one() {
2032 let probe = log_refany();
2035 let (styled, state) = wired(Slider::create(0.0, -10.0, 90.0).with_on_value_change(
2036 probe.clone(),
2037 record_value as SliderOnValueChangeCallbackType,
2038 ));
2039 let (update, _) = with_info(unlaid(styled), node(0), cursor(50.0, 8.0), |info| {
2040 on_slider_pointer_down(state.clone(), *info)
2041 });
2042 assert_eq!(update, Update::RefreshDom);
2043 assert_eq!(
2044 read_log(&probe).seen,
2045 vec![SliderState {
2046 value: expected_value(50.0, DESIGN_WIDTH, -10.0, 90.0),
2047 min: -10.0,
2048 max: 90.0,
2049 }],
2050 );
2051 }
2052
2053 #[test]
2054 fn the_hook_is_not_called_when_there_is_no_cursor() {
2055 let probe = log_refany();
2056 let (styled, state) = wired(Slider::create(0.0, 0.0, 100.0).with_on_value_change(
2057 probe.clone(),
2058 record_value as SliderOnValueChangeCallbackType,
2059 ));
2060 with_info(unlaid(styled), node(0), OptionLogicalPosition::None, |info| {
2061 on_slider_pointer_down(state.clone(), *info)
2062 });
2063 assert!(read_log(&probe).seen.is_empty(), "the hook fired without a cursor");
2064 }
2065
2066 #[test]
2067 fn the_hook_is_not_called_on_release() {
2068 let probe = log_refany();
2069 let (styled, state) = wired(Slider::create(0.0, 0.0, 100.0).with_on_value_change(
2070 probe.clone(),
2071 record_value as SliderOnValueChangeCallbackType,
2072 ));
2073 with_info(unlaid(styled), node(0), cursor(100.0, 8.0), |info| {
2074 on_slider_pointer_up(state.clone(), *info)
2075 });
2076 assert!(read_log(&probe).seen.is_empty(), "the release reported a value change");
2077 }
2078
2079 #[test]
2080 fn apply_cursor_value_tolerates_a_degenerate_range_without_panicking() {
2081 for (min, max) in DEGENERATE_RANGES {
2086 let mut wrapper = SliderStateWrapper {
2087 inner: SliderState { value: 0.0, min, max },
2088 ..Default::default()
2089 };
2090 let styled = StyledDom::create_from_dom(Slider::create(0.0, 0.0, 100.0).dom());
2091 let (update, changes) =
2092 with_info(unlaid(styled), node(0), cursor(100.0, 8.0), |info| {
2093 apply_cursor_value(&mut wrapper, info)
2094 });
2095 assert_eq!(update, Update::DoNothing);
2096 let v = wrapper.inner.value;
2097 assert!(
2098 v.is_nan() || (v >= min.min(max) && v <= min.max(max)),
2099 "[{min}, {max}] interpolated to {v}, outside the bounds in either order",
2100 );
2101 for (_, margin) in pushed_margins(&changes) {
2104 assert!(
2105 (0.0..=TRAVEL).contains(&margin),
2106 "[{min}, {max}] slid the thumb to {margin}",
2107 );
2108 }
2109 }
2110 }
2111
2112 #[test]
2113 fn apply_cursor_value_keeps_the_value_inside_any_sane_range() {
2114 for (min, max) in SANE_RANGES {
2118 for x in [-1.0e9_f32, -1.0, 0.0, 73.0, 200.0, 1.0e9] {
2119 let mut wrapper = SliderStateWrapper {
2120 inner: SliderState { value: min, min, max },
2121 ..Default::default()
2122 };
2123 let styled = StyledDom::create_from_dom(Slider::create(0.0, 0.0, 100.0).dom());
2124 with_info(unlaid(styled), node(0), cursor(x, 8.0), |info| {
2125 apply_cursor_value(&mut wrapper, info)
2126 });
2127 let v = wrapper.inner.value;
2128 assert!(
2129 (min..=max).contains(&v),
2130 "a press at x = {x} put the value at {v}, outside [{min}, {max}]",
2131 );
2132 }
2133 }
2134 }
2135
2136 #[test]
2137 fn apply_cursor_value_is_idempotent_for_a_stationary_cursor() {
2138 let mut wrapper = SliderStateWrapper {
2139 inner: SliderState { value: 0.0, min: 0.0, max: 100.0 },
2140 ..Default::default()
2141 };
2142 for _ in 0..3 {
2143 let styled = StyledDom::create_from_dom(Slider::create(0.0, 0.0, 100.0).dom());
2144 with_info(unlaid(styled), node(0), cursor(100.0, 8.0), |info| {
2145 apply_cursor_value(&mut wrapper, info)
2146 });
2147 assert_eq!(wrapper.inner.value, 50.0);
2148 }
2149 }
2150
2151 #[test]
2152 fn deliver_smoke_test_covers_every_handler_and_layout_combination() {
2153 for handler in [
2157 on_slider_pointer_down as extern "C" fn(RefAny, CallbackInfo) -> Update,
2158 on_slider_pointer_move,
2159 on_slider_pointer_up,
2160 ] {
2161 for track in [None, Some(LogicalSize::new(0.0, 0.0)), Some(LogicalSize::new(1.0e9, 16.0))] {
2162 for at in [
2163 OptionLogicalPosition::None,
2164 cursor(0.0, 0.0),
2165 cursor(-1.0e9, 0.0),
2166 cursor(f32::NAN, 0.0),
2167 cursor(f32::INFINITY, 0.0),
2168 ] {
2169 let slider = Slider::create(0.0, 0.0, 100.0);
2170 let (_, state) = wired(slider.clone());
2171 let (_, changes) =
2172 deliver(slider, &state, node(0), at, track, handler);
2173 let _ = read_state(&state);
2175 for (_, margin) in pushed_margins(&changes) {
2176 assert!(
2177 margin.is_finite(),
2178 "a non-finite thumb offset ({margin}) reached the DOM",
2179 );
2180 }
2181 }
2182 }
2183 }
2184 }
2185}