1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14pub mod thumb;
15
16use std::{any::Any, fmt, ops::Range, sync::Arc};
17
18use colors::ACCENT_COLOR_VAR;
19use parking_lot::Mutex;
20use zng_var::{AnyVar, AnyVarValue, BoxedAnyVar};
21use zng_wgt::prelude::*;
22use zng_wgt_input::{focus::FocusableMix, pointer_capture::capture_pointer};
23use zng_wgt_style::{Style, StyleMix, impl_style_fn, style_fn};
24
25#[widget($crate::Slider)]
27pub struct Slider(FocusableMix<StyleMix<WidgetBase>>);
28impl Slider {
29 fn widget_intrinsic(&mut self) {
30 self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
31
32 widget_set! {
33 self;
34 style_base_fn = style_fn!(|_| DefaultStyle!());
35 capture_pointer = true;
36 }
37 }
38}
39impl_style_fn!(Slider);
40
41#[widget($crate::DefaultStyle)]
43pub struct DefaultStyle(Style);
44impl DefaultStyle {
45 fn widget_intrinsic(&mut self) {
46 widget_set! {
47 self;
48 zng_wgt_container::child = SliderTrack! {
49 zng_wgt::corner_radius = 5;
50 zng_wgt_fill::background_color = ACCENT_COLOR_VAR.rgba();
51 zng_wgt::margin = 8; when #{SLIDER_DIRECTION_VAR}.is_horizontal() {
54 zng_wgt_size_offset::height = 5;
55 }
56 when #{SLIDER_DIRECTION_VAR}.is_vertical() {
57 zng_wgt_size_offset::width = 5;
58 }
59 };
60 zng_wgt_container::child_align = Align::FILL_X;
61 }
62 }
63}
64
65trait SelectorImpl: Send {
66 fn selection(&self) -> BoxedAnyVar;
67 fn set(&mut self, nearest: Factor, to: Factor);
68 fn thumbs(&self) -> Vec<ThumbValue>;
69 fn to_offset(&self, t: &dyn AnyVarValue) -> Option<Factor>;
70 #[allow(clippy::wrong_self_convention)]
71 fn from_offset(&self, offset: Factor) -> Box<dyn Any>;
72}
73
74trait OffsetConvert<T>: Send {
75 fn to(&self, t: &T) -> Factor;
76 fn from(&self, f: Factor) -> T;
77}
78impl<T, Tf: Fn(&T) -> Factor + Send, Ff: Fn(Factor) -> T + Send> OffsetConvert<T> for (Tf, Ff) {
79 fn to(&self, t: &T) -> Factor {
80 (self.0)(t)
81 }
82
83 fn from(&self, f: Factor) -> T {
84 (self.1)(f)
85 }
86}
87
88pub trait SelectorValue: VarValue {
95 fn to_selector(value: BoxedVar<Self>, min: Self, max: Self) -> Selector;
97}
98
99#[derive(Clone)]
103pub struct Selector(Arc<Mutex<dyn SelectorImpl>>);
104impl fmt::Debug for Selector {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(f, "Selector(_)")
107 }
108}
109impl PartialEq for Selector {
110 fn eq(&self, other: &Self) -> bool {
111 Arc::ptr_eq(&self.0, &other.0)
112 }
113}
114impl Selector {
115 pub fn value<T: SelectorValue>(selection: impl IntoVar<T>, min: T, max: T) -> Self {
117 T::to_selector(selection.into_var().boxed(), min, max)
118 }
119
120 pub fn value_with<T>(
125 selection: impl IntoVar<T>,
126 to_offset: impl Fn(&T) -> Factor + Send + 'static,
127 from_offset: impl Fn(Factor) -> T + Send + 'static,
128 ) -> Self
129 where
130 T: VarValue,
131 {
132 struct SingleImpl<T> {
133 selection: BoxedVar<T>,
134 selection_fct: Factor,
135 to_from: Box<dyn OffsetConvert<T>>,
136 }
137 impl<T: VarValue> SelectorImpl for SingleImpl<T> {
138 fn selection(&self) -> BoxedAnyVar {
139 self.selection.clone_any()
140 }
141
142 fn set(&mut self, _: Factor, to: Factor) {
143 self.selection_fct = to;
144 let _ = self.selection.set(self.to_from.from(to));
145 }
146
147 fn thumbs(&self) -> Vec<ThumbValue> {
148 vec![ThumbValue {
149 offset: self.selection_fct,
150 n_of: (0, 0),
151 }]
152 }
153
154 fn to_offset(&self, t: &dyn AnyVarValue) -> Option<Factor> {
155 let f = self.to_from.to(t.as_any().downcast_ref::<T>()?);
156 Some(f)
157 }
158
159 fn from_offset(&self, offset: Factor) -> Box<dyn Any> {
160 Box::new(self.to_from.from(offset))
161 }
162 }
163 let selection = selection.into_var();
164 Self(Arc::new(Mutex::new(SingleImpl {
165 selection_fct: selection.with(&to_offset),
166 selection: selection.boxed(),
167 to_from: Box::new((to_offset, from_offset)),
168 })))
169 }
170
171 pub fn range<T: SelectorValue>(range: impl IntoVar<std::ops::Range<T>>, min: T, max: T) -> Self {
173 let convert = T::to_selector(zng_var::LocalVar(min.clone()).boxed(), min, max);
175 Self::range_with(range.into_var(), clmv!(convert, |t| convert.to_offset(t).unwrap()), move |f| {
176 convert.from_offset(f).unwrap()
177 })
178 }
179
180 pub fn range_with<T>(
186 range: impl IntoVar<std::ops::Range<T>>,
187 to_offset: impl Fn(&T) -> Factor + Send + 'static,
188 from_offset: impl Fn(Factor) -> T + Send + 'static,
189 ) -> Self
190 where
191 T: VarValue,
192 {
193 struct RangeImpl<T> {
194 selection: BoxedVar<Range<T>>,
195 selection_fct: [Factor; 2],
196 to_from: Box<dyn OffsetConvert<T>>,
197 }
198 impl<T: VarValue> SelectorImpl for RangeImpl<T> {
199 fn selection(&self) -> BoxedAnyVar {
200 self.selection.clone_any()
201 }
202
203 fn set(&mut self, nearest: Factor, to: Factor) {
204 if (self.selection_fct[0] - nearest).abs() < (self.selection_fct[1] - nearest).abs() {
205 self.selection_fct[0] = to;
206 } else {
207 self.selection_fct[1] = to;
208 }
209 if self.selection_fct[0] > self.selection_fct[1] {
210 self.selection_fct.swap(0, 1);
211 }
212 let start = self.to_from.from(self.selection_fct[0]);
213 let end = self.to_from.from(self.selection_fct[1]);
214 let _ = self.selection.set(start..end);
215 }
216
217 fn thumbs(&self) -> Vec<ThumbValue> {
218 vec![
219 ThumbValue {
220 offset: self.selection_fct[0],
221 n_of: (0, 2),
222 },
223 ThumbValue {
224 offset: self.selection_fct[1],
225 n_of: (1, 2),
226 },
227 ]
228 }
229
230 fn to_offset(&self, t: &dyn AnyVarValue) -> Option<Factor> {
231 let f = self.to_from.to(t.as_any().downcast_ref::<T>()?);
232 Some(f)
233 }
234
235 fn from_offset(&self, offset: Factor) -> Box<dyn Any> {
236 Box::new(self.to_from.from(offset))
237 }
238 }
239 let selection = range.into_var();
240
241 Self(Arc::new(Mutex::new(RangeImpl {
242 selection_fct: selection.with(|r| [to_offset(&r.start), to_offset(&r.end)]),
243 selection: selection.boxed(),
244 to_from: Box::new((to_offset, from_offset)),
245 })))
246 }
247
248 pub fn many<T: SelectorValue>(many: impl IntoVar<Vec<T>>, min: T, max: T) -> Self {
250 let convert = T::to_selector(zng_var::LocalVar(min.clone()).boxed(), min, max);
252 Self::many_with(many.into_var(), clmv!(convert, |t| convert.to_offset(t).unwrap()), move |f| {
253 convert.from_offset(f).unwrap()
254 })
255 }
256
257 pub fn many_with<T>(
263 many: impl IntoVar<Vec<T>>,
264 to_offset: impl Fn(&T) -> Factor + Send + 'static,
265 from_offset: impl Fn(Factor) -> T + Send + 'static,
266 ) -> Self
267 where
268 T: VarValue,
269 {
270 struct ManyImpl<T> {
271 selection: BoxedVar<Vec<T>>,
272 selection_fct: Vec<Factor>,
273 to_from: Box<dyn OffsetConvert<T>>,
274 }
275 impl<T: VarValue> SelectorImpl for ManyImpl<T> {
276 fn selection(&self) -> BoxedAnyVar {
277 self.selection.clone_any()
278 }
279
280 fn set(&mut self, nearest: Factor, to: Factor) {
281 if let Some((i, _)) = self
282 .selection_fct
283 .iter()
284 .enumerate()
285 .map(|(i, &f)| (i, (f - nearest).abs()))
286 .reduce(|a, b| if a.1 < b.1 { a } else { b })
287 {
288 self.selection_fct[i] = to;
289 self.selection_fct.sort_by(|a, b| a.0.total_cmp(&b.0));
290 let s: Vec<_> = self.selection_fct.iter().map(|&f| self.to_from.from(f)).collect();
291 let _ = self.selection.set(s);
292 }
293 }
294
295 fn thumbs(&self) -> Vec<ThumbValue> {
296 let len = self.selection_fct.len().min(u16::MAX as usize) as u16;
297 self.selection_fct
298 .iter()
299 .enumerate()
300 .map(|(i, &f)| ThumbValue {
301 offset: f,
302 n_of: (i.min(u16::MAX as usize) as u16, len),
303 })
304 .collect()
305 }
306
307 fn to_offset(&self, t: &dyn AnyVarValue) -> Option<Factor> {
308 let f = self.to_from.to(t.as_any().downcast_ref::<T>()?);
309 Some(f)
310 }
311
312 fn from_offset(&self, offset: Factor) -> Box<dyn Any> {
313 Box::new(self.to_from.from(offset))
314 }
315 }
316 let selection = many.into_var();
317 Self(Arc::new(Mutex::new(ManyImpl {
318 selection_fct: selection.with(|m| m.iter().map(&to_offset).collect()),
319 selection: selection.boxed(),
320 to_from: Box::new((to_offset, from_offset)),
321 })))
322 }
323
324 pub fn nil() -> Self {
326 Self::many_with(vec![], |_: &bool| 0.fct(), |_| false)
327 }
328
329 pub fn to_offset<T: VarValue>(&self, t: &T) -> Option<Factor> {
333 self.0.lock().to_offset(t)
334 }
335
336 pub fn from_offset<T: VarValue>(&self, offset: impl IntoValue<Factor>) -> Option<T> {
340 let b = self.0.lock().from_offset(offset.into()).downcast().ok()?;
341 Some(*b)
342 }
343
344 pub fn thumbs(&self) -> Vec<ThumbValue> {
346 self.0.lock().thumbs()
347 }
348
349 pub fn set(&self, nearest_thumb: impl IntoValue<Factor>, to: impl IntoValue<Factor>) {
353 self.0.lock().set(nearest_thumb.into(), to.into())
354 }
355
356 pub fn selection(&self) -> BoxedAnyVar {
360 self.0.lock().selection()
361 }
362}
363
364#[derive(Clone, Debug, PartialEq, Copy)]
366pub struct ThumbValue {
367 offset: Factor,
368 n_of: (u16, u16),
369}
370impl ThumbValue {
371 pub fn offset(&self) -> Factor {
373 self.offset
374 }
375
376 pub fn n_of(&self) -> (u16, u16) {
380 self.n_of
381 }
382
383 pub fn is_first(&self) -> bool {
385 self.n_of.0 == 0
386 }
387
388 pub fn is_last(&self) -> bool {
390 self.n_of.0 == self.n_of.1
391 }
392}
393
394context_local! {
395 pub static SELECTOR: Selector = Selector::nil();
397}
398context_var! {
399 pub static THUMB_FN_VAR: WidgetFn<ThumbArgs> = wgt_fn!(|a: ThumbArgs| thumb::Thumb!(a.thumb()));
401}
402
403#[property(CONTEXT, default(Selector::nil()), widget_impl(Slider))]
405pub fn selector(child: impl UiNode, selector: impl IntoValue<Selector>) -> impl UiNode {
406 with_context_local(child, &SELECTOR, selector)
407}
408
409#[property(CONTEXT, default(THUMB_FN_VAR))]
411pub fn thumb_fn(child: impl UiNode, thumb: impl IntoVar<WidgetFn<ThumbArgs>>) -> impl UiNode {
412 with_context_var(child, THUMB_FN_VAR, thumb)
413}
414
415pub struct ThumbArgs {
417 thumb: ArcVar<ThumbValue>,
418}
419impl ThumbArgs {
420 pub fn thumb(&self) -> ReadOnlyArcVar<ThumbValue> {
422 self.thumb.read_only()
423 }
424}
425
426pub trait WidgetInfoExt {
428 fn is_slider_track(&self) -> bool;
430
431 fn slider_track(&self) -> Option<WidgetInfo>;
433}
434impl WidgetInfoExt for WidgetInfo {
435 fn is_slider_track(&self) -> bool {
436 self.meta().flagged(*IS_SLIDER_ID)
437 }
438
439 fn slider_track(&self) -> Option<WidgetInfo> {
440 self.self_and_ancestors().find(|w| w.is_slider_track())
441 }
442}
443
444static_id! {
445 static ref IS_SLIDER_ID: StateId<()>;
446}
447
448#[derive(Clone, Copy, PartialEq, Eq)]
450pub enum SliderDirection {
451 StartToEnd,
455 EndToStart,
459 LeftToRight,
461 RightToLeft,
463 BottomToTop,
465 TopToBottom,
467}
468impl SliderDirection {
469 pub fn is_vertical(&self) -> bool {
471 matches!(self, Self::BottomToTop | Self::TopToBottom)
472 }
473
474 pub fn is_horizontal(&self) -> bool {
476 !self.is_vertical()
477 }
478
479 pub fn layout(&self, direction: LayoutDirection) -> Self {
481 match *self {
482 SliderDirection::StartToEnd => {
483 if direction.is_ltr() {
484 Self::LeftToRight
485 } else {
486 Self::RightToLeft
487 }
488 }
489 SliderDirection::EndToStart => {
490 if direction.is_ltr() {
491 Self::RightToLeft
492 } else {
493 Self::LeftToRight
494 }
495 }
496 s => s,
497 }
498 }
499}
500impl fmt::Debug for SliderDirection {
501 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502 if f.alternate() {
503 write!(f, "SliderDirection::")?;
504 }
505 match self {
506 Self::StartToEnd => write!(f, "StartToEnd"),
507 Self::EndToStart => write!(f, "EndToStart"),
508 Self::LeftToRight => write!(f, "LeftToRight"),
509 Self::RightToLeft => write!(f, "RightToLeft"),
510 Self::BottomToTop => write!(f, "BottomToTop"),
511 Self::TopToBottom => write!(f, "TopToBottom"),
512 }
513 }
514}
515
516context_var! {
517 pub static SLIDER_DIRECTION_VAR: SliderDirection = SliderDirection::StartToEnd;
519}
520
521#[property(CONTEXT, default(SLIDER_DIRECTION_VAR), widget_impl(Slider))]
525fn direction(child: impl UiNode, direction: impl IntoVar<SliderDirection>) -> impl UiNode {
526 with_context_var(child, SLIDER_DIRECTION_VAR, direction)
527}
528
529#[widget($crate::SliderTrack)]
534pub struct SliderTrack(WidgetBase);
535impl SliderTrack {
536 fn widget_intrinsic(&mut self) {
537 self.widget_builder().push_build_action(|wgt| {
538 wgt.set_child(slider_track_node());
539 })
540 }
541}
542
543fn slider_track_node() -> impl UiNode {
544 let mut thumbs = ui_vec![];
545 let mut thumb_vars = vec![];
546 match_node_leaf(move |op| match op {
547 UiNodeOp::Init => {
548 WIDGET.sub_var(&THUMB_FN_VAR);
549
550 thumb_vars = SELECTOR.get().thumbs().into_iter().map(zng_var::var).collect();
551 thumbs.reserve(thumb_vars.len());
552
553 let thumb_fn = THUMB_FN_VAR.get();
554 for v in &thumb_vars {
555 thumbs.push(thumb_fn(ThumbArgs { thumb: v.clone() }))
556 }
557
558 thumbs.init_all();
559 }
560 UiNodeOp::Deinit => {
561 thumbs.deinit_all();
562 thumbs = ui_vec![];
563 thumb_vars = vec![];
564 }
565 UiNodeOp::Info { info } => {
566 info.flag_meta(*IS_SLIDER_ID);
567 thumbs.info_all(info);
568 }
569 UiNodeOp::Measure { desired_size, .. } => {
570 *desired_size = LAYOUT.constraints().fill_size();
571 }
572 UiNodeOp::Layout { final_size, wl } => {
573 *final_size = LAYOUT.constraints().fill_size();
574 let _ = thumbs.layout_each(wl, |_, n, wl| n.layout(wl), |_, _| PxSize::zero());
575 }
576 UiNodeOp::Update { updates } => {
577 if let Some(thumb_fn) = THUMB_FN_VAR.get_new() {
578 thumbs.deinit_all();
579 thumb_vars.clear();
580 thumbs.clear();
581
582 for value in SELECTOR.get().thumbs() {
583 let var = zng_var::var(value);
584 let thumb = thumb_fn(ThumbArgs { thumb: var.clone() });
585 thumb_vars.push(var);
586 thumbs.push(thumb);
587 }
588
589 thumbs.init_all();
590
591 WIDGET.update_info().layout().render();
592 } else {
593 thumbs.update_all(updates, &mut ());
594
595 let mut thumb_values = SELECTOR.get().thumbs();
598 match thumb_values.len().cmp(&thumb_vars.len()) {
599 std::cmp::Ordering::Less => {
600 for mut drop in thumbs.drain(thumb_values.len()..) {
602 drop.deinit();
603 }
604 thumb_vars.truncate(thumbs.len());
605 }
606 std::cmp::Ordering::Greater => {
607 let thumb_fn = THUMB_FN_VAR.get();
609 for value in thumb_values.drain(thumbs.len()..) {
610 let var = zng_var::var(value);
611 let mut thumb = thumb_fn(ThumbArgs { thumb: var.clone() });
612 thumb.init();
613 thumb_vars.push(var);
614 thumbs.push(thumb);
615 }
616 }
617 std::cmp::Ordering::Equal => {}
618 }
619
620 for (var, value) in thumb_vars.iter().zip(thumb_values) {
622 var.set(value);
623 }
624 }
625 }
626 op => thumbs.op(op),
627 })
628}
629
630macro_rules! impl_32 {
631 (($to_f32:expr, $from_f32:expr) => $($T:ident),+ $(,)?) => {
632 $(
633 impl SelectorValue for $T {
634 #[allow(clippy::unnecessary_cast)]
635 fn to_selector(value: BoxedVar<Self>, min: Self, max: Self) -> Selector {
636 let to_f32 = $to_f32;
637 let from_f32 = $from_f32;
638
639 let min = to_f32(min);
640 let max = to_f32(max);
641 if min >= max {
642 Selector::nil()
643 } else {
644 let d = max - min;
645 Selector::value_with(value, move |i| {
646 let i = to_f32(i.clone());
647 ((i - min) / d).fct()
648 }, move |f| {
649 from_f32((f.0 * d + min).round())
650 })
651 }
652 }
653 }
654 )+
655 };
656}
657impl_32!((|i| i as f32, |f| f as Self) => u32, i32, u16, i16, u8, i8, f32);
658impl_32!((|p: Self| p.0 as f32, |f| Self(f as _)) => Px, Factor, FactorPercent);
659impl_32!((|p: Dip| p.to_f32(), |f| Dip::new_f32(f)) => Dip);
660
661macro_rules! impl_64 {
662 ($($T:ident),+ $(,)?) => {
663 $(
664 impl SelectorValue for $T {
665 fn to_selector(value: BoxedVar<Self>, min: Self, max: Self) -> Selector {
666 let min = min as f64;
667 let max = max as f64;
668 if min >= max {
669 Selector::nil()
670 } else {
671 let d = max - min;
672 Selector::value_with(value, move |&i| {
673 let i = i as f64;
674 Factor(((i - min) / d) as f32)
675 }, move |f| {
676 ((f.0 as f64) * d + min).round() as Self
677 })
678 }
679 }
680 }
681 )+
682 };
683}
684impl_64!(u64, i64, u128, i128, f64);
685
686impl SelectorValue for Length {
687 fn to_selector(value: BoxedVar<Self>, min: Self, max: Self) -> Selector {
688 let (min_f32, max_f32) = LAYOUT.with_context(LayoutMetrics::new(1.fct(), PxSize::splat(Px(1000)), Px(16)), || {
689 (min.layout_f32(LayoutAxis::X), max.layout_f32(LayoutAxis::X))
690 });
691 if min_f32 >= max_f32 {
692 Selector::nil()
693 } else {
694 let d_f32 = max_f32 - min_f32;
695 let d = max - min.clone();
696 Selector::value_with(
697 value,
698 move |l| {
699 let l = LAYOUT.with_context(LayoutMetrics::new(1.fct(), PxSize::splat(Px(1000)), Px(16)), || {
700 l.layout_f32(LayoutAxis::X)
701 });
702 Factor((l - min_f32) / d_f32)
703 },
704 move |f| d.clone() * f + min.clone(),
705 )
706 }
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 fn selector_value_t<T: SelectorValue>(min: T, max: T) {
715 let s = Selector::value(min.clone(), min.clone(), max.clone());
716 assert_eq!(s.to_offset(&min), Some(0.fct()));
717 assert_eq!(s.to_offset(&max), Some(1.fct()));
718 assert_eq!(s.from_offset(0.fct()), Some(min));
719 assert_eq!(s.from_offset(1.fct()), Some(max));
720 }
721
722 #[test]
723 fn selector_value_u8() {
724 selector_value_t(u8::MIN, u8::MAX);
725 selector_value_t(20u8, 120u8);
726 }
727
728 #[test]
729 fn selector_value_i32() {
730 selector_value_t(i32::MIN, i32::MAX);
731 selector_value_t(20i32, 120i32);
732 }
733
734 #[test]
735 fn selector_value_i64() {
736 selector_value_t(i64::MIN, i64::MAX);
737 }
738
739 #[test]
740 fn selector_value_f64() {
741 selector_value_t(-200f64, 200f64);
742 selector_value_t(20f64, 120f64);
743 }
744
745 #[test]
746 fn selector_pct() {
747 selector_value_t(0.pct(), 100.pct());
748 }
749
750 #[test]
751 fn selector_value_px() {
752 selector_value_t(Px(20), Px(200));
753 }
754
755 #[test]
756 fn selector_value_dip() {
757 selector_value_t(Dip::new(20), Dip::new(200));
758 }
759
760 #[test]
761 fn selector_value_length() {
762 selector_value_t(20.px(), 200.px());
763 }
764
765 #[test]
766 fn selector_value_set() {
767 let s = Selector::value(10u8, 0, 100);
768 s.set(10.pct(), 20.pct());
769 assert_eq!(s.thumbs()[0].offset, 0.2.fct());
770 }
771
772 #[test]
773 fn selector_range_set() {
774 let s = Selector::range(10u8..20u8, 0, 100);
775 s.set(0.pct(), 5.pct());
777 assert_eq!(s.thumbs()[0].offset, 0.05.fct());
778 assert_eq!(s.thumbs()[1].offset, 0.2.fct());
779
780 s.set(25.pct(), 30.pct());
782 assert_eq!(s.thumbs()[0].offset, 0.05.fct());
783 assert_eq!(s.thumbs()[1].offset, 0.3.fct());
784
785 s.set(6.pct(), 7.pct());
787 assert_eq!(s.thumbs()[0].offset, 0.07.fct());
788 assert_eq!(s.thumbs()[1].offset, 0.3.fct());
789
790 s.set(7.pct(), 40.pct());
792 assert_eq!(s.thumbs()[0].offset, 0.3.fct());
793 assert_eq!(s.thumbs()[1].offset, 0.4.fct());
794 }
795
796 #[test]
797 fn selector_range_set_eq() {
798 let s = Selector::range(10u8..10, 0, 100);
799 assert_eq!(s.thumbs()[0].offset, 0.1.fct());
800 assert_eq!(s.thumbs()[1].offset, 0.1.fct());
801
802 s.set(10.pct(), 20.pct());
804 assert_eq!(s.thumbs()[0].offset, 0.1.fct());
805 assert_eq!(s.thumbs()[1].offset, 0.2.fct());
806
807 let s = Selector::range(10u8..10, 0, 100);
808 s.set(5.pct(), 5.pct());
809 assert_eq!(s.thumbs()[0].offset, 0.05.fct());
810 assert_eq!(s.thumbs()[1].offset, 0.1.fct());
811 }
812}