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