Skip to main content

agg_gui/widgets/scroll_view/
widget_impl.rs

1use super::*;
2
3impl Widget for ScrollView {
4    fn type_name(&self) -> &'static str {
5        "ScrollView"
6    }
7    fn bounds(&self) -> Rect {
8        self.bounds
9    }
10    fn set_bounds(&mut self, b: Rect) {
11        self.bounds = b;
12    }
13    fn children(&self) -> &[Box<dyn Widget>] {
14        &self.children
15    }
16    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
17        &mut self.children
18    }
19
20    fn needs_draw(&self) -> bool {
21        if !self.is_visible() {
22            return false;
23        }
24        self.scrollbar_animation_active()
25            || self.painted_style_epoch.get() != current_scroll_style_epoch()
26            || self.children().iter().any(|c| c.needs_draw())
27    }
28
29    fn margin(&self) -> Insets {
30        self.base.margin
31    }
32    fn widget_base(&self) -> Option<&WidgetBase> {
33        Some(&self.base)
34    }
35    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
36        Some(&mut self.base)
37    }
38    fn h_anchor(&self) -> HAnchor {
39        self.base.h_anchor
40    }
41    fn v_anchor(&self) -> VAnchor {
42        self.base.v_anchor
43    }
44    fn min_size(&self) -> Size {
45        self.base.min_size
46    }
47    fn max_size(&self) -> Size {
48        self.base.max_size
49    }
50
51    fn hit_test(&self, local_pos: Point) -> bool {
52        if self.v.dragging || self.h.dragging || self.middle_dragging {
53            return true;
54        }
55        let b = self.bounds();
56        local_pos.x >= 0.0
57            && local_pos.x <= b.width
58            && local_pos.y >= 0.0
59            && local_pos.y <= b.height
60    }
61
62    fn claims_pointer_exclusively(&self, local_pos: Point) -> bool {
63        if self.v.dragging || self.h.dragging || self.middle_dragging {
64            return true;
65        }
66        let (vw, vh) = self.viewport();
67        if self.v.enabled && self.v.content > vh && self.pos_in_v_hover(local_pos) {
68            return true;
69        }
70        if self.h.enabled && self.h.content > vw && self.pos_in_h_hover(local_pos) {
71            return true;
72        }
73        false
74    }
75
76    fn layout(&mut self, available: Size) -> Size {
77        // Pull live state from external cells first.
78        if let Some(c) = &self.offset_cell {
79            self.v.offset = c.get();
80        }
81        if let Some(c) = &self.h_offset_cell {
82            self.h.offset = c.get();
83        }
84        if let Some(c) = &self.visibility_cell {
85            self.bar_visibility = c.get();
86        } else if !self.visibility_explicit {
87            self.bar_visibility = current_scroll_visibility();
88        }
89        if let Some(c) = &self.style_cell {
90            self.style = c.get();
91        } else if !self.style_explicit {
92            // No explicit override → follow the global scroll-bar style so
93            // the Appearance demo restyles every `ScrollView` in the app.
94            self.style = current_scroll_style();
95        }
96
97        self.bounds = Rect::new(0.0, 0.0, available.width, available.height);
98
99        // For horizontal scrolling, content width is unconstrained (the child
100        // may return a width larger than our viewport).  For vertical-only, we
101        // pin child to the viewport width so wrapping widgets behave.
102        let (vw_guess, _vh_guess) = self.viewport();
103        let child_in_w = if self.h.enabled {
104            f64::MAX / 2.0
105        } else {
106            vw_guess
107        };
108        let child_in_h = f64::MAX / 2.0;
109
110        if let Some(child) = self.children.first_mut() {
111            let natural = child.layout(Size::new(child_in_w, child_in_h));
112            self.v.content = natural.height;
113            self.h.content = if self.h.enabled {
114                natural.width
115            } else {
116                vw_guess
117            };
118        }
119
120        // Re-query viewport now that content dimensions are known (Solid bars
121        // may reserve different space once we know overflow).
122        let (vw, vh) = self.viewport();
123
124        if self.stick_to_bottom && self.was_at_bottom {
125            self.v.offset = self.v.max_scroll(vh);
126        }
127        self.clamp_offsets();
128        self.was_at_bottom = (self.v.max_scroll(vh) - self.v.offset).abs() < 0.5;
129
130        // Publish offsets / max / viewport.
131        if let Some(c) = &self.offset_cell {
132            c.set(self.v.offset);
133        }
134        if let Some(c) = &self.max_scroll_cell {
135            c.set(self.v.max_scroll(vh));
136        }
137        if let Some(c) = &self.h_offset_cell {
138            c.set(self.h.offset);
139        }
140        if let Some(c) = &self.h_max_scroll_cell {
141            c.set(self.h.max_scroll(vw));
142        }
143        if let Some(c) = &self.viewport_cell {
144            // Content-space viewport rect in Y-UP content coords:
145            //   x = h_offset  (left edge of visible region)
146            //   y = (v_content_height - vh - v_offset) if inverting, but we
147            //       expose TOP-DOWN coords for easier row math: y = v_offset.
148            // We output a rect where (x, y) is the TOP-LEFT of visible content
149            // in a conventional top-down space, and (width, height) = viewport.
150            c.set(Rect::new(self.h.offset, self.v.offset, vw, vh));
151        }
152
153        // Position child inside the widget.
154        if let Some(child) = self.children.first_mut() {
155            let child_y = vh - self.v.content + self.v.offset;
156            let child_x = -self.h.offset;
157            child.set_bounds(Rect::new(
158                child_x.round(),
159                child_y.round(),
160                if self.h.enabled { self.h.content } else { vw },
161                self.v.content,
162            ));
163        }
164
165        available
166    }
167
168    fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
169
170    fn clip_children_rect(&self) -> Option<(f64, f64, f64, f64)> {
171        // Clip children to the VIEWPORT so the content never overpaints the
172        // scrollbar gutter or the edge guards.
173        let (vw, vh) = self.viewport();
174        Some((0.0, self.bounds.height - vh, vw, vh))
175    }
176
177    fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
178        self.painted_style_epoch.set(current_scroll_style_epoch());
179
180        // ── Fade gradient under the scrollbars ──
181        //
182        // egui paints the fade after content but before the bars, so the
183        // fade hints clipped content without dimming the scrollbar itself.
184        if self.style.fade_strength > 0.001 && self.style.fade_size > 0.5 {
185            self.paint_fade(ctx);
186        }
187
188        // ── Vertical bar ──
189        let (_, vh) = self.viewport();
190        let v_geom = self.v_scrollbar_geometry();
191        if let Some(bar) = self
192            .v
193            .prepare_paint(vh, self.style, self.bar_visibility, v_geom)
194        {
195            paint_prepared_scrollbar(ctx, bar);
196        }
197
198        // ── Horizontal bar ──
199        let (vw, _) = self.viewport();
200        let h_geom = self.h_scrollbar_geometry();
201        if let Some(bar) = self
202            .h
203            .prepare_paint(vw, self.style, self.bar_visibility, h_geom)
204        {
205            paint_prepared_scrollbar(ctx, bar);
206        }
207    }
208
209    fn on_event(&mut self, event: &Event) -> EventResult {
210        match event {
211            // ── Mouse wheel ───────────────────────────────────────────────────
212            Event::MouseWheel {
213                delta_y, delta_x, ..
214            } => {
215                let mut consumed = false;
216                if self.v.enabled {
217                    self.v.offset = self.v.offset + delta_y * 40.0;
218                    consumed = true;
219                }
220                if self.h.enabled {
221                    self.h.offset = self.h.offset + delta_x * 40.0;
222                    consumed = true;
223                }
224                self.clamp_offsets();
225                let (_, vh) = self.viewport();
226                self.was_at_bottom = (self.v.max_scroll(vh) - self.v.offset).abs() < 0.5;
227                if let Some(c) = &self.offset_cell {
228                    c.set(self.v.offset);
229                }
230                if let Some(c) = &self.h_offset_cell {
231                    c.set(self.h.offset);
232                }
233                if consumed {
234                    crate::animation::request_draw();
235                    EventResult::Consumed
236                } else {
237                    EventResult::Ignored
238                }
239            }
240
241            // ── Mouse move ────────────────────────────────────────────────────
242            Event::MouseMove { pos } => {
243                if self.middle_dragging {
244                    let world = crate::widget::current_mouse_world().unwrap_or(*pos);
245                    let dx = world.x - self.middle_start_world.x;
246                    let dy = world.y - self.middle_start_world.y;
247                    if self.h.enabled {
248                        self.h.offset = self.middle_start_h_offset - dx;
249                    }
250                    if self.v.enabled {
251                        self.v.offset = self.middle_start_v_offset + dy;
252                    }
253                    self.clamp_offsets();
254                    let (_, vh) = self.viewport();
255                    self.was_at_bottom = (self.v.max_scroll(vh) - self.v.offset).abs() < 0.5;
256                    self.publish_offsets();
257                    crate::animation::request_draw();
258                    return EventResult::Consumed;
259                }
260
261                let (vw, vh) = self.viewport();
262                let v_scroll = self.v.enabled && self.v.content > vh;
263                let h_scroll = self.h.enabled && self.h.content > vw;
264                let v_hover_changed =
265                    self.v
266                        .update_hover(*pos, vh, self.style, self.v_scrollbar_geometry());
267                let h_hover_changed =
268                    self.h
269                        .update_hover(*pos, vw, self.style, self.h_scrollbar_geometry());
270                if (v_scroll && v_hover_changed) || (h_scroll && h_hover_changed) {
271                    crate::animation::request_draw();
272                }
273
274                if self.v.dragging {
275                    if self
276                        .v
277                        .drag_to(*pos, vh, self.style, self.v_scrollbar_geometry())
278                    {
279                        self.was_at_bottom = (self.v.max_scroll(vh) - self.v.offset).abs() < 0.5;
280                        if let Some(c) = &self.offset_cell {
281                            c.set(self.v.offset);
282                        }
283                        crate::animation::request_draw();
284                    }
285                    return EventResult::Consumed;
286                }
287                if self.h.dragging {
288                    if self
289                        .h
290                        .drag_to(*pos, vw, self.style, self.h_scrollbar_geometry())
291                    {
292                        if let Some(c) = &self.h_offset_cell {
293                            c.set(self.h.offset);
294                        }
295                        crate::animation::request_draw();
296                    }
297                    return EventResult::Consumed;
298                }
299                EventResult::Ignored
300            }
301
302            // ── Mouse down ────────────────────────────────────────────────────
303            Event::MouseDown {
304                pos,
305                button: MouseButton::Middle,
306                ..
307            } => {
308                let (vw, vh) = self.viewport();
309                if (self.v.enabled && self.v.content > vh)
310                    || (self.h.enabled && self.h.content > vw)
311                {
312                    self.middle_dragging = true;
313                    self.middle_start_world = crate::widget::current_mouse_world().unwrap_or(*pos);
314                    self.middle_start_v_offset = self.v.offset;
315                    self.middle_start_h_offset = self.h.offset;
316                    crate::animation::request_draw();
317                    return EventResult::Consumed;
318                }
319                EventResult::Ignored
320            }
321
322            Event::MouseDown {
323                pos,
324                button: MouseButton::Left,
325                ..
326            } => {
327                let (vw, vh) = self.viewport();
328                let v_scroll = self.v.enabled && self.v.content > vh;
329                let h_scroll = self.h.enabled && self.h.content > vw;
330
331                if v_scroll && self.pos_in_v_hover(*pos) {
332                    if self
333                        .v
334                        .begin_drag(*pos, vh, self.style, self.v_scrollbar_geometry())
335                    {
336                        // No tick: thumb grab has no visible effect until
337                        // the cursor actually moves.
338                    } else if self
339                        .v
340                        .page_at(*pos, vh, self.style, self.v_scrollbar_geometry())
341                    {
342                        if let Some(c) = &self.offset_cell {
343                            c.set(self.v.offset);
344                        }
345                        // Offset changed — visible scroll.
346                        crate::animation::request_draw();
347                    }
348                    return EventResult::Consumed;
349                }
350                if h_scroll && self.pos_in_h_hover(*pos) {
351                    if self
352                        .h
353                        .begin_drag(*pos, vw, self.style, self.h_scrollbar_geometry())
354                    {
355                        // No tick — see v-axis thumb grab comment above.
356                    } else if self
357                        .h
358                        .page_at(*pos, vw, self.style, self.h_scrollbar_geometry())
359                    {
360                        if let Some(c) = &self.h_offset_cell {
361                            c.set(self.h.offset);
362                        }
363                        crate::animation::request_draw();
364                    }
365                    return EventResult::Consumed;
366                }
367                EventResult::Ignored
368            }
369
370            // ── Mouse up ──────────────────────────────────────────────────────
371            Event::MouseUp { button, .. } => {
372                let was = self.v.dragging
373                    || self.h.dragging
374                    || (*button == MouseButton::Middle && self.middle_dragging);
375                self.v.dragging = false;
376                self.h.dragging = false;
377                if *button == MouseButton::Middle {
378                    self.middle_dragging = false;
379                }
380                if was {
381                    crate::animation::request_draw();
382                    EventResult::Consumed
383                } else {
384                    EventResult::Ignored
385                }
386            }
387
388            _ => EventResult::Ignored,
389        }
390    }
391
392    /// Surface the per-axis offsets and the maximum scroll distance as
393    /// inspector / test properties.  Tests use these to verify that a
394    /// shrunken viewport actually exposes scrollable overflow.
395    fn properties(&self) -> Vec<(&'static str, String)> {
396        let (vw, vh) = self.viewport();
397        vec![
398            ("v_enabled", self.v.enabled.to_string()),
399            ("h_enabled", self.h.enabled.to_string()),
400            ("bar_visibility", format!("{:?}", self.bar_visibility)),
401            ("v_offset", format!("{:.1}", self.v.offset)),
402            ("h_offset", format!("{:.1}", self.h.offset)),
403            ("max_scroll", format!("{:.1}", self.v.max_scroll(vh))),
404            ("h_max_scroll", format!("{:.1}", self.h.max_scroll(vw))),
405            ("v_content", format!("{:.1}", self.v.content)),
406            ("h_content", format!("{:.1}", self.h.content)),
407        ]
408    }
409}
410
411impl ScrollView {
412    /// Paint a gradient fade at the scroll-axis edges using thin horizontal or
413    /// vertical strips with linearly interpolated alpha.  The strip closest to
414    /// the clip edge is most opaque; the strip furthest inside the viewport is
415    /// transparent — giving a smooth dissolve into the surrounding background.
416    fn paint_fade(&self, ctx: &mut dyn DrawCtx) {
417        let v = ctx.visuals();
418        // Default to window_fill (correct only when the ScrollView sits
419        // directly on a window).  Callers placing the view on a panel
420        // / coloured container MUST pass `with_fade_color(...)` to
421        // match the ancestor background — otherwise the fade looks
422        // like a bright halo of the wrong colour.  See the doc-comment
423        // on `ScrollView::with_fade_color`.
424        let c = self.fade_color.unwrap_or(v.window_fill);
425        let (vw, vh) = self.viewport();
426        let strength = self.style.fade_strength.clamp(0.0, 1.0) as f32;
427        let size = self.style.fade_size.max(0.0);
428        let max_a = strength;
429
430        // Fade appears only near edges where content is clipped.
431        if self.v.enabled {
432            if self.v.offset > 0.5 {
433                // Top edge (Y-up: high Y).  Gradient transparent→opaque going up.
434                Self::fill_v_gradient(
435                    ctx,
436                    c,
437                    max_a,
438                    0.0,
439                    self.bounds.height - size,
440                    vw,
441                    size,
442                    false,
443                );
444            }
445            if (self.v.max_scroll(vh) - self.v.offset) > 0.5 {
446                // Bottom edge.  Gradient transparent→opaque going down.
447                let y_bottom = self.bounds.height - vh;
448                Self::fill_v_gradient(ctx, c, max_a, 0.0, y_bottom, vw, size, true);
449            }
450        }
451        if self.h.enabled {
452            if self.h.offset > 0.5 {
453                // Left edge.  Gradient transparent→opaque going left.
454                Self::fill_h_gradient(ctx, c, max_a, 0.0, self.bounds.height - vh, size, vh, true);
455            }
456            if (self.h.max_scroll(vw) - self.h.offset) > 0.5 {
457                // Right edge.  Gradient transparent→opaque going right.
458                Self::fill_h_gradient(
459                    ctx,
460                    c,
461                    max_a,
462                    vw - size,
463                    self.bounds.height - vh,
464                    size,
465                    vh,
466                    false,
467                );
468            }
469        }
470    }
471
472    /// Draw a vertical gradient rect using `STEPS` thin strips.
473    ///
474    /// When `opaque_at_bottom` is `true` the gradient runs opaque→transparent
475    /// bottom-to-top (bottom edge fade); when `false` it runs
476    /// transparent→opaque bottom-to-top (top edge fade).
477    fn fill_v_gradient(
478        ctx: &mut dyn DrawCtx,
479        c: Color,
480        max_alpha: f32,
481        x: f64,
482        y: f64,
483        w: f64,
484        h: f64,
485        opaque_at_bottom: bool,
486    ) {
487        const STEPS: usize = 64;
488        let strip_h = h / STEPS as f64;
489        for i in 0..STEPS {
490            // t = 0 at the transparent end, 1 at the opaque end.
491            let t = (i as f32 + 0.5) / STEPS as f32;
492            let a = if opaque_at_bottom { 1.0 - t } else { t };
493            ctx.set_fill_color(Color::rgba(c.r, c.g, c.b, a * max_alpha));
494            ctx.begin_path();
495            ctx.rect(x, y + i as f64 * strip_h, w, strip_h + 0.5);
496            ctx.fill();
497        }
498    }
499
500    /// Draw a horizontal gradient rect using `STEPS` thin strips.
501    ///
502    /// When `opaque_at_left` is `true` the gradient runs opaque→transparent
503    /// left-to-right (left edge fade); when `false` it runs
504    /// transparent→opaque left-to-right (right edge fade).
505    fn fill_h_gradient(
506        ctx: &mut dyn DrawCtx,
507        c: Color,
508        max_alpha: f32,
509        x: f64,
510        y: f64,
511        w: f64,
512        h: f64,
513        opaque_at_left: bool,
514    ) {
515        const STEPS: usize = 64;
516        let strip_w = w / STEPS as f64;
517        for i in 0..STEPS {
518            let t = (i as f32 + 0.5) / STEPS as f32;
519            let a = if opaque_at_left { 1.0 - t } else { t };
520            ctx.set_fill_color(Color::rgba(c.r, c.g, c.b, a * max_alpha));
521            ctx.begin_path();
522            ctx.rect(x + i as f64 * strip_w, y, strip_w + 0.5, h);
523            ctx.fill();
524        }
525    }
526}