use crate::round_scaling_list::ScalingParams;
use std::f32::consts::FRAC_PI_2;
pub const INDICATOR_HEIGHT_DP: f32 = 50.0;
pub const INDICATOR_WIDTH_DP: f32 = 6.0;
pub const INDICATOR_NARROW_WIDTH_DP: f32 = 5.0;
pub const INDICATOR_LARGE_SCREEN_DP: f32 = 225.0;
pub const INDICATOR_EDGE_PADDING_DP: f32 = 2.0;
pub const INDICATOR_GAP_DP: f32 = 3.0;
pub const INDICATOR_MIN_THUMB: f32 = 0.3;
pub const INDICATOR_MAX_THUMB: f32 = 0.7;
pub fn indicator_width_dp(display_dp: f32) -> f32 {
if display_dp.is_finite() && display_dp >= INDICATOR_LARGE_SCREEN_DP {
INDICATOR_WIDTH_DP
} else {
INDICATOR_NARROW_WIDTH_DP
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IndicatorArc {
centreline: f32,
width: f32,
half_sweep: f32,
segment_inset: f32,
}
impl IndicatorArc {
pub fn centreline(self) -> f32 {
self.centreline
}
pub fn width(self) -> f32 {
self.width
}
pub fn segment_inset(self) -> f32 {
self.segment_inset
}
pub fn start_angle(self) -> f32 {
-self.half_sweep
}
pub fn sweep(self) -> f32 {
self.half_sweep * 2.0
}
pub fn cap_sweep(self) -> f32 {
if self.centreline > 0.0 {
self.width / self.centreline
} else {
0.0
}
}
}
fn height_to_sweep(height: f32, radius: f32) -> f32 {
if radius <= 0.0 || !radius.is_finite() {
return 0.0;
}
(height * 0.5 / radius).clamp(-1.0, 1.0).asin() * 2.0
}
pub fn indicator_arc(radius: f32) -> IndicatorArc {
let width = indicator_width_dp(radius * 2.0);
let usable_radius = radius - INDICATOR_EDGE_PADDING_DP;
let centreline = usable_radius - width * 0.5;
if centreline <= 0.0 || !centreline.is_finite() {
return IndicatorArc {
centreline: 0.0,
width,
half_sweep: 0.0,
segment_inset: 0.0,
};
}
let segment_inset = height_to_sweep(width + INDICATOR_GAP_DP, usable_radius);
let half_sweep = ((height_to_sweep(INDICATOR_HEIGHT_DP, usable_radius) + segment_inset) * 0.5)
.min(FRAC_PI_2);
IndicatorArc {
centreline,
width,
half_sweep,
segment_inset,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IndicatorGeometry {
pub thumb: f32,
pub offset: f32,
}
pub fn indicator_geometry(content: f32, viewport: f32, scrolled: f32) -> Option<IndicatorGeometry> {
if !(content.is_finite() && viewport.is_finite() && scrolled.is_finite()) {
return None;
}
if viewport <= 0.0 || content <= viewport {
return None;
}
let thumb = (viewport / content).clamp(INDICATOR_MIN_THUMB, INDICATOR_MAX_THUMB);
let travel = content - viewport;
let progress = (scrolled / travel).clamp(0.0, 1.0);
Some(IndicatorGeometry {
thumb,
offset: progress * (1.0 - thumb),
})
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IndicatorItem {
pub index: usize,
pub start_offset: f32,
pub size: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScalingList<'a> {
pub visible: &'a [IndicatorItem],
pub total: usize,
pub viewport: f32,
pub before_padding: f32,
pub after_padding: f32,
}
pub fn decimal_first_item_index(list: ScalingList<'_>) -> f32 {
let Some(first) = list.visible.first() else {
return 0.0;
};
let offset_from_start = if first.index == 0 {
list.before_padding
} else {
0.0
};
let start = first.start_offset - offset_from_start;
let top = -(list.viewport / 2.0);
let fraction = ((top - start) / (first.size + offset_from_start).max(1.0)).max(0.0);
finite(first.index as f32 + fraction)
}
pub fn decimal_last_item_index(list: ScalingList<'_>) -> f32 {
let Some(last) = list.visible.last() else {
return 0.0;
};
let span = last.size
+ if last.index + 1 == list.total {
list.after_padding
} else {
0.0
};
let end = last.start_offset + span;
let bottom = list.viewport / 2.0;
let fraction = (1.0 - (end - bottom) / span.max(1.0)).min(1.0);
finite(last.index as f32 + fraction)
}
pub fn position_fraction(list: ScalingList<'_>) -> f32 {
if list.visible.is_empty() {
return 0.0;
}
let first = decimal_first_item_index(list);
let remaining = list.total as f32 - decimal_last_item_index(list);
if first + remaining == 0.0 {
0.0
} else {
finite(first / (first + remaining))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ThumbLength {
fraction: f32,
items: usize,
}
impl ThumbLength {
pub fn of(&mut self, list: ScalingList<'_>) -> f32 {
if list.visible.is_empty() {
return 0.0;
}
if self.items != list.total {
self.items = list.total;
let span = decimal_last_item_index(list) - decimal_first_item_index(list);
let share = span / list.total.max(1) as f32;
self.fraction = if share.is_finite() {
share.clamp(INDICATOR_MIN_THUMB, INDICATOR_MAX_THUMB)
} else {
INDICATOR_MIN_THUMB
};
}
self.fraction
}
pub fn forget(&mut self) {
*self = Self::default();
}
}
pub fn scaling_list_geometry(
thumb: &mut ThumbLength,
list: ScalingList<'_>,
) -> Option<IndicatorGeometry> {
if list.visible.is_empty() || list.total == 0 || !list.viewport.is_finite() {
return None;
}
let size = thumb.of(list);
let position = position_fraction(list).clamp(0.0, 1.0);
Some(IndicatorGeometry {
thumb: size,
offset: position * (1.0 - size),
})
}
pub fn scaling_list_items<I>(viewport: f32, density: f32, rows: I, out: &mut Vec<IndicatorItem>)
where
I: IntoIterator<Item = (f32, f32)>,
{
scaling_list_items_with(ScalingParams::WEAR, viewport, density, rows, out)
}
pub fn scaling_list_items_with<I>(
params: ScalingParams,
viewport: f32,
density: f32,
rows: I,
out: &mut Vec<IndicatorItem>,
) where
I: IntoIterator<Item = (f32, f32)>,
{
out.clear();
if !viewport.is_finite() || !density.is_finite() {
return;
}
let pixels = density > 0.0;
let to_px = |value: f32| if pixels { value * density } else { value };
let round_px = |value: f32| if pixels { value.round() } else { value };
let viewport_px = round_px(to_px(viewport));
let centre_line = if pixels {
(viewport_px * 0.5).floor()
} else {
viewport_px * 0.5
};
for (index, (top, height)) in rows.into_iter().enumerate() {
let Some(placed) =
crate::round_scaling_list::place_row_with(params, viewport, top, height, density)
else {
continue;
};
let height_px = round_px(to_px(height));
let size = round_px(height_px * placed.scale);
let drawn_top = to_px(placed.top);
let carried = if pixels { odd_pixel(height_px) } else { 0.0 };
let stacked_top = drawn_top - carried + if pixels { odd_pixel(size) } else { 0.0 };
if stacked_top > viewport_px || stacked_top + size < 0.0 {
if out.is_empty() {
continue;
}
break;
}
out.push(IndicatorItem {
index,
start_offset: drawn_top - carried - centre_line,
size,
});
}
}
fn odd_pixel(pixels: f32) -> f32 {
let half = pixels * 0.5;
half - half.floor()
}
fn finite(value: f32) -> f32 {
if value.is_finite() {
value
} else {
0.0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum IndicatorSegment {
Arc { start: f32, sweep: f32, alpha: f32 },
Dot {
angle: f32,
radius: f32,
alpha: f32,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IndicatorPart {
Track,
Thumb,
}
pub fn indicator_segments(
arc: IndicatorArc,
geometry: IndicatorGeometry,
alpha: f32,
) -> [(IndicatorPart, IndicatorSegment); 3] {
let alpha = if alpha.is_finite() {
alpha.clamp(0.0, 1.0)
} else {
0.0
};
let thumb = if geometry.thumb.is_finite() {
geometry.thumb.clamp(0.0, 1.0)
} else {
0.0
};
let offset = if geometry.offset.is_finite() {
geometry.offset.clamp(0.0, 1.0 - thumb)
} else {
0.0
};
let sweep = arc.sweep();
let top = arc.start_angle();
let thumb_start = top + sweep * offset;
let thumb_sweep = sweep * thumb;
let below_start = thumb_start + thumb_sweep;
[
(
IndicatorPart::Track,
segment(top, thumb_start - top, arc.width, arc.segment_inset, alpha),
),
(
IndicatorPart::Thumb,
segment(
thumb_start,
thumb_sweep,
arc.width,
arc.segment_inset,
alpha,
),
),
(
IndicatorPart::Track,
segment(
below_start,
top + sweep - below_start,
arc.width,
arc.segment_inset,
alpha,
),
),
]
}
fn segment(start: f32, sweep: f32, width: f32, inset: f32, alpha: f32) -> IndicatorSegment {
if sweep <= 0.0 || inset <= 0.0 {
return IndicatorSegment::Arc {
start,
sweep: 0.0,
alpha: 0.0,
};
}
if sweep < inset {
let fill = sweep / inset;
return IndicatorSegment::Dot {
angle: start + sweep * 0.5,
radius: width * 0.5 * fill,
alpha: alpha * fill,
};
}
IndicatorSegment::Arc {
start: start + inset * 0.5,
sweep: sweep - inset,
alpha,
}
}
#[cfg(test)]
mod tests {
use super::*;
const LARGE_RADIUS_DP: f32 = 113.5; const SMALL_RADIUS_DP: f32 = 96.0;
#[test]
fn stroke_width_switches_at_the_wear_large_screen_breakpoint() {
assert_eq!(indicator_width_dp(224.99), INDICATOR_NARROW_WIDTH_DP);
assert_eq!(indicator_width_dp(225.0), INDICATOR_WIDTH_DP);
assert_eq!(indicator_width_dp(f32::NAN), INDICATOR_NARROW_WIDTH_DP);
}
#[test]
fn the_track_lands_where_the_shipping_compose_build_draws_it() {
let large = indicator_arc(LARGE_RADIUS_DP);
assert!((large.centreline() - 108.5).abs() < 0.01, "{large:?}");
assert!((large.width() - 6.0).abs() < 0.01, "{large:?}");
let small = indicator_arc(SMALL_RADIUS_DP);
assert!((small.centreline() - 91.5).abs() < 0.01, "{small:?}");
assert!((small.width() - 5.0).abs() < 0.01, "{small:?}");
}
#[test]
fn the_sweep_is_a_height_in_dp_not_a_fixed_angle() {
let large = indicator_arc(LARGE_RADIUS_DP).sweep().to_degrees();
let small = indicator_arc(SMALL_RADIUS_DP).sweep().to_degrees();
assert!((large - 30.54).abs() < 0.05, "{large}");
assert!((small - 35.73).abs() < 0.05, "{small}");
assert!(small > large);
}
#[test]
fn a_list_that_fits_on_screen_shows_no_indicator_at_all() {
assert_eq!(indicator_geometry(100.0, 100.0, 0.0), None);
assert_eq!(indicator_geometry(80.0, 100.0, 0.0), None);
assert_eq!(indicator_geometry(f32::NAN, 100.0, 0.0), None);
assert_eq!(indicator_geometry(200.0, 0.0, 0.0), None);
}
#[test]
fn the_thumb_is_the_viewport_share_clamped_at_both_ends() {
let half = indicator_geometry(200.0, 100.0, 0.0).unwrap();
assert!((half.thumb - 0.5).abs() < 1e-6, "{half:?}");
let long = indicator_geometry(10_000.0, 100.0, 0.0).unwrap();
assert!((long.thumb - INDICATOR_MIN_THUMB).abs() < 1e-6, "{long:?}");
let short = indicator_geometry(105.0, 100.0, 0.0).unwrap();
assert!(
(short.thumb - INDICATOR_MAX_THUMB).abs() < 1e-6,
"{short:?}"
);
}
#[test]
fn the_thumb_reaches_the_bottom_of_the_track_and_no_further() {
let bottom = indicator_geometry(200.0, 100.0, 100.0).unwrap();
assert!(
(bottom.offset + bottom.thumb - 1.0).abs() < 1e-6,
"{bottom:?}"
);
let past = indicator_geometry(200.0, 100.0, 500.0).unwrap();
assert_eq!(past, bottom);
}
#[test]
fn the_indicator_is_three_segments_with_a_gap_either_side_of_the_thumb() {
let arc = indicator_arc(LARGE_RADIUS_DP);
let geometry = IndicatorGeometry {
thumb: 0.4,
offset: 0.3,
};
let parts = indicator_segments(arc, geometry, 1.0);
assert_eq!(parts[0].0, IndicatorPart::Track);
assert_eq!(parts[1].0, IndicatorPart::Thumb);
assert_eq!(parts[2].0, IndicatorPart::Track);
let ink_bounds = |segment: IndicatorSegment| match segment {
IndicatorSegment::Arc { start, sweep, .. } => {
(start - arc.cap_sweep() * 0.5, sweep + arc.cap_sweep())
}
other => panic!("expected an arc, got {other:?}"),
};
let (above_start, above_sweep) = ink_bounds(parts[0].1);
let (thumb_start, thumb_sweep) = ink_bounds(parts[1].1);
let (below_start, below_sweep) = ink_bounds(parts[2].1);
let gap = arc.segment_inset() - arc.cap_sweep();
assert!((above_start - arc.start_angle() - gap * 0.5).abs() < 1e-4);
assert!((thumb_start - (above_start + above_sweep) - gap).abs() < 1e-4);
assert!((below_start - (thumb_start + thumb_sweep) - gap).abs() < 1e-4);
assert!(
(below_start + below_sweep + gap * 0.5 - (arc.start_angle() + arc.sweep())).abs()
< 1e-4,
"the track has to end where it should"
);
}
#[test]
fn a_segment_shorter_than_its_stroke_becomes_a_shrinking_dot() {
let arc = indicator_arc(LARGE_RADIUS_DP);
let parts = indicator_segments(
arc,
IndicatorGeometry {
thumb: 0.7,
offset: 0.0,
},
1.0,
);
match parts[0].1 {
IndicatorSegment::Dot { radius, alpha, .. } => {
assert!(
radius <= arc.width() * 0.5,
"a dot never exceeds the stroke"
);
assert!(alpha < 1.0, "it fades on the same fraction as it shrinks");
}
IndicatorSegment::Arc { sweep, .. } => {
assert!(sweep <= 0.0, "an arc this short should have been a dot");
}
}
}
#[test]
fn fading_the_indicator_fades_every_piece_of_it() {
let arc = indicator_arc(LARGE_RADIUS_DP);
let geometry = IndicatorGeometry {
thumb: 0.4,
offset: 0.3,
};
for (_, segment) in indicator_segments(arc, geometry, 0.25) {
let alpha = match segment {
IndicatorSegment::Arc { alpha, .. } => alpha,
IndicatorSegment::Dot { alpha, .. } => alpha,
};
assert!(alpha <= 0.25 + 1e-6, "{segment:?}");
}
}
#[test]
fn a_display_too_small_to_hold_the_track_degrades_instead_of_panicking() {
let tiny = indicator_arc(1.0);
assert_eq!(tiny.centreline(), 0.0);
assert_eq!(tiny.sweep(), 0.0);
assert_eq!(tiny.cap_sweep(), 0.0);
let parts = indicator_segments(
tiny,
IndicatorGeometry {
thumb: 0.4,
offset: 0.3,
},
1.0,
);
for (_, segment) in parts {
assert!(matches!(segment, IndicatorSegment::Arc { sweep: 0.0, .. }));
}
}
const VIEWPORT: f32 = 400.0;
fn list<'a>(visible: &'a [IndicatorItem]) -> ScalingList<'a> {
ScalingList {
visible,
total: 10,
viewport: VIEWPORT,
before_padding: 0.0,
after_padding: 0.0,
}
}
fn row(index: usize, start_offset: f32) -> IndicatorItem {
IndicatorItem {
index,
start_offset,
size: 100.0,
}
}
#[test]
fn a_row_flush_with_the_top_of_the_screen_is_a_whole_index() {
let rows = [row(3, -200.0), row(6, 100.0)];
assert_eq!(decimal_first_item_index(list(&rows)), 3.0);
}
#[test]
fn a_row_half_off_the_top_reads_half_an_index() {
let rows = [row(3, -250.0), row(6, 100.0)];
assert_eq!(decimal_first_item_index(list(&rows)), 3.5);
}
#[test]
fn the_last_index_counts_how_much_of_the_row_is_on_screen() {
let rows = [row(3, -200.0), row(6, 150.0)];
assert_eq!(decimal_last_item_index(list(&rows)), 6.5);
}
#[test]
fn the_padding_outside_the_list_counts_only_at_the_end_it_belongs_to() {
let rows = [row(0, -250.0), row(9, 150.0)];
let padded = ScalingList {
before_padding: 80.0,
after_padding: 60.0,
..list(&rows)
};
assert!((decimal_first_item_index(padded) - 130.0 / 180.0).abs() < 1e-6);
assert!((decimal_last_item_index(padded) - (9.0 + 0.3125)).abs() < 1e-6);
let inner = [row(3, -250.0), row(6, 150.0)];
let inner = ScalingList {
before_padding: 80.0,
after_padding: 60.0,
..list(&inner)
};
assert_eq!(decimal_first_item_index(inner), 3.5);
assert_eq!(decimal_last_item_index(inner), 6.5);
}
#[test]
fn the_thumb_is_the_share_of_the_items_on_screen_not_of_the_pixels() {
let rows = [row(3, -250.0), row(8, 150.0)];
let mut thumb = ThumbLength::default();
assert!((thumb.of(list(&rows)) - 0.5).abs() < 1e-6);
}
#[test]
fn the_thumb_is_clamped_at_both_ends_however_long_the_list_is() {
let rows = [row(3, -250.0), row(4, 150.0)];
let mut short = ThumbLength::default();
assert_eq!(short.of(list(&rows)), INDICATOR_MIN_THUMB);
let rows = [row(0, -250.0), row(9, 150.0)];
let mut long = ThumbLength::default();
assert_eq!(long.of(list(&rows)), INDICATOR_MAX_THUMB);
}
#[test]
fn the_thumb_is_measured_once_and_then_only_when_the_list_changes_length() {
let mut thumb = ThumbLength::default();
let five = [row(3, -250.0), row(8, 150.0)];
assert!((thumb.of(list(&five)) - 0.5).abs() < 1e-6);
let three = [row(3, -250.0), row(6, 150.0)];
assert!(
(thumb.of(list(&three)) - 0.5).abs() < 1e-6,
"the window shrank but the list did not, so the thumb holds"
);
let longer = ScalingList {
total: 20,
..list(&three)
};
assert_eq!(thumb.of(longer), INDICATOR_MIN_THUMB);
thumb.forget();
assert!((thumb.of(list(&five)) - 0.5).abs() < 1e-6);
}
#[test]
fn the_position_is_how_many_items_are_left_not_how_far_the_pixels_went() {
let rows = [row(3, -250.0), row(6, 150.0)];
assert!((position_fraction(list(&rows)) - 0.5).abs() < 1e-6);
}
#[test]
fn a_list_at_the_top_puts_the_thumb_at_the_top_and_one_at_the_end_at_the_end() {
let mut thumb = ThumbLength::default();
let top = [row(0, -200.0), row(3, 150.0)];
let geometry = scaling_list_geometry(&mut thumb, list(&top)).unwrap();
assert_eq!(geometry.offset, 0.0);
let mut thumb = ThumbLength::default();
let end = [row(6, -250.0), row(9, 100.0)];
let geometry = scaling_list_geometry(&mut thumb, list(&end)).unwrap();
assert_eq!(decimal_last_item_index(list(&end)), 10.0);
assert!((geometry.offset + geometry.thumb - 1.0).abs() < 1e-6);
}
#[test]
fn a_list_with_nothing_on_screen_has_no_indicator() {
let mut thumb = ThumbLength::default();
assert_eq!(scaling_list_geometry(&mut thumb, list(&[])), None);
let rows = [row(3, -250.0)];
let empty = ScalingList {
total: 0,
..list(&rows)
};
assert_eq!(scaling_list_geometry(&mut thumb, empty), None);
}
#[test]
fn the_two_models_disagree_the_moment_the_rows_are_not_all_the_same_height() {
let heights: Vec<f32> = std::iter::once(600.0).chain([100.0; 9]).collect();
let content: f32 = heights.iter().sum();
let pixel = indicator_geometry(content, VIEWPORT, 0.0).unwrap();
let rows = [row(0, -200.0), row(3, 100.0)];
let mut thumb = ThumbLength::default();
let wear = scaling_list_geometry(&mut thumb, list(&rows)).unwrap();
assert!((pixel.thumb - INDICATOR_MIN_THUMB).abs() < 1e-6);
assert!((wear.thumb - 0.4).abs() < 1e-6, "{wear:?}");
}
#[test]
fn a_reported_row_is_not_the_row_as_it_is_drawn() {
let mut out = Vec::new();
let density = 2.0;
let viewport = 227.0;
for (height, carried) in [(51.5, 0.5), (52.0, 0.0)] {
scaling_list_items(viewport, density, [(20.0, height)], &mut out);
let drawn = crate::round_scaling_list::place_row(viewport, 20.0, height, density)
.expect("placed");
let item = out.first().expect("on screen");
assert!(
(item.start_offset - (drawn.top * density - carried - 227.0)).abs() < 1e-4,
"{height}dp: reported {} against drawn {}",
item.start_offset,
drawn.top * density
);
assert_eq!(item.size, (drawn.height * density).round());
}
}
#[test]
fn a_list_that_does_not_scale_its_rows_reports_them_at_full_height() {
let mut wear = Vec::new();
let mut still = Vec::new();
let rows = [(4.0, 52.0), (60.0, 52.0), (116.0, 52.0)];
scaling_list_items(227.0, 2.0, rows, &mut wear);
scaling_list_items_with(
ScalingParams::WEAR.reduced_motion(),
227.0,
2.0,
rows,
&mut still,
);
assert_eq!(wear.len(), still.len());
assert!(
wear[0].size < still[0].size,
"the top row shrinks under the Wear ramp and not under a stilled \
one: {} vs {}",
wear[0].size,
still[0].size
);
assert_eq!(still[0].size, 104.0, "52dp at density 2, unscaled");
let mut default = Vec::new();
scaling_list_items_with(ScalingParams::WEAR, 227.0, 2.0, rows, &mut default);
assert_eq!(default, wear);
}
#[test]
fn the_window_is_the_rows_that_still_meet_the_display() {
let mut out = Vec::new();
let rows: Vec<(f32, f32)> = (0..10)
.map(|index| (index as f32 * 40.0 - 100.0, 40.0))
.collect();
scaling_list_items(227.0, 2.0, rows.iter().copied(), &mut out);
let indices: Vec<usize> = out.iter().map(|item| item.index).collect();
assert_eq!(indices, vec![2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn invalid_scaling_list_input_never_produces_a_non_finite_thumb() {
let mut out = Vec::new();
scaling_list_items(f32::NAN, 2.0, [(0.0, 40.0)], &mut out);
assert!(out.is_empty());
scaling_list_items(227.0, f32::NAN, [(0.0, 40.0)], &mut out);
assert!(out.is_empty());
let rows = [
IndicatorItem {
index: 0,
start_offset: f32::NAN,
size: 0.0,
},
IndicatorItem {
index: 3,
start_offset: f32::INFINITY,
size: -1.0,
},
];
let mut thumb = ThumbLength::default();
let geometry = scaling_list_geometry(&mut thumb, list(&rows)).expect("a geometry");
assert!(
geometry.thumb.is_finite() && geometry.offset.is_finite(),
"{geometry:?}"
);
assert!(geometry.thumb >= INDICATOR_MIN_THUMB && geometry.thumb <= INDICATOR_MAX_THUMB);
assert!(geometry.offset >= 0.0 && geometry.offset <= 1.0);
}
#[test]
fn invalid_public_inputs_never_emit_non_finite_draw_values() {
for radius in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1.0] {
let arc = indicator_arc(radius);
assert_eq!(arc.sweep(), 0.0);
assert_eq!(arc.segment_inset(), 0.0);
}
let parts = indicator_segments(
indicator_arc(LARGE_RADIUS_DP),
IndicatorGeometry {
thumb: f32::NAN,
offset: f32::INFINITY,
},
f32::NAN,
);
for (_, part) in parts {
match part {
IndicatorSegment::Arc {
start,
sweep,
alpha,
} => assert!(start.is_finite() && sweep.is_finite() && alpha == 0.0),
IndicatorSegment::Dot {
angle,
radius,
alpha,
} => assert!(angle.is_finite() && radius.is_finite() && alpha == 0.0),
}
}
}
}