rosace_scroll/
scrollbar.rs1use rosace_core::types::{Point, Rect, Size};
2use rosace_render::{Color, SkiaCanvas};
3
4use crate::physics::ScrollDirection;
5
6pub struct ScrollbarMetrics {
8 pub thumb_pos: f32,
10 pub thumb_size: f32,
12}
13
14impl ScrollbarMetrics {
15 pub fn compute(offset: f32, content_size: f32, viewport_size: f32) -> Self {
17 if content_size <= viewport_size {
18 return Self {
19 thumb_pos: 0.0,
20 thumb_size: 1.0,
21 };
22 }
23 let ratio = viewport_size / content_size;
24 let max_offset = content_size - viewport_size;
25 Self {
26 thumb_pos: (offset / max_offset) * (1.0 - ratio),
27 thumb_size: ratio.clamp(0.05, 1.0),
28 }
29 }
30}
31
32#[allow(clippy::too_many_arguments)]
36pub fn render_scrollbar(
37 canvas: &mut SkiaCanvas,
38 direction: ScrollDirection,
39 viewport_x: f32,
40 viewport_y: f32,
41 viewport_w: f32,
42 viewport_h: f32,
43 offset: f32,
44 content_size: f32,
45 opacity: f32,
46) {
47 if opacity <= 0.0 {
48 return;
49 }
50 let alpha = (opacity * 120.0) as u8;
51 let thumb_color = Color::rgba(180, 185, 210, alpha);
52 const TRACK_W: f32 = 6.0;
53 const MARGIN: f32 = 3.0;
54
55 match direction {
56 ScrollDirection::Vertical | ScrollDirection::Both => {
57 let metrics =
58 ScrollbarMetrics::compute(offset, content_size, viewport_h);
59 let track_h = viewport_h - MARGIN * 2.0;
60 let thumb_h = (track_h * metrics.thumb_size).max(20.0);
61 let thumb_y = viewport_y + MARGIN + track_h * metrics.thumb_pos;
62 let thumb_x = viewport_x + viewport_w - TRACK_W - MARGIN;
63 canvas.fill_rect(
64 Rect {
65 origin: Point {
66 x: thumb_x,
67 y: thumb_y,
68 },
69 size: Size {
70 width: TRACK_W,
71 height: thumb_h,
72 },
73 },
74 thumb_color,
75 );
76 }
77 ScrollDirection::Horizontal => {
78 let metrics =
79 ScrollbarMetrics::compute(offset, content_size, viewport_w);
80 let track_w = viewport_w - MARGIN * 2.0;
81 let thumb_w = (track_w * metrics.thumb_size).max(20.0);
82 let thumb_x = viewport_x + MARGIN + track_w * metrics.thumb_pos;
83 let thumb_y = viewport_y + viewport_h - TRACK_W - MARGIN;
84 canvas.fill_rect(
85 Rect {
86 origin: Point {
87 x: thumb_x,
88 y: thumb_y,
89 },
90 size: Size {
91 width: thumb_w,
92 height: TRACK_W,
93 },
94 },
95 thumb_color,
96 );
97 }
98 }
99}
100
101#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn scrollbar_metrics_thumb_size_equals_ratio_when_content_exceeds_viewport() {
110 let m = ScrollbarMetrics::compute(0.0, 400.0, 200.0);
111 assert!((m.thumb_size - 0.5).abs() < 1e-6);
113 }
114
115 #[test]
116 fn scrollbar_metrics_thumb_size_is_one_when_content_fits() {
117 let m = ScrollbarMetrics::compute(0.0, 100.0, 200.0);
118 assert_eq!(m.thumb_size, 1.0);
119 assert_eq!(m.thumb_pos, 0.0);
120 }
121
122 #[test]
123 fn scrollbar_metrics_thumb_pos_at_max_offset() {
124 let m = ScrollbarMetrics::compute(200.0, 400.0, 200.0);
126 assert!((m.thumb_pos - 0.5).abs() < 1e-5);
127 }
128
129 #[test]
130 fn render_scrollbar_does_not_panic_with_zero_opacity() {
131 let mut canvas = SkiaCanvas::new(200, 400);
132 canvas.clear(Color::WHITE);
133 render_scrollbar(
135 &mut canvas,
136 ScrollDirection::Vertical,
137 0.0, 0.0, 200.0, 400.0,
138 50.0, 800.0, 0.0,
139 );
140 }
141
142 #[test]
143 fn render_scrollbar_vertical_does_not_panic() {
144 let mut canvas = SkiaCanvas::new(200, 400);
145 canvas.clear(Color::WHITE);
146 render_scrollbar(
147 &mut canvas,
148 ScrollDirection::Vertical,
149 0.0, 0.0, 200.0, 400.0,
150 100.0, 800.0, 1.0,
151 );
152 }
153
154 #[test]
155 fn render_scrollbar_horizontal_does_not_panic() {
156 let mut canvas = SkiaCanvas::new(400, 200);
157 canvas.clear(Color::WHITE);
158 render_scrollbar(
159 &mut canvas,
160 ScrollDirection::Horizontal,
161 0.0, 0.0, 400.0, 200.0,
162 50.0, 800.0, 0.8,
163 );
164 }
165}