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