1use crate::color::Color;
2use crate::draw_ctx::DrawCtx;
3use crate::geometry::{Point, Rect};
4
5use super::scroll_view::{ScrollBarColor, ScrollBarKind, ScrollBarStyle, ScrollBarVisibility};
6
7pub const DEFAULT_GRAB_MARGIN: f64 = 6.0;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ScrollbarOrientation {
11 Vertical,
12 Horizontal,
13}
14
15#[derive(Clone, Copy)]
16pub struct ScrollbarGeometry {
17 pub orientation: ScrollbarOrientation,
18 pub track_start: f64,
19 pub track_end: f64,
20 pub cross_end: f64,
22 pub hit_margin: f64,
23}
24
25#[derive(Clone, Copy)]
26pub struct PreparedScrollbar {
27 pub track: Rect,
28 pub thumb: Rect,
29 pub radius: f64,
30 pub alpha: f64,
31 pub color: ScrollBarColor,
32 pub hovered_thumb: bool,
33 pub dragging: bool,
34}
35
36#[derive(Clone, Copy)]
37pub struct ScrollbarAxis {
38 pub enabled: bool,
39 pub offset: f64,
40 pub content: f64,
41 pub hovered_bar: bool,
42 pub hovered_thumb: bool,
43 pub dragging: bool,
44 pub drag_thumb_offset: f64,
45 pub hover_anim: crate::animation::Tween,
46 pub visibility_anim: crate::animation::Tween,
47}
48
49impl Default for ScrollbarAxis {
50 fn default() -> Self {
51 Self {
52 enabled: false,
53 offset: 0.0,
54 content: 0.0,
55 hovered_bar: false,
56 hovered_thumb: false,
57 dragging: false,
58 drag_thumb_offset: 0.0,
59 hover_anim: crate::animation::Tween::new(0.0, 0.12),
60 visibility_anim: crate::animation::Tween::new(0.0, 0.18),
61 }
62 }
63}
64
65impl ScrollbarAxis {
66 pub fn max_scroll(&self, viewport: f64) -> f64 {
67 (self.content - viewport).max(0.0)
68 }
69
70 pub fn interact(&self) -> bool {
71 self.hovered_bar || self.hovered_thumb || self.dragging
72 }
73
74 pub fn animation_active(&self) -> bool {
75 self.hover_anim.is_animating() || self.visibility_anim.is_animating()
76 }
77
78 pub fn can_scroll(&self, viewport: f64) -> bool {
79 self.enabled && self.content > viewport
80 }
81
82 pub fn clamp_offset(&mut self, viewport: f64) {
83 self.offset = self.offset.clamp(0.0, self.max_scroll(viewport)).round();
84 }
85
86 pub fn scroll_by(&mut self, delta: f64, viewport: f64) -> bool {
87 let old = self.offset;
88 self.offset += delta;
89 self.clamp_offset(viewport);
90 (self.offset - old).abs() > 1e-6
91 }
92
93 pub fn thumb_metrics(
94 &self,
95 viewport: f64,
96 style: ScrollBarStyle,
97 geom: ScrollbarGeometry,
98 ) -> Option<(f64, f64)> {
99 if self.content <= viewport {
100 return None;
101 }
102 let track_len = geom.track_end - geom.track_start;
103 let ratio = viewport / self.content;
104 let thumb_len = (track_len * ratio).max(style.handle_min_length);
105 let travel = (track_len - thumb_len).max(0.0);
106 let max_s = self.max_scroll(viewport);
107 let start = if max_s > 0.0 {
108 match geom.orientation {
109 ScrollbarOrientation::Vertical => {
110 geom.track_start + travel * (1.0 - self.offset / max_s)
111 }
112 ScrollbarOrientation::Horizontal => {
113 geom.track_start + travel * (self.offset / max_s)
114 }
115 }
116 } else {
117 match geom.orientation {
118 ScrollbarOrientation::Vertical => geom.track_start + travel,
119 ScrollbarOrientation::Horizontal => geom.track_start,
120 }
121 };
122 Some((start, thumb_len))
123 }
124
125 pub fn pos_on_thumb(
126 &self,
127 pos: Point,
128 viewport: f64,
129 style: ScrollBarStyle,
130 geom: ScrollbarGeometry,
131 ) -> bool {
132 match geom.orientation {
133 ScrollbarOrientation::Vertical => {
134 let hit_left = geom.cross_end - style.bar_width - geom.hit_margin;
135 if pos.x < hit_left || pos.x >= geom.cross_end {
136 return false;
137 }
138 self.thumb_metrics(viewport, style, geom)
139 .is_some_and(|(start, len)| pos.y >= start && pos.y <= start + len)
140 }
141 ScrollbarOrientation::Horizontal => {
142 let hit_top = geom.cross_end + style.bar_width + geom.hit_margin;
143 if pos.y < geom.cross_end || pos.y >= hit_top {
144 return false;
145 }
146 self.thumb_metrics(viewport, style, geom)
147 .is_some_and(|(start, len)| pos.x >= start && pos.x <= start + len)
148 }
149 }
150 }
151
152 pub fn pos_in_hover(&self, pos: Point, style: ScrollBarStyle, geom: ScrollbarGeometry) -> bool {
153 match geom.orientation {
154 ScrollbarOrientation::Vertical => {
155 let left = geom.cross_end - style.bar_width - geom.hit_margin;
156 pos.x >= left && pos.x < geom.cross_end
157 }
158 ScrollbarOrientation::Horizontal => {
159 let top = geom.cross_end + style.bar_width + geom.hit_margin;
160 pos.y >= geom.cross_end && pos.y < top
161 }
162 }
163 }
164
165 pub fn update_hover(
166 &mut self,
167 pos: Point,
168 viewport: f64,
169 style: ScrollBarStyle,
170 geom: ScrollbarGeometry,
171 ) -> bool {
172 let was_bar = self.hovered_bar;
173 let was_thumb = self.hovered_thumb;
174 let can_scroll = self.can_scroll(viewport);
175 self.hovered_bar = can_scroll && self.pos_in_hover(pos, style, geom);
176 self.hovered_thumb = can_scroll && self.pos_on_thumb(pos, viewport, style, geom);
177 was_bar != self.hovered_bar || was_thumb != self.hovered_thumb
178 }
179
180 pub fn begin_drag(
181 &mut self,
182 pos: Point,
183 viewport: f64,
184 style: ScrollBarStyle,
185 geom: ScrollbarGeometry,
186 ) -> bool {
187 if !self.pos_on_thumb(pos, viewport, style, geom) {
188 return false;
189 }
190 let start = self
191 .thumb_metrics(viewport, style, geom)
192 .map(|(start, _)| start)
193 .unwrap_or(0.0);
194 self.dragging = true;
195 self.drag_thumb_offset = match geom.orientation {
196 ScrollbarOrientation::Vertical => pos.y - start,
197 ScrollbarOrientation::Horizontal => pos.x - start,
198 };
199 true
200 }
201
202 pub fn drag_to(
203 &mut self,
204 pos: Point,
205 viewport: f64,
206 style: ScrollBarStyle,
207 geom: ScrollbarGeometry,
208 ) -> bool {
209 let Some((_, thumb_len)) = self.thumb_metrics(viewport, style, geom) else {
210 return false;
211 };
212 let travel = (geom.track_end - geom.track_start - thumb_len).max(1.0);
213 let raw_start = match geom.orientation {
214 ScrollbarOrientation::Vertical => pos.y - self.drag_thumb_offset,
215 ScrollbarOrientation::Horizontal => pos.x - self.drag_thumb_offset,
216 };
217 let new_start = raw_start.clamp(geom.track_start, geom.track_start + travel);
218 let frac = (new_start - geom.track_start) / travel;
219 let old = self.offset;
220 self.offset = match geom.orientation {
221 ScrollbarOrientation::Vertical => (1.0 - frac) * self.max_scroll(viewport),
222 ScrollbarOrientation::Horizontal => frac * self.max_scroll(viewport),
223 };
224 self.clamp_offset(viewport);
225 (self.offset - old).abs() > 1e-6
226 }
227
228 pub fn page_at(
229 &mut self,
230 pos: Point,
231 viewport: f64,
232 style: ScrollBarStyle,
233 geom: ScrollbarGeometry,
234 ) -> bool {
235 let Some((start, len)) = self.thumb_metrics(viewport, style, geom) else {
236 return false;
237 };
238 let page = (viewport - 16.0).max(20.0);
239 let old = self.offset;
240 match geom.orientation {
241 ScrollbarOrientation::Vertical => {
242 if pos.y > start + len {
243 self.offset = (self.offset - page).max(0.0);
244 } else if pos.y < start {
245 self.offset = (self.offset + page).min(self.max_scroll(viewport));
246 }
247 }
248 ScrollbarOrientation::Horizontal => {
249 if pos.x < start {
250 self.offset = (self.offset - page).max(0.0);
251 } else if pos.x > start + len {
252 self.offset = (self.offset + page).min(self.max_scroll(viewport));
253 }
254 }
255 }
256 self.clamp_offset(viewport);
257 (self.offset - old).abs() > 1e-6
258 }
259
260 pub fn should_paint(
261 &self,
262 viewport: f64,
263 style: ScrollBarStyle,
264 visibility: ScrollBarVisibility,
265 ) -> bool {
266 if self.content <= viewport {
267 return false;
268 }
269 let floating = style.kind == ScrollBarKind::Floating;
270 match visibility {
271 ScrollBarVisibility::AlwaysHidden => false,
272 ScrollBarVisibility::AlwaysVisible => true,
273 ScrollBarVisibility::VisibleWhenNeeded => {
274 !floating || self.hovered_bar || self.dragging
275 }
276 }
277 }
278
279 pub fn prepare_paint(
280 &mut self,
281 viewport: f64,
282 style: ScrollBarStyle,
283 visibility: ScrollBarVisibility,
284 geom: ScrollbarGeometry,
285 ) -> Option<PreparedScrollbar> {
286 self.visibility_anim
287 .set_target(if self.should_paint(viewport, style, visibility) {
288 1.0
289 } else {
290 0.0
291 });
292 let alpha = self.visibility_anim.tick();
293 if !self.enabled || self.content <= viewport || alpha <= 0.001 {
294 return None;
295 }
296 let (thumb_start, thumb_len) = self.thumb_metrics(viewport, style, geom)?;
297 self.hover_anim
298 .set_target(if self.interact() { 1.0 } else { 0.0 });
299 let t = self.hover_anim.tick();
300 let bar_w = style.bar_width_at(t);
301 let radius = bar_w * 0.5;
302 let (track, thumb) = match geom.orientation {
303 ScrollbarOrientation::Vertical => {
304 let bar_x = geom.cross_end - bar_w;
305 (
306 Rect::new(
307 bar_x,
308 geom.track_start,
309 bar_w,
310 geom.track_end - geom.track_start,
311 ),
312 Rect::new(bar_x, thumb_start, bar_w, thumb_len),
313 )
314 }
315 ScrollbarOrientation::Horizontal => (
316 Rect::new(
317 geom.track_start,
318 geom.cross_end,
319 geom.track_end - geom.track_start,
320 bar_w,
321 ),
322 Rect::new(thumb_start, geom.cross_end, thumb_len, bar_w),
323 ),
324 };
325 Some(PreparedScrollbar {
326 track,
327 thumb,
328 radius,
329 alpha,
330 color: style.color,
331 hovered_thumb: self.hovered_thumb,
332 dragging: self.dragging,
333 })
334 }
335}
336
337impl PreparedScrollbar {
338 pub fn translated(mut self, dx: f64, dy: f64) -> Self {
339 self.track.x += dx;
340 self.track.y += dy;
341 self.thumb.x += dx;
342 self.thumb.y += dy;
343 self
344 }
345}
346
347pub fn paint_prepared_scrollbar(ctx: &mut dyn DrawCtx, bar: PreparedScrollbar) {
348 let v = ctx.visuals();
349 let track_color = v.scroll_track;
350 ctx.set_fill_color(scale_alpha(track_color, bar.alpha));
351 ctx.begin_path();
352 ctx.rounded_rect(
353 bar.track.x,
354 bar.track.y,
355 bar.track.width,
356 bar.track.height,
357 bar.radius,
358 );
359 ctx.fill();
360
361 let thumb_color = match bar.color {
362 ScrollBarColor::Background if bar.dragging => v.scroll_thumb_dragging,
363 ScrollBarColor::Background if bar.hovered_thumb => v.scroll_thumb_hovered,
364 ScrollBarColor::Background => v.scroll_thumb,
365 ScrollBarColor::Foreground if bar.dragging => v.accent_pressed,
366 ScrollBarColor::Foreground if bar.hovered_thumb => v.accent_hovered,
367 ScrollBarColor::Foreground => v.scroll_thumb,
368 };
369 ctx.set_fill_color(scale_alpha(thumb_color, bar.alpha));
370 ctx.begin_path();
371 ctx.rounded_rect(
372 bar.thumb.x,
373 bar.thumb.y,
374 bar.thumb.width,
375 bar.thumb.height,
376 bar.radius,
377 );
378 ctx.fill();
379}
380
381fn scale_alpha(c: Color, a: f64) -> Color {
382 Color::rgba(c.r, c.g, c.b, c.a * (a as f32).clamp(0.0, 1.0))
383}