Skip to main content

agg_gui/widgets/rich_text/editor/
scroll.rs

1//! Internal vertical scrolling for [`RichTextEdit`], composing the shared
2//! [`ScrollbarAxis`](crate::widgets::scrollbar::ScrollbarAxis) exactly as
3//! `TextArea` does, so overflow math, thumb geometry, drag/hover and painting
4//! stay pixel-consistent with every other scroll bar in the app.
5//!
6//! The scroll offset (`vbar.offset`) is folded into
7//! [`doc_top_yup`](RichTextEdit::doc_top_yup), so all geometry (paint,
8//! hit-test, caret) inherits scrolling for free.
9
10use crate::draw_ctx::DrawCtx;
11use crate::geometry::Point;
12use crate::widgets::scroll_view::{
13    current_scroll_style, current_scroll_visibility, ScrollBarStyle, ScrollBarVisibility,
14};
15use crate::widgets::scrollbar::{
16    paint_prepared_scrollbar, ScrollbarGeometry, ScrollbarOrientation, DEFAULT_GRAB_MARGIN,
17};
18
19use super::super::model::DocPos;
20use super::RichTextEdit;
21
22/// Outcome of routing a `MouseMove` through the scroll bar first.
23pub(crate) enum ScrollMove {
24    /// The bar thumb is being dragged; `bool` = offset changed this move.
25    Dragging(bool),
26    /// Not dragging; `bool` = the bar's hover state changed this move.
27    Hover(bool),
28}
29
30impl RichTextEdit {
31    /// Furthest the content can scroll (0 when everything fits).
32    pub(crate) fn max_scroll_y(&self) -> f64 {
33        (self.content_height() - self.inner_height()).max(0.0)
34    }
35
36    /// Current vertical scroll offset (0 = top). Exposed for tests/inspectors.
37    pub fn scroll_offset(&self) -> f64 {
38        self.vbar.offset
39    }
40
41    fn scroll_style(&self) -> ScrollBarStyle {
42        current_scroll_style()
43    }
44    fn scroll_visibility(&self) -> ScrollBarVisibility {
45        current_scroll_visibility()
46    }
47
48    /// Refresh the axis with the current content height and re-clamp the offset.
49    pub(crate) fn sync_scroll(&mut self) {
50        self.vbar.enabled = true;
51        self.vbar.content = self.content_height();
52        self.vbar.clamp_offset(self.inner_height());
53    }
54
55    fn v_scrollbar_geometry(&self) -> ScrollbarGeometry {
56        ScrollbarGeometry {
57            orientation: ScrollbarOrientation::Vertical,
58            track_start: self.padding,
59            track_end: (self.bounds.height - self.padding).max(self.padding),
60            cross_end: (self.bounds.width - 2.0).max(0.0),
61            hit_margin: DEFAULT_GRAB_MARGIN,
62        }
63    }
64
65    /// Scroll so the caret's visual line is fully visible.
66    pub(crate) fn ensure_caret_visible(&mut self) {
67        let caret = self.core.borrow().caret();
68        let vlines = self.visual_lines();
69        let vi = self.caret_visual_line(caret);
70        let Some(&(_, _, y_top, height)) = vlines.get(vi) else {
71            return;
72        };
73        let inner_h = self.inner_height();
74        let top = y_top;
75        let bottom = y_top + height;
76        let mut off = self.vbar.offset;
77        if top < off {
78            off = top;
79        } else if bottom > off + inner_h {
80            off = bottom - inner_h;
81        }
82        self.vbar.offset = off.clamp(0.0, self.max_scroll_y());
83    }
84
85    pub(crate) fn scroll_by_wheel(&mut self, delta_y: f64) -> bool {
86        if self.max_scroll_y() <= 0.0 {
87            return false;
88        }
89        // Windows convention: one wheel notch scrolls 3 lines. `delta_y` arrives
90        // in line-units (1.0 == one notch; the platform adapter pre-scales
91        // trackpad pixel deltas into fractional line-units), so scaling by
92        // 3 * line-height gives a crisp 3-line notch while keeping pixel-precise
93        // trackpad panning smooth — more content-aware than ScrollView's flat
94        // 40 px/line, which felt slow for the editor's line pitch.
95        let line_h = self.wheel_line_height();
96        self.vbar
97            .scroll_by(-delta_y * 3.0 * line_h, self.inner_height())
98    }
99
100    /// A representative visual-line height for wheel scrolling. Rich text has
101    /// variable line heights, so use the first laid-out line as the notch unit,
102    /// falling back to the 16 px base size when no layout exists yet.
103    fn wheel_line_height(&self) -> f64 {
104        self.visual_lines()
105            .first()
106            .map(|&(_, _, _, h)| h)
107            .filter(|h| *h > 0.0)
108            .unwrap_or(16.0 * 1.35)
109    }
110
111    pub(crate) fn scrollbar_begin_drag(&mut self, pos: Point) -> bool {
112        if self.max_scroll_y() <= 0.0 {
113            return false;
114        }
115        let geom = self.v_scrollbar_geometry();
116        self.vbar
117            .begin_drag(pos, self.inner_height(), self.scroll_style(), geom)
118    }
119
120    pub(crate) fn scrollbar_on_mouse_move(&mut self, pos: Point) -> ScrollMove {
121        let geom = self.v_scrollbar_geometry();
122        let vp = self.inner_height();
123        let style = self.scroll_style();
124        if self.vbar.dragging {
125            ScrollMove::Dragging(self.vbar.drag_to(pos, vp, style, geom))
126        } else {
127            ScrollMove::Hover(self.vbar.update_hover(pos, vp, style, geom))
128        }
129    }
130
131    pub(crate) fn scrollbar_end_drag(&mut self) -> bool {
132        let was = self.vbar.dragging;
133        self.vbar.dragging = false;
134        was
135    }
136
137    pub(crate) fn paint_scrollbar(&mut self, ctx: &mut dyn DrawCtx) {
138        let vp = self.inner_height();
139        let style = self.scroll_style();
140        let vis = self.scroll_visibility();
141        let geom = self.v_scrollbar_geometry();
142        if let Some(bar) = self.vbar.prepare_paint(vp, style, vis, geom) {
143            paint_prepared_scrollbar(ctx, bar);
144        }
145    }
146
147    pub(crate) fn scrollbar_animating(&self) -> bool {
148        self.vbar.animation_active()
149    }
150
151    /// Scroll the caret into view after an edit or navigation keystroke, given
152    /// the (possibly just-moved) caret position.
153    pub(crate) fn ensure_pos_visible(&mut self, _caret: DocPos) {
154        self.ensure_caret_visible();
155    }
156}