use std::f32::consts::FRAC_PI_2;
use crate::{
round_scaling_list::ScalingParams,
scrollbar::{ThumbBounds, thumb_geometry},
};
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> {
thumb_geometry(
content,
viewport,
scrolled,
ThumbBounds::new(INDICATOR_MIN_THUMB, INDICATOR_MAX_THUMB),
)
.map(|geometry| IndicatorGeometry {
thumb: geometry.length,
offset: geometry.offset,
})
}
#[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)]
#[path = "tests/round_scroll_indicator_tests.rs"]
mod tests;