Skip to main content

agg_gui/widgets/
scrollbar.rs

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    /// Vertical: right edge of the bar. Horizontal: bottom edge of the bar.
21    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}