1use rosace_render::SkiaCanvas;
2
3use crate::{
4 controller::ScrollController,
5 physics::{MomentumState, ScrollDirection, ScrollPhysics},
6 scrollbar::render_scrollbar,
7};
8
9pub struct ScrollView {
15 pub direction: ScrollDirection,
16 pub physics: ScrollPhysics,
17 pub show_scrollbar: bool,
18 pub controller: ScrollController,
19 pub content_width: f32,
20 pub content_height: f32,
21 pub viewport_width: f32,
22 pub viewport_height: f32,
23 momentum: MomentumState,
24 scrollbar_opacity: f32,
25 scrollbar_fade_timer: f32,
26}
27
28impl ScrollView {
29 pub fn new(viewport_w: f32, viewport_h: f32) -> Self {
31 let controller = ScrollController::new();
32 controller.viewport_size.set([viewport_w, viewport_h]);
33 Self {
34 direction: ScrollDirection::Vertical,
35 physics: ScrollPhysics::default(),
36 show_scrollbar: true,
37 controller,
38 content_width: viewport_w,
39 content_height: viewport_h,
40 viewport_width: viewport_w,
41 viewport_height: viewport_h,
42 momentum: MomentumState::new(),
43 scrollbar_opacity: 0.0,
44 scrollbar_fade_timer: 0.0,
45 }
46 }
47
48 pub fn direction(mut self, d: ScrollDirection) -> Self {
51 self.direction = d;
52 self
53 }
54
55 pub fn physics(mut self, p: ScrollPhysics) -> Self {
56 self.physics = p;
57 self
58 }
59
60 pub fn show_scrollbar(mut self, s: bool) -> Self {
61 self.show_scrollbar = s;
62 self
63 }
64
65 pub fn content_size(mut self, w: f32, h: f32) -> Self {
66 self.content_width = w;
67 self.content_height = h;
68 self.controller.content_size.set([w, h]);
69 self
70 }
71
72 pub fn tick(&mut self, dt: f32) {
77 let (dx, dy) = self.momentum.tick(self.physics);
78 if dx.abs() > 0.0 || dy.abs() > 0.0 {
79 self.controller.scroll_by(dx, dy);
80 self.scrollbar_opacity = 1.0;
81 self.scrollbar_fade_timer = 1.5;
82 }
83 if self.scrollbar_fade_timer > 0.0 {
85 self.scrollbar_fade_timer -= dt;
86 if self.scrollbar_fade_timer <= 0.0 {
87 self.scrollbar_opacity = (self.scrollbar_opacity - dt * 2.0).max(0.0);
88 }
89 }
90 }
91
92 pub fn on_scroll(&mut self, dx: f32, dy: f32) {
96 self.momentum.push(dx, dy);
97 self.controller.scroll_by(dx, dy);
98 self.scrollbar_opacity = 1.0;
99 self.scrollbar_fade_timer = 1.5;
100 }
101
102 pub fn render_chrome(&self, canvas: &mut SkiaCanvas, x: f32, y: f32) {
108 if !self.show_scrollbar {
109 return;
110 }
111 let [ox, oy] = self.controller.offset();
112 match self.direction {
113 ScrollDirection::Vertical | ScrollDirection::Both => {
114 render_scrollbar(
115 canvas,
116 ScrollDirection::Vertical,
117 x,
118 y,
119 self.viewport_width,
120 self.viewport_height,
121 oy,
122 self.content_height,
123 self.scrollbar_opacity,
124 );
125 }
126 ScrollDirection::Horizontal => {
127 render_scrollbar(
128 canvas,
129 ScrollDirection::Horizontal,
130 x,
131 y,
132 self.viewport_width,
133 self.viewport_height,
134 ox,
135 self.content_width,
136 self.scrollbar_opacity,
137 );
138 }
139 }
140 }
141
142 pub fn scroll_offset(&self) -> [f32; 2] {
147 self.controller.offset()
148 }
149
150 pub fn controller(&self) -> &ScrollController {
151 &self.controller
152 }
153}
154
155#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn scroll_view_on_scroll_updates_offset() {
164 let mut sv = ScrollView::new(300.0, 400.0).content_size(300.0, 1200.0);
165 sv.on_scroll(0.0, 50.0);
166 let [_x, y] = sv.scroll_offset();
167 assert_eq!(y, 50.0);
168 }
169
170 #[test]
171 fn scroll_view_on_scroll_clamps_to_content_bounds() {
172 let mut sv = ScrollView::new(300.0, 400.0).content_size(300.0, 600.0);
173 sv.on_scroll(0.0, 9999.0);
175 let [_x, y] = sv.scroll_offset();
176 assert_eq!(y, 200.0);
177 }
178
179 #[test]
180 fn scroll_view_tick_advances_momentum() {
181 let mut sv = ScrollView::new(300.0, 400.0)
182 .content_size(300.0, 2000.0)
183 .physics(ScrollPhysics::Momentum { friction: 0.92 });
184 sv.on_scroll(0.0, 100.0);
185 let before = sv.scroll_offset()[1];
187 sv.tick(0.016);
188 let after = sv.scroll_offset()[1];
189 assert!(after >= before);
191 }
192
193 #[test]
194 fn scroll_view_render_chrome_does_not_panic() {
195 let mut canvas = SkiaCanvas::new(300, 400);
196 canvas.clear(rosace_render::Color::WHITE);
197 let sv = ScrollView::new(300.0, 400.0).content_size(300.0, 800.0);
198 sv.render_chrome(&mut canvas, 0.0, 0.0);
199 }
200
201 #[test]
202 fn scroll_view_controller_accessible() {
203 let sv = ScrollView::new(300.0, 400.0).content_size(300.0, 800.0);
204 let c = sv.controller();
205 assert_eq!(c.offset(), [0.0, 0.0]);
207 }
208}