1use crate::round_scaling_list::ScalingParams;
20use std::f32::consts::FRAC_PI_2;
21
22pub const INDICATOR_HEIGHT_DP: f32 = 50.0;
25pub const INDICATOR_WIDTH_DP: f32 = 6.0;
28pub const INDICATOR_NARROW_WIDTH_DP: f32 = 5.0;
29pub const INDICATOR_LARGE_SCREEN_DP: f32 = 225.0;
31pub const INDICATOR_EDGE_PADDING_DP: f32 = 2.0;
34pub const INDICATOR_GAP_DP: f32 = 3.0;
37pub const INDICATOR_MIN_THUMB: f32 = 0.3;
40pub const INDICATOR_MAX_THUMB: f32 = 0.7;
41
42pub fn indicator_width_dp(display_dp: f32) -> f32 {
44 if display_dp.is_finite() && display_dp >= INDICATOR_LARGE_SCREEN_DP {
45 INDICATOR_WIDTH_DP
46 } else {
47 INDICATOR_NARROW_WIDTH_DP
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq)]
53pub struct IndicatorArc {
54 centreline: f32,
56 width: f32,
58 half_sweep: f32,
60 segment_inset: f32,
63}
64
65impl IndicatorArc {
66 pub fn centreline(self) -> f32 {
68 self.centreline
69 }
70
71 pub fn width(self) -> f32 {
73 self.width
74 }
75
76 pub fn segment_inset(self) -> f32 {
78 self.segment_inset
79 }
80
81 pub fn start_angle(self) -> f32 {
84 -self.half_sweep
85 }
86
87 pub fn sweep(self) -> f32 {
89 self.half_sweep * 2.0
90 }
91
92 pub fn cap_sweep(self) -> f32 {
98 if self.centreline > 0.0 {
99 self.width / self.centreline
100 } else {
101 0.0
102 }
103 }
104}
105
106fn height_to_sweep(height: f32, radius: f32) -> f32 {
107 if radius <= 0.0 || !radius.is_finite() {
108 return 0.0;
109 }
110 (height * 0.5 / radius).clamp(-1.0, 1.0).asin() * 2.0
111}
112
113pub fn indicator_arc(radius: f32) -> IndicatorArc {
125 let width = indicator_width_dp(radius * 2.0);
126 let usable_radius = radius - INDICATOR_EDGE_PADDING_DP;
127 let centreline = usable_radius - width * 0.5;
128 if centreline <= 0.0 || !centreline.is_finite() {
129 return IndicatorArc {
130 centreline: 0.0,
131 width,
132 half_sweep: 0.0,
133 segment_inset: 0.0,
134 };
135 }
136 let segment_inset = height_to_sweep(width + INDICATOR_GAP_DP, usable_radius);
137 let half_sweep = ((height_to_sweep(INDICATOR_HEIGHT_DP, usable_radius) + segment_inset) * 0.5)
138 .min(FRAC_PI_2);
139 IndicatorArc {
140 centreline,
141 width,
142 half_sweep,
143 segment_inset,
144 }
145}
146
147#[derive(Clone, Copy, Debug, PartialEq)]
150pub struct IndicatorGeometry {
151 pub thumb: f32,
153 pub offset: f32,
155}
156
157pub fn indicator_geometry(content: f32, viewport: f32, scrolled: f32) -> Option<IndicatorGeometry> {
169 if !(content.is_finite() && viewport.is_finite() && scrolled.is_finite()) {
170 return None;
171 }
172 if viewport <= 0.0 || content <= viewport {
173 return None;
174 }
175 let thumb = (viewport / content).clamp(INDICATOR_MIN_THUMB, INDICATOR_MAX_THUMB);
176 let travel = content - viewport;
177 let progress = (scrolled / travel).clamp(0.0, 1.0);
178 Some(IndicatorGeometry {
179 thumb,
180 offset: progress * (1.0 - thumb),
181 })
182}
183
184#[derive(Clone, Copy, Debug, PartialEq)]
195pub struct IndicatorItem {
196 pub index: usize,
198 pub start_offset: f32,
201 pub size: f32,
206}
207
208#[derive(Clone, Copy, Debug, PartialEq)]
210pub struct ScalingList<'a> {
211 pub visible: &'a [IndicatorItem],
216 pub total: usize,
219 pub viewport: f32,
221 pub before_padding: f32,
225 pub after_padding: f32,
228}
229
230pub fn decimal_first_item_index(list: ScalingList<'_>) -> f32 {
237 let Some(first) = list.visible.first() else {
238 return 0.0;
239 };
240 let offset_from_start = if first.index == 0 {
241 list.before_padding
242 } else {
243 0.0
244 };
245 let start = first.start_offset - offset_from_start;
246 let top = -(list.viewport / 2.0);
247 let fraction = ((top - start) / (first.size + offset_from_start).max(1.0)).max(0.0);
248 finite(first.index as f32 + fraction)
249}
250
251pub fn decimal_last_item_index(list: ScalingList<'_>) -> f32 {
256 let Some(last) = list.visible.last() else {
257 return 0.0;
258 };
259 let span = last.size
260 + if last.index + 1 == list.total {
261 list.after_padding
262 } else {
263 0.0
264 };
265 let end = last.start_offset + span;
266 let bottom = list.viewport / 2.0;
267 let fraction = (1.0 - (end - bottom) / span.max(1.0)).min(1.0);
268 finite(last.index as f32 + fraction)
269}
270
271pub fn position_fraction(list: ScalingList<'_>) -> f32 {
279 if list.visible.is_empty() {
280 return 0.0;
281 }
282 let first = decimal_first_item_index(list);
283 let remaining = list.total as f32 - decimal_last_item_index(list);
284 if first + remaining == 0.0 {
285 0.0
286 } else {
287 finite(first / (first + remaining))
288 }
289}
290
291#[derive(Clone, Copy, Debug, Default, PartialEq)]
305pub struct ThumbLength {
306 fraction: f32,
307 items: usize,
308}
309
310impl ThumbLength {
311 pub fn of(&mut self, list: ScalingList<'_>) -> f32 {
313 if list.visible.is_empty() {
314 return 0.0;
315 }
316 if self.items != list.total {
317 self.items = list.total;
318 let span = decimal_last_item_index(list) - decimal_first_item_index(list);
319 let share = span / list.total.max(1) as f32;
320 self.fraction = if share.is_finite() {
321 share.clamp(INDICATOR_MIN_THUMB, INDICATOR_MAX_THUMB)
322 } else {
323 INDICATOR_MIN_THUMB
324 };
325 }
326 self.fraction
327 }
328
329 pub fn forget(&mut self) {
331 *self = Self::default();
332 }
333}
334
335pub fn scaling_list_geometry(
349 thumb: &mut ThumbLength,
350 list: ScalingList<'_>,
351) -> Option<IndicatorGeometry> {
352 if list.visible.is_empty() || list.total == 0 || !list.viewport.is_finite() {
353 return None;
354 }
355 let size = thumb.of(list);
356 let position = position_fraction(list).clamp(0.0, 1.0);
357 Some(IndicatorGeometry {
358 thumb: size,
359 offset: position * (1.0 - size),
360 })
361}
362
363pub fn scaling_list_items<I>(viewport: f32, density: f32, rows: I, out: &mut Vec<IndicatorItem>)
379where
380 I: IntoIterator<Item = (f32, f32)>,
381{
382 scaling_list_items_with(ScalingParams::WEAR, viewport, density, rows, out)
383}
384
385pub fn scaling_list_items_with<I>(
394 params: ScalingParams,
395 viewport: f32,
396 density: f32,
397 rows: I,
398 out: &mut Vec<IndicatorItem>,
399) where
400 I: IntoIterator<Item = (f32, f32)>,
401{
402 out.clear();
403 if !viewport.is_finite() || !density.is_finite() {
404 return;
405 }
406 let pixels = density > 0.0;
410 let to_px = |value: f32| if pixels { value * density } else { value };
411 let round_px = |value: f32| if pixels { value.round() } else { value };
412 let viewport_px = round_px(to_px(viewport));
413 let centre_line = if pixels {
416 (viewport_px * 0.5).floor()
417 } else {
418 viewport_px * 0.5
419 };
420 for (index, (top, height)) in rows.into_iter().enumerate() {
421 let Some(placed) =
422 crate::round_scaling_list::place_row_with(params, viewport, top, height, density)
423 else {
424 continue;
425 };
426 let height_px = round_px(to_px(height));
427 let size = round_px(height_px * placed.scale);
428 let drawn_top = to_px(placed.top);
438 let carried = if pixels { odd_pixel(height_px) } else { 0.0 };
439 let stacked_top = drawn_top - carried + if pixels { odd_pixel(size) } else { 0.0 };
440 if stacked_top > viewport_px || stacked_top + size < 0.0 {
441 if out.is_empty() {
442 continue;
443 }
444 break;
445 }
446 out.push(IndicatorItem {
447 index,
448 start_offset: drawn_top - carried - centre_line,
449 size,
450 });
451 }
452}
453
454fn odd_pixel(pixels: f32) -> f32 {
456 let half = pixels * 0.5;
457 half - half.floor()
458}
459
460fn finite(value: f32) -> f32 {
461 if value.is_finite() {
462 value
463 } else {
464 0.0
465 }
466}
467
468#[derive(Clone, Copy, Debug, PartialEq)]
474pub enum IndicatorSegment {
475 Arc { start: f32, sweep: f32, alpha: f32 },
478 Dot {
480 angle: f32,
482 radius: f32,
484 alpha: f32,
485 },
486}
487
488#[derive(Clone, Copy, Debug, PartialEq, Eq)]
491pub enum IndicatorPart {
492 Track,
493 Thumb,
494}
495
496pub fn indicator_segments(
505 arc: IndicatorArc,
506 geometry: IndicatorGeometry,
507 alpha: f32,
508) -> [(IndicatorPart, IndicatorSegment); 3] {
509 let alpha = if alpha.is_finite() {
510 alpha.clamp(0.0, 1.0)
511 } else {
512 0.0
513 };
514 let thumb = if geometry.thumb.is_finite() {
515 geometry.thumb.clamp(0.0, 1.0)
516 } else {
517 0.0
518 };
519 let offset = if geometry.offset.is_finite() {
520 geometry.offset.clamp(0.0, 1.0 - thumb)
521 } else {
522 0.0
523 };
524 let sweep = arc.sweep();
525 let top = arc.start_angle();
526 let thumb_start = top + sweep * offset;
527 let thumb_sweep = sweep * thumb;
528 let below_start = thumb_start + thumb_sweep;
529 [
530 (
531 IndicatorPart::Track,
532 segment(top, thumb_start - top, arc.width, arc.segment_inset, alpha),
533 ),
534 (
535 IndicatorPart::Thumb,
536 segment(
537 thumb_start,
538 thumb_sweep,
539 arc.width,
540 arc.segment_inset,
541 alpha,
542 ),
543 ),
544 (
545 IndicatorPart::Track,
546 segment(
547 below_start,
548 top + sweep - below_start,
549 arc.width,
550 arc.segment_inset,
551 alpha,
552 ),
553 ),
554 ]
555}
556
557fn segment(start: f32, sweep: f32, width: f32, inset: f32, alpha: f32) -> IndicatorSegment {
559 if sweep <= 0.0 || inset <= 0.0 {
560 return IndicatorSegment::Arc {
561 start,
562 sweep: 0.0,
563 alpha: 0.0,
564 };
565 }
566 if sweep < inset {
567 let fill = sweep / inset;
571 return IndicatorSegment::Dot {
572 angle: start + sweep * 0.5,
573 radius: width * 0.5 * fill,
574 alpha: alpha * fill,
575 };
576 }
577 IndicatorSegment::Arc {
580 start: start + inset * 0.5,
581 sweep: sweep - inset,
582 alpha,
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 const LARGE_RADIUS_DP: f32 = 113.5; const SMALL_RADIUS_DP: f32 = 96.0; #[test]
595 fn stroke_width_switches_at_the_wear_large_screen_breakpoint() {
596 assert_eq!(indicator_width_dp(224.99), INDICATOR_NARROW_WIDTH_DP);
597 assert_eq!(indicator_width_dp(225.0), INDICATOR_WIDTH_DP);
598 assert_eq!(indicator_width_dp(f32::NAN), INDICATOR_NARROW_WIDTH_DP);
599 }
600
601 #[test]
602 fn the_track_lands_where_the_shipping_compose_build_draws_it() {
603 let large = indicator_arc(LARGE_RADIUS_DP);
607 assert!((large.centreline() - 108.5).abs() < 0.01, "{large:?}");
608 assert!((large.width() - 6.0).abs() < 0.01, "{large:?}");
609
610 let small = indicator_arc(SMALL_RADIUS_DP);
611 assert!((small.centreline() - 91.5).abs() < 0.01, "{small:?}");
612 assert!((small.width() - 5.0).abs() < 0.01, "{small:?}");
613 }
614
615 #[test]
616 fn the_sweep_is_a_height_in_dp_not_a_fixed_angle() {
617 let large = indicator_arc(LARGE_RADIUS_DP).sweep().to_degrees();
620 let small = indicator_arc(SMALL_RADIUS_DP).sweep().to_degrees();
621 assert!((large - 30.54).abs() < 0.05, "{large}");
622 assert!((small - 35.73).abs() < 0.05, "{small}");
623 assert!(small > large);
624 }
625
626 #[test]
627 fn a_list_that_fits_on_screen_shows_no_indicator_at_all() {
628 assert_eq!(indicator_geometry(100.0, 100.0, 0.0), None);
629 assert_eq!(indicator_geometry(80.0, 100.0, 0.0), None);
630 assert_eq!(indicator_geometry(f32::NAN, 100.0, 0.0), None);
631 assert_eq!(indicator_geometry(200.0, 0.0, 0.0), None);
632 }
633
634 #[test]
635 fn the_thumb_is_the_viewport_share_clamped_at_both_ends() {
636 let half = indicator_geometry(200.0, 100.0, 0.0).unwrap();
638 assert!((half.thumb - 0.5).abs() < 1e-6, "{half:?}");
639 let long = indicator_geometry(10_000.0, 100.0, 0.0).unwrap();
642 assert!((long.thumb - INDICATOR_MIN_THUMB).abs() < 1e-6, "{long:?}");
643 let short = indicator_geometry(105.0, 100.0, 0.0).unwrap();
644 assert!(
645 (short.thumb - INDICATOR_MAX_THUMB).abs() < 1e-6,
646 "{short:?}"
647 );
648 }
649
650 #[test]
651 fn the_thumb_reaches_the_bottom_of_the_track_and_no_further() {
652 let bottom = indicator_geometry(200.0, 100.0, 100.0).unwrap();
653 assert!(
654 (bottom.offset + bottom.thumb - 1.0).abs() < 1e-6,
655 "{bottom:?}"
656 );
657 let past = indicator_geometry(200.0, 100.0, 500.0).unwrap();
659 assert_eq!(past, bottom);
660 }
661
662 #[test]
663 fn the_indicator_is_three_segments_with_a_gap_either_side_of_the_thumb() {
664 let arc = indicator_arc(LARGE_RADIUS_DP);
665 let geometry = IndicatorGeometry {
666 thumb: 0.4,
667 offset: 0.3,
668 };
669 let parts = indicator_segments(arc, geometry, 1.0);
670 assert_eq!(parts[0].0, IndicatorPart::Track);
671 assert_eq!(parts[1].0, IndicatorPart::Thumb);
672 assert_eq!(parts[2].0, IndicatorPart::Track);
673
674 let ink_bounds = |segment: IndicatorSegment| match segment {
678 IndicatorSegment::Arc { start, sweep, .. } => {
679 (start - arc.cap_sweep() * 0.5, sweep + arc.cap_sweep())
680 }
681 other => panic!("expected an arc, got {other:?}"),
682 };
683 let (above_start, above_sweep) = ink_bounds(parts[0].1);
684 let (thumb_start, thumb_sweep) = ink_bounds(parts[1].1);
685 let (below_start, below_sweep) = ink_bounds(parts[2].1);
686 let gap = arc.segment_inset() - arc.cap_sweep();
687
688 assert!((above_start - arc.start_angle() - gap * 0.5).abs() < 1e-4);
689 assert!((thumb_start - (above_start + above_sweep) - gap).abs() < 1e-4);
690 assert!((below_start - (thumb_start + thumb_sweep) - gap).abs() < 1e-4);
691 assert!(
692 (below_start + below_sweep + gap * 0.5 - (arc.start_angle() + arc.sweep())).abs()
693 < 1e-4,
694 "the track has to end where it should"
695 );
696 }
697
698 #[test]
699 fn a_segment_shorter_than_its_stroke_becomes_a_shrinking_dot() {
700 let arc = indicator_arc(LARGE_RADIUS_DP);
701 let parts = indicator_segments(
703 arc,
704 IndicatorGeometry {
705 thumb: 0.7,
706 offset: 0.0,
707 },
708 1.0,
709 );
710 match parts[0].1 {
711 IndicatorSegment::Dot { radius, alpha, .. } => {
712 assert!(
713 radius <= arc.width() * 0.5,
714 "a dot never exceeds the stroke"
715 );
716 assert!(alpha < 1.0, "it fades on the same fraction as it shrinks");
717 }
718 IndicatorSegment::Arc { sweep, .. } => {
719 assert!(sweep <= 0.0, "an arc this short should have been a dot");
720 }
721 }
722 }
723
724 #[test]
725 fn fading_the_indicator_fades_every_piece_of_it() {
726 let arc = indicator_arc(LARGE_RADIUS_DP);
727 let geometry = IndicatorGeometry {
728 thumb: 0.4,
729 offset: 0.3,
730 };
731 for (_, segment) in indicator_segments(arc, geometry, 0.25) {
732 let alpha = match segment {
733 IndicatorSegment::Arc { alpha, .. } => alpha,
734 IndicatorSegment::Dot { alpha, .. } => alpha,
735 };
736 assert!(alpha <= 0.25 + 1e-6, "{segment:?}");
737 }
738 }
739
740 #[test]
741 fn a_display_too_small_to_hold_the_track_degrades_instead_of_panicking() {
742 let tiny = indicator_arc(1.0);
743 assert_eq!(tiny.centreline(), 0.0);
744 assert_eq!(tiny.sweep(), 0.0);
745 assert_eq!(tiny.cap_sweep(), 0.0);
746 let parts = indicator_segments(
748 tiny,
749 IndicatorGeometry {
750 thumb: 0.4,
751 offset: 0.3,
752 },
753 1.0,
754 );
755 for (_, segment) in parts {
756 assert!(matches!(segment, IndicatorSegment::Arc { sweep: 0.0, .. }));
757 }
758 }
759
760 const VIEWPORT: f32 = 400.0;
764
765 fn list<'a>(visible: &'a [IndicatorItem]) -> ScalingList<'a> {
766 ScalingList {
767 visible,
768 total: 10,
769 viewport: VIEWPORT,
770 before_padding: 0.0,
771 after_padding: 0.0,
772 }
773 }
774
775 fn row(index: usize, start_offset: f32) -> IndicatorItem {
776 IndicatorItem {
777 index,
778 start_offset,
779 size: 100.0,
780 }
781 }
782
783 #[test]
784 fn a_row_flush_with_the_top_of_the_screen_is_a_whole_index() {
785 let rows = [row(3, -200.0), row(6, 100.0)];
788 assert_eq!(decimal_first_item_index(list(&rows)), 3.0);
789 }
790
791 #[test]
792 fn a_row_half_off_the_top_reads_half_an_index() {
793 let rows = [row(3, -250.0), row(6, 100.0)];
797 assert_eq!(decimal_first_item_index(list(&rows)), 3.5);
798 }
799
800 #[test]
801 fn the_last_index_counts_how_much_of_the_row_is_on_screen() {
802 let rows = [row(3, -200.0), row(6, 150.0)];
805 assert_eq!(decimal_last_item_index(list(&rows)), 6.5);
806 }
807
808 #[test]
809 fn the_padding_outside_the_list_counts_only_at_the_end_it_belongs_to() {
810 let rows = [row(0, -250.0), row(9, 150.0)];
811 let padded = ScalingList {
812 before_padding: 80.0,
813 after_padding: 60.0,
814 ..list(&rows)
815 };
816 assert!((decimal_first_item_index(padded) - 130.0 / 180.0).abs() < 1e-6);
819 assert!((decimal_last_item_index(padded) - (9.0 + 0.3125)).abs() < 1e-6);
821
822 let inner = [row(3, -250.0), row(6, 150.0)];
824 let inner = ScalingList {
825 before_padding: 80.0,
826 after_padding: 60.0,
827 ..list(&inner)
828 };
829 assert_eq!(decimal_first_item_index(inner), 3.5);
830 assert_eq!(decimal_last_item_index(inner), 6.5);
831 }
832
833 #[test]
834 fn the_thumb_is_the_share_of_the_items_on_screen_not_of_the_pixels() {
835 let rows = [row(3, -250.0), row(8, 150.0)];
838 let mut thumb = ThumbLength::default();
839 assert!((thumb.of(list(&rows)) - 0.5).abs() < 1e-6);
840 }
841
842 #[test]
843 fn the_thumb_is_clamped_at_both_ends_however_long_the_list_is() {
844 let rows = [row(3, -250.0), row(4, 150.0)];
845 let mut short = ThumbLength::default();
846 assert_eq!(short.of(list(&rows)), INDICATOR_MIN_THUMB);
847
848 let rows = [row(0, -250.0), row(9, 150.0)];
849 let mut long = ThumbLength::default();
850 assert_eq!(long.of(list(&rows)), INDICATOR_MAX_THUMB);
851 }
852
853 #[test]
854 fn the_thumb_is_measured_once_and_then_only_when_the_list_changes_length() {
855 let mut thumb = ThumbLength::default();
859 let five = [row(3, -250.0), row(8, 150.0)];
860 assert!((thumb.of(list(&five)) - 0.5).abs() < 1e-6);
861
862 let three = [row(3, -250.0), row(6, 150.0)];
863 assert!(
864 (thumb.of(list(&three)) - 0.5).abs() < 1e-6,
865 "the window shrank but the list did not, so the thumb holds"
866 );
867
868 let longer = ScalingList {
869 total: 20,
870 ..list(&three)
871 };
872 assert_eq!(thumb.of(longer), INDICATOR_MIN_THUMB);
873
874 thumb.forget();
875 assert!((thumb.of(list(&five)) - 0.5).abs() < 1e-6);
876 }
877
878 #[test]
879 fn the_position_is_how_many_items_are_left_not_how_far_the_pixels_went() {
880 let rows = [row(3, -250.0), row(6, 150.0)];
882 assert!((position_fraction(list(&rows)) - 0.5).abs() < 1e-6);
883 }
884
885 #[test]
886 fn a_list_at_the_top_puts_the_thumb_at_the_top_and_one_at_the_end_at_the_end() {
887 let mut thumb = ThumbLength::default();
888 let top = [row(0, -200.0), row(3, 150.0)];
889 let geometry = scaling_list_geometry(&mut thumb, list(&top)).unwrap();
890 assert_eq!(geometry.offset, 0.0);
891
892 let mut thumb = ThumbLength::default();
895 let end = [row(6, -250.0), row(9, 100.0)];
896 let geometry = scaling_list_geometry(&mut thumb, list(&end)).unwrap();
897 assert_eq!(decimal_last_item_index(list(&end)), 10.0);
898 assert!((geometry.offset + geometry.thumb - 1.0).abs() < 1e-6);
899 }
900
901 #[test]
902 fn a_list_with_nothing_on_screen_has_no_indicator() {
903 let mut thumb = ThumbLength::default();
904 assert_eq!(scaling_list_geometry(&mut thumb, list(&[])), None);
905 let rows = [row(3, -250.0)];
906 let empty = ScalingList {
907 total: 0,
908 ..list(&rows)
909 };
910 assert_eq!(scaling_list_geometry(&mut thumb, empty), None);
911 }
912
913 #[test]
914 fn the_two_models_disagree_the_moment_the_rows_are_not_all_the_same_height() {
915 let heights: Vec<f32> = std::iter::once(600.0).chain([100.0; 9]).collect();
922 let content: f32 = heights.iter().sum();
923 let pixel = indicator_geometry(content, VIEWPORT, 0.0).unwrap();
924
925 let rows = [row(0, -200.0), row(3, 100.0)];
926 let mut thumb = ThumbLength::default();
927 let wear = scaling_list_geometry(&mut thumb, list(&rows)).unwrap();
928
929 assert!((pixel.thumb - INDICATOR_MIN_THUMB).abs() < 1e-6);
930 assert!((wear.thumb - 0.4).abs() < 1e-6, "{wear:?}");
931 }
932
933 #[test]
934 fn a_reported_row_is_not_the_row_as_it_is_drawn() {
935 let mut out = Vec::new();
939 let density = 2.0;
940 let viewport = 227.0;
941 for (height, carried) in [(51.5, 0.5), (52.0, 0.0)] {
942 scaling_list_items(viewport, density, [(20.0, height)], &mut out);
943 let drawn = crate::round_scaling_list::place_row(viewport, 20.0, height, density)
944 .expect("placed");
945 let item = out.first().expect("on screen");
946 assert!(
947 (item.start_offset - (drawn.top * density - carried - 227.0)).abs() < 1e-4,
948 "{height}dp: reported {} against drawn {}",
949 item.start_offset,
950 drawn.top * density
951 );
952 assert_eq!(item.size, (drawn.height * density).round());
955 }
956 }
957
958 #[test]
959 fn a_list_that_does_not_scale_its_rows_reports_them_at_full_height() {
960 let mut wear = Vec::new();
966 let mut still = Vec::new();
967 let rows = [(4.0, 52.0), (60.0, 52.0), (116.0, 52.0)];
968 scaling_list_items(227.0, 2.0, rows, &mut wear);
969 scaling_list_items_with(
970 ScalingParams::WEAR.reduced_motion(),
971 227.0,
972 2.0,
973 rows,
974 &mut still,
975 );
976 assert_eq!(wear.len(), still.len());
977 assert!(
978 wear[0].size < still[0].size,
979 "the top row shrinks under the Wear ramp and not under a stilled \
980 one: {} vs {}",
981 wear[0].size,
982 still[0].size
983 );
984 assert_eq!(still[0].size, 104.0, "52dp at density 2, unscaled");
985 let mut default = Vec::new();
987 scaling_list_items_with(ScalingParams::WEAR, 227.0, 2.0, rows, &mut default);
988 assert_eq!(default, wear);
989 }
990
991 #[test]
992 fn the_window_is_the_rows_that_still_meet_the_display() {
993 let mut out = Vec::new();
998 let rows: Vec<(f32, f32)> = (0..10)
999 .map(|index| (index as f32 * 40.0 - 100.0, 40.0))
1000 .collect();
1001 scaling_list_items(227.0, 2.0, rows.iter().copied(), &mut out);
1002 let indices: Vec<usize> = out.iter().map(|item| item.index).collect();
1003 assert_eq!(indices, vec![2, 3, 4, 5, 6, 7, 8]);
1004 }
1005
1006 #[test]
1007 fn invalid_scaling_list_input_never_produces_a_non_finite_thumb() {
1008 let mut out = Vec::new();
1009 scaling_list_items(f32::NAN, 2.0, [(0.0, 40.0)], &mut out);
1010 assert!(out.is_empty());
1011 scaling_list_items(227.0, f32::NAN, [(0.0, 40.0)], &mut out);
1012 assert!(out.is_empty());
1013
1014 let rows = [
1015 IndicatorItem {
1016 index: 0,
1017 start_offset: f32::NAN,
1018 size: 0.0,
1019 },
1020 IndicatorItem {
1021 index: 3,
1022 start_offset: f32::INFINITY,
1023 size: -1.0,
1024 },
1025 ];
1026 let mut thumb = ThumbLength::default();
1027 let geometry = scaling_list_geometry(&mut thumb, list(&rows)).expect("a geometry");
1028 assert!(
1029 geometry.thumb.is_finite() && geometry.offset.is_finite(),
1030 "{geometry:?}"
1031 );
1032 assert!(geometry.thumb >= INDICATOR_MIN_THUMB && geometry.thumb <= INDICATOR_MAX_THUMB);
1033 assert!(geometry.offset >= 0.0 && geometry.offset <= 1.0);
1034 }
1035
1036 #[test]
1037 fn invalid_public_inputs_never_emit_non_finite_draw_values() {
1038 for radius in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1.0] {
1039 let arc = indicator_arc(radius);
1040 assert_eq!(arc.sweep(), 0.0);
1041 assert_eq!(arc.segment_inset(), 0.0);
1042 }
1043
1044 let parts = indicator_segments(
1045 indicator_arc(LARGE_RADIUS_DP),
1046 IndicatorGeometry {
1047 thumb: f32::NAN,
1048 offset: f32::INFINITY,
1049 },
1050 f32::NAN,
1051 );
1052 for (_, part) in parts {
1053 match part {
1054 IndicatorSegment::Arc {
1055 start,
1056 sweep,
1057 alpha,
1058 } => assert!(start.is_finite() && sweep.is_finite() && alpha == 0.0),
1059 IndicatorSegment::Dot {
1060 angle,
1061 radius,
1062 alpha,
1063 } => assert!(angle.is_finite() && radius.is_finite() && alpha == 0.0),
1064 }
1065 }
1066 }
1067}