#![allow(non_snake_case)]
use crate::composable;
use crate::modifier::{Brush, Color, Modifier, Point};
use crate::round_scroll_indicator::{
indicator_arc, indicator_segments, scaling_list_geometry, IndicatorGeometry, IndicatorPart,
IndicatorSegment,
};
use crate::widgets::wear::scaling_list::WearScalingListState;
use crate::widgets::wear::theme::WearColors;
use crate::widgets::Canvas;
use cranpose_core::NodeId;
use cranpose_ui_graphics::{DrawScope, Stroke, StrokeCap};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollIndicatorSpec {
pub colors: WearColors,
pub alpha: f32,
}
impl Default for ScrollIndicatorSpec {
fn default() -> Self {
Self {
colors: WearColors::default(),
alpha: 1.0,
}
}
}
impl ScrollIndicatorSpec {
pub fn colors(mut self, colors: WearColors) -> Self {
self.colors = colors;
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha;
self
}
}
pub fn indicator_for_scaling_list(state: &WearScalingListState) -> Option<IndicatorGeometry> {
let info = state.layout_info();
let travel = info.travel();
if travel <= 0.0 || info.viewport <= 0.0 || info.content <= info.viewport {
return None;
}
state.with_indicator_list(scaling_list_geometry)
}
pub fn draw_scroll_indicator(
scope: &mut dyn DrawScope,
geometry: IndicatorGeometry,
spec: ScrollIndicatorSpec,
) {
let size = scope.size();
let radius = size.width.min(size.height) * 0.5;
if radius <= 0.0 || !radius.is_finite() || spec.alpha <= 0.0 {
return;
}
let centre = Point {
x: size.width * 0.5,
y: size.height * 0.5,
};
let arc = indicator_arc(radius);
if arc.centreline() <= 0.0 {
return;
}
let stroke = Stroke::new(arc.width()).with_cap(StrokeCap::Round);
for (part, segment) in indicator_segments(arc, geometry, spec.alpha) {
let color = match part {
IndicatorPart::Track => spec.colors.indicator_track,
IndicatorPart::Thumb => spec.colors.indicator_thumb,
};
match segment {
IndicatorSegment::Arc {
start,
sweep,
alpha,
} => {
if sweep <= 0.0 || alpha <= 0.0 {
continue;
}
scope.draw_arc(
Brush::Solid(with_alpha(color, alpha)),
centre,
arc.centreline(),
start,
sweep,
stroke,
);
}
IndicatorSegment::Dot {
angle,
radius: dot,
alpha,
} => {
if dot <= 0.0 || alpha <= 0.0 {
continue;
}
scope.draw_circle(
Brush::Solid(with_alpha(color, alpha)),
Point {
x: centre.x + arc.centreline() * angle.cos(),
y: centre.y + arc.centreline() * angle.sin(),
},
dot,
);
}
}
}
}
fn with_alpha(color: Color, alpha: f32) -> Color {
Color::rgba(color.0, color.1, color.2, color.3 * alpha)
}
#[composable]
pub fn ScrollIndicator(
modifier: Modifier,
state: WearScalingListState,
spec: ScrollIndicatorSpec,
) -> NodeId {
let draw_state = state.clone();
Canvas(
modifier.fill_max_size(),
move |scope: &mut dyn DrawScope| {
if let Some(geometry) = indicator_for_scaling_list(&draw_state) {
draw_scroll_indicator(scope, geometry, spec);
}
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use cranpose_ui_graphics::{DrawPrimitive, DrawScopeDefault, Size};
fn scene(geometry: IndicatorGeometry, alpha: f32) -> Vec<DrawPrimitive> {
let mut scope = DrawScopeDefault::new(Size::new(227.0, 227.0));
draw_scroll_indicator(
&mut scope,
geometry,
ScrollIndicatorSpec::default().alpha(alpha),
);
scope.into_primitives()
}
#[test]
fn the_indicator_is_three_segments_not_a_thumb_over_a_rail() {
let primitives = scene(
IndicatorGeometry {
thumb: 0.4,
offset: 0.3,
},
1.0,
);
assert_eq!(
primitives.len(),
3,
"track, thumb, track — and no full-length rail underneath"
);
}
#[test]
fn a_segment_shorter_than_its_stroke_becomes_a_dot() {
let primitives = scene(
IndicatorGeometry {
thumb: 0.7,
offset: 0.0,
},
1.0,
);
assert_eq!(primitives.len(), 2);
}
#[test]
fn a_faded_indicator_draws_nothing_at_all() {
let primitives = scene(
IndicatorGeometry {
thumb: 0.4,
offset: 0.3,
},
0.0,
);
assert!(primitives.is_empty());
}
#[test]
fn a_display_with_no_room_does_not_panic() {
let mut scope = DrawScopeDefault::new(Size::new(1.0, 1.0));
draw_scroll_indicator(
&mut scope,
IndicatorGeometry {
thumb: 0.4,
offset: 0.3,
},
ScrollIndicatorSpec::default(),
);
assert!(scope.into_primitives().is_empty());
}
}