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