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_segments, scaling_list_geometry, 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> {
83 let info = state.layout_info();
84 let travel = info.travel();
85 if travel <= 0.0 || info.viewport <= 0.0 || info.content <= info.viewport {
86 return None;
87 }
88 state.with_indicator_list(scaling_list_geometry)
89}
90
91pub fn draw_scroll_indicator(
96 scope: &mut dyn DrawScope,
97 geometry: IndicatorGeometry,
98 spec: ScrollIndicatorSpec,
99) {
100 let size = scope.size();
101 let radius = size.width.min(size.height) * 0.5;
102 if radius <= 0.0 || !radius.is_finite() || spec.alpha <= 0.0 {
103 return;
104 }
105 let centre = Point {
106 x: size.width * 0.5,
107 y: size.height * 0.5,
108 };
109 let arc = indicator_arc(radius);
110 if arc.centreline() <= 0.0 {
111 return;
112 }
113 let stroke = Stroke::new(arc.width()).with_cap(StrokeCap::Round);
114 for (part, segment) in indicator_segments(arc, geometry, spec.alpha) {
115 let color = match part {
116 IndicatorPart::Track => spec.colors.indicator_track,
117 IndicatorPart::Thumb => spec.colors.indicator_thumb,
118 };
119 match segment {
120 IndicatorSegment::Arc {
121 start,
122 sweep,
123 alpha,
124 } => {
125 if sweep <= 0.0 || alpha <= 0.0 {
126 continue;
127 }
128 scope.draw_arc(
129 Brush::Solid(with_alpha(color, alpha)),
130 centre,
131 arc.centreline(),
132 start,
133 sweep,
134 stroke,
135 );
136 }
137 IndicatorSegment::Dot {
138 angle,
139 radius: dot,
140 alpha,
141 } => {
142 if dot <= 0.0 || alpha <= 0.0 {
143 continue;
144 }
145 scope.draw_circle(
146 Brush::Solid(with_alpha(color, alpha)),
147 Point {
148 x: centre.x + arc.centreline() * angle.cos(),
149 y: centre.y + arc.centreline() * angle.sin(),
150 },
151 dot,
152 );
153 }
154 }
155 }
156}
157
158fn with_alpha(color: Color, alpha: f32) -> Color {
159 Color::rgba(color.0, color.1, color.2, color.3 * alpha)
160}
161
162#[composable]
166pub fn ScrollIndicator(
167 modifier: Modifier,
168 state: WearScalingListState,
169 spec: ScrollIndicatorSpec,
170) -> NodeId {
171 let draw_state = state.clone();
172 Canvas(
173 modifier.fill_max_size(),
174 move |scope: &mut dyn DrawScope| {
175 if let Some(geometry) = indicator_for_scaling_list(&draw_state) {
176 draw_scroll_indicator(scope, geometry, spec);
177 }
178 },
179 )
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use cranpose_ui_graphics::{DrawPrimitive, DrawScopeDefault, Size};
186
187 fn scene(geometry: IndicatorGeometry, alpha: f32) -> Vec<DrawPrimitive> {
188 let mut scope = DrawScopeDefault::new(Size::new(227.0, 227.0));
189 draw_scroll_indicator(
190 &mut scope,
191 geometry,
192 ScrollIndicatorSpec::default().alpha(alpha),
193 );
194 scope.into_primitives()
195 }
196
197 #[test]
198 fn the_indicator_is_three_segments_not_a_thumb_over_a_rail() {
199 let primitives = scene(
200 IndicatorGeometry {
201 thumb: 0.4,
202 offset: 0.3,
203 },
204 1.0,
205 );
206 assert_eq!(
207 primitives.len(),
208 3,
209 "track, thumb, track — and no full-length rail underneath"
210 );
211 }
212
213 #[test]
214 fn a_segment_shorter_than_its_stroke_becomes_a_dot() {
215 let primitives = scene(
217 IndicatorGeometry {
218 thumb: 0.7,
219 offset: 0.0,
220 },
221 1.0,
222 );
223 assert_eq!(primitives.len(), 2);
226 }
227
228 #[test]
229 fn a_faded_indicator_draws_nothing_at_all() {
230 let primitives = scene(
231 IndicatorGeometry {
232 thumb: 0.4,
233 offset: 0.3,
234 },
235 0.0,
236 );
237 assert!(primitives.is_empty());
238 }
239
240 #[test]
241 fn a_display_with_no_room_does_not_panic() {
242 let mut scope = DrawScopeDefault::new(Size::new(1.0, 1.0));
243 draw_scroll_indicator(
244 &mut scope,
245 IndicatorGeometry {
246 thumb: 0.4,
247 offset: 0.3,
248 },
249 ScrollIndicatorSpec::default(),
250 );
251 assert!(scope.into_primitives().is_empty());
252 }
253}