cranpose_ui/widgets/wear/
scroll_indicator.rs1#![allow(non_snake_case)]
17
18use crate::composable;
19use crate::modifier::{Brush, Color, Modifier, Point};
20use crate::round_scroll_indicator::{
21 indicator_arc, indicator_geometry, indicator_segments, IndicatorGeometry, IndicatorPart,
22 IndicatorSegment,
23};
24use crate::widgets::wear::scaling_list::WearScalingListState;
25use crate::widgets::wear::theme::WearColors;
26use crate::widgets::Canvas;
27use cranpose_core::NodeId;
28use cranpose_ui_graphics::{DrawScope, Stroke, StrokeCap};
29
30#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct ScrollIndicatorSpec {
33 pub colors: WearColors,
34 pub alpha: f32,
42}
43
44impl Default for ScrollIndicatorSpec {
45 fn default() -> Self {
46 Self {
47 colors: WearColors::default(),
48 alpha: 1.0,
49 }
50 }
51}
52
53impl ScrollIndicatorSpec {
54 pub fn colors(mut self, colors: WearColors) -> Self {
55 self.colors = colors;
56 self
57 }
58
59 pub fn alpha(mut self, alpha: f32) -> Self {
60 self.alpha = alpha;
61 self
62 }
63}
64
65pub fn indicator_for_scaling_list(state: &WearScalingListState) -> Option<IndicatorGeometry> {
72 let info = state.layout_info();
73 let travel = info.travel();
74 if travel <= 0.0 || info.viewport <= 0.0 || info.content <= info.viewport {
75 return None;
76 }
77 indicator_geometry(info.content, info.viewport, info.scrolled())
78}
79
80pub fn draw_scroll_indicator(
85 scope: &mut dyn DrawScope,
86 geometry: IndicatorGeometry,
87 spec: ScrollIndicatorSpec,
88) {
89 let size = scope.size();
90 let radius = size.width.min(size.height) * 0.5;
91 if radius <= 0.0 || !radius.is_finite() || spec.alpha <= 0.0 {
92 return;
93 }
94 let centre = Point {
95 x: size.width * 0.5,
96 y: size.height * 0.5,
97 };
98 let arc = indicator_arc(radius);
99 if arc.centreline() <= 0.0 {
100 return;
101 }
102 let stroke = Stroke::new(arc.width()).with_cap(StrokeCap::Round);
103 for (part, segment) in indicator_segments(arc, geometry, spec.alpha) {
104 let color = match part {
105 IndicatorPart::Track => spec.colors.indicator_track,
106 IndicatorPart::Thumb => spec.colors.indicator_thumb,
107 };
108 match segment {
109 IndicatorSegment::Arc {
110 start,
111 sweep,
112 alpha,
113 } => {
114 if sweep <= 0.0 || alpha <= 0.0 {
115 continue;
116 }
117 scope.draw_arc(
118 Brush::Solid(with_alpha(color, alpha)),
119 centre,
120 arc.centreline(),
121 start,
122 sweep,
123 stroke,
124 );
125 }
126 IndicatorSegment::Dot {
127 angle,
128 radius: dot,
129 alpha,
130 } => {
131 if dot <= 0.0 || alpha <= 0.0 {
132 continue;
133 }
134 scope.draw_circle(
135 Brush::Solid(with_alpha(color, alpha)),
136 Point {
137 x: centre.x + arc.centreline() * angle.cos(),
138 y: centre.y + arc.centreline() * angle.sin(),
139 },
140 dot,
141 );
142 }
143 }
144 }
145}
146
147fn with_alpha(color: Color, alpha: f32) -> Color {
148 Color::rgba(color.0, color.1, color.2, color.3 * alpha)
149}
150
151#[composable]
155pub fn ScrollIndicator(
156 modifier: Modifier,
157 state: WearScalingListState,
158 spec: ScrollIndicatorSpec,
159) -> NodeId {
160 let draw_state = state.clone();
161 Canvas(
162 modifier.fill_max_size(),
163 move |scope: &mut dyn DrawScope| {
164 if let Some(geometry) = indicator_for_scaling_list(&draw_state) {
165 draw_scroll_indicator(scope, geometry, spec);
166 }
167 },
168 )
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use cranpose_ui_graphics::{DrawPrimitive, DrawScopeDefault, Size};
175
176 fn scene(geometry: IndicatorGeometry, alpha: f32) -> Vec<DrawPrimitive> {
177 let mut scope = DrawScopeDefault::new(Size::new(227.0, 227.0));
178 draw_scroll_indicator(
179 &mut scope,
180 geometry,
181 ScrollIndicatorSpec::default().alpha(alpha),
182 );
183 scope.into_primitives()
184 }
185
186 #[test]
187 fn the_indicator_is_three_segments_not_a_thumb_over_a_rail() {
188 let primitives = scene(
189 IndicatorGeometry {
190 thumb: 0.4,
191 offset: 0.3,
192 },
193 1.0,
194 );
195 assert_eq!(
196 primitives.len(),
197 3,
198 "track, thumb, track — and no full-length rail underneath"
199 );
200 }
201
202 #[test]
203 fn a_segment_shorter_than_its_stroke_becomes_a_dot() {
204 let primitives = scene(
206 IndicatorGeometry {
207 thumb: 0.7,
208 offset: 0.0,
209 },
210 1.0,
211 );
212 assert_eq!(primitives.len(), 2);
215 }
216
217 #[test]
218 fn a_faded_indicator_draws_nothing_at_all() {
219 let primitives = scene(
220 IndicatorGeometry {
221 thumb: 0.4,
222 offset: 0.3,
223 },
224 0.0,
225 );
226 assert!(primitives.is_empty());
227 }
228
229 #[test]
230 fn a_display_with_no_room_does_not_panic() {
231 let mut scope = DrawScopeDefault::new(Size::new(1.0, 1.0));
232 draw_scroll_indicator(
233 &mut scope,
234 IndicatorGeometry {
235 thumb: 0.4,
236 offset: 0.3,
237 },
238 ScrollIndicatorSpec::default(),
239 );
240 assert!(scope.into_primitives().is_empty());
241 }
242}