Skip to main content

azul_layout/solver3/
scrollbar.rs

1//! Scrollbar geometry computation — single source of truth for the layout solver.
2//!
3//! Provides [`ScrollbarRequirements`] (whether scrollbars are needed and how much
4//! space they reserve) and [`ScrollbarGeometry`] (track, thumb, and button rects).
5//!
6//! The main entry point is [`compute_scrollbar_geometry`], whose output is consumed by:
7//! - Display list painting (`paint_scrollbars`)
8//! - GPU transform updates (`update_scrollbar_transforms`)
9//! - Hit-testing (`hit_test_component`)
10//! - Drag delta conversion (`handle_scrollbar_drag`)
11
12use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
13use azul_core::dom::ScrollbarOrientation;
14
15/// Information about scrollbar requirements and dimensions
16// +spec:overflow:55c244 - scrollbar appearance, size, and edge placement are UA-defined
17#[derive(Copy, Debug, Clone, Default)]
18#[repr(C)]
19pub struct ScrollbarRequirements {
20    pub needs_horizontal: bool,
21    pub needs_vertical: bool,
22    /// Layout-reserved width for a vertical scrollbar (0.0 for overlay)
23    pub scrollbar_width: f32,
24    /// Layout-reserved height for a horizontal scrollbar (0.0 for overlay)
25    pub scrollbar_height: f32,
26    /// Visual rendering width of the scrollbar in CSS pixels (e.g. 8.0 for thin).
27    /// Non-zero even for overlay scrollbars. Used by GPU state for thumb positioning.
28    pub visual_width_px: f32,
29}
30
31impl ScrollbarRequirements {
32    /// Checks if the presence of scrollbars reduces the available inner size,
33    /// which would necessitate a reflow of the content.
34    #[must_use] pub fn needs_reflow(&self) -> bool {
35        self.scrollbar_width > 0.0 || self.scrollbar_height > 0.0
36    }
37
38    // +spec:box-model:20c3c8 - scrollbar space reserved between inner border edge and outer padding edge
39    // +spec:box-model:32cd53 - scrollbar space subtracted from containing block dimensions
40    // +spec:overflow:30a49c - scrollbar space subtracted from content area
41    /// Takes a size (representing a content-box) and returns a new size
42    /// reduced by the dimensions of any active scrollbars.
43    #[must_use] pub fn shrink_size(&self, size: LogicalSize) -> LogicalSize {
44        LogicalSize {
45            width: (size.width - self.scrollbar_width).max(0.0),
46            height: (size.height - self.scrollbar_height).max(0.0),
47        }
48    }
49}
50
51/// Single source of truth for scrollbar geometry.
52///
53/// Computed once by [`compute_scrollbar_geometry`], then used by:
54/// - Display list painting (`paint_scrollbars`)
55/// - GPU transform updates (`update_scrollbar_transforms`)
56/// - Hit-testing (`hit_test_component`)
57/// - Drag delta conversion (`handle_scrollbar_drag`)
58#[derive(Debug, Clone, Copy)]
59pub struct ScrollbarGeometry {
60    /// Orientation (vertical or horizontal)
61    pub orientation: ScrollbarOrientation,
62    /// The full track rect in the container's coordinate space
63    pub track_rect: LogicalRect,
64    /// Button size (square: width = height = `scrollbar_width_px`)
65    pub button_size: f32,
66    /// Usable track length after subtracting buttons and corner
67    /// = `track_total` - 2*`button_size`
68    pub usable_track_length: f32,
69    /// The thumb length (min-clamped to 2*`width_px`)
70    pub thumb_length: f32,
71    /// Thumb size as ratio of viewport / content (0.0–1.0)
72    pub thumb_size_ratio: f32,
73    /// Scroll ratio (0.0 at top/left, 1.0 at bottom/right)
74    pub scroll_ratio: f32,
75    /// Thumb offset in pixels from the start of the usable track region
76    pub thumb_offset: f32,
77    /// Max scroll distance in content pixels
78    pub max_scroll: f32,
79    /// CSS-specified scrollbar thickness (width for vertical, height for horizontal)
80    pub width_px: f32,
81}
82
83impl Default for ScrollbarGeometry {
84    fn default() -> Self {
85        Self {
86            orientation: ScrollbarOrientation::Vertical,
87            track_rect: LogicalRect::zero(),
88            button_size: 0.0,
89            usable_track_length: 0.0,
90            thumb_length: 0.0,
91            thumb_size_ratio: 0.0,
92            scroll_ratio: 0.0,
93            thumb_offset: 0.0,
94            max_scroll: 0.0,
95            width_px: 0.0,
96        }
97    }
98}
99
100/// Compute scrollbar geometry for one axis.
101///
102/// This is the **single source of truth** for all scrollbar calculations.
103/// All consumers (display list painting, GPU transforms, hit-testing, drag)
104/// must use this function to ensure consistent geometry.
105///
106/// # Parameters
107/// - `orientation`: Vertical or horizontal scrollbar
108/// - `inner_rect`: The padding-box (border-box minus borders) of the scroll container,
109///   in the container's coordinate space (absolute window coordinates)
110/// - `content_size`: Total content size (from `get_content_size()` or `virtual_scroll_size`)
111/// - `scroll_offset`: Current scroll offset (y for vertical, x for horizontal; positive = scrolled)
112/// - `scrollbar_width_px`: CSS-resolved scrollbar thickness in pixels
113/// - `has_other_scrollbar`: Whether the perpendicular scrollbar is also visible
114///   (reduces track length by one `scrollbar_width_px` for the corner)
115#[must_use] pub fn compute_scrollbar_geometry(
116    orientation: ScrollbarOrientation,
117    inner_rect: LogicalRect,
118    content_size: LogicalSize,
119    scroll_offset: f32,
120    scrollbar_width_px: f32,
121    has_other_scrollbar: bool,
122) -> ScrollbarGeometry {
123    // For macOS-style overlay scrollbars, callers should pass button_size=0.
124    // For legacy scrollbars with arrow buttons, button_size=scrollbar_width_px.
125    compute_scrollbar_geometry_with_button_size(
126        orientation,
127        inner_rect,
128        content_size,
129        scroll_offset,
130        scrollbar_width_px,
131        has_other_scrollbar,
132        scrollbar_width_px, // default: reserve button space
133    )
134}
135
136/// Like [`compute_scrollbar_geometry`] but allows overriding the button size.
137/// Pass `button_size = 0.0` for macOS-style overlay scrollbars (no arrow buttons).
138#[must_use] pub fn compute_scrollbar_geometry_with_button_size(
139    orientation: ScrollbarOrientation,
140    inner_rect: LogicalRect,
141    content_size: LogicalSize,
142    scroll_offset: f32,
143    scrollbar_width_px: f32,
144    has_other_scrollbar: bool,
145    button_size: f32,
146) -> ScrollbarGeometry {
147    let (track_total, viewport_length, content_length, track_rect) = match orientation {
148        ScrollbarOrientation::Vertical => {
149            let track_total = if has_other_scrollbar {
150                inner_rect.size.height - scrollbar_width_px
151            } else {
152                inner_rect.size.height
153            };
154            let track_rect = LogicalRect {
155                origin: LogicalPosition::new(
156                    inner_rect.origin.x + inner_rect.size.width - scrollbar_width_px,
157                    inner_rect.origin.y,
158                ),
159                size: LogicalSize::new(scrollbar_width_px, track_total),
160            };
161            (track_total, inner_rect.size.height, content_size.height, track_rect)
162        }
163        ScrollbarOrientation::Horizontal => {
164            let track_total = if has_other_scrollbar {
165                inner_rect.size.width - scrollbar_width_px
166            } else {
167                inner_rect.size.width
168            };
169            let track_rect = LogicalRect {
170                origin: LogicalPosition::new(
171                    inner_rect.origin.x,
172                    inner_rect.origin.y + inner_rect.size.height - scrollbar_width_px,
173                ),
174                size: LogicalSize::new(track_total, scrollbar_width_px),
175            };
176            (track_total, inner_rect.size.width, content_size.width, track_rect)
177        }
178    };
179
180    compute_thumb_geometry(
181        orientation,
182        track_rect,
183        track_total,
184        viewport_length,
185        content_length,
186        button_size,
187        scrollbar_width_px,
188        scroll_offset,
189    )
190}
191
192#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
193fn compute_thumb_geometry(
194    orientation: ScrollbarOrientation,
195    track_rect: LogicalRect,
196    track_total: f32,
197    viewport_length: f32,
198    content_length: f32,
199    button_size: f32,
200    scrollbar_width_px: f32,
201    scroll_offset: f32,
202) -> ScrollbarGeometry {
203    let usable_track_length = (track_total - 2.0 * button_size).max(0.0);
204
205    let thumb_size_ratio = if content_length > 0.0 {
206        (viewport_length / content_length).min(1.0)
207    } else {
208        1.0
209    };
210    let thumb_length = (usable_track_length * thumb_size_ratio)
211        .max(scrollbar_width_px * 2.0)
212        .min(usable_track_length);
213
214    let max_scroll = (content_length - viewport_length).max(0.0);
215    let scroll_ratio = if max_scroll > 0.0 {
216        (scroll_offset.abs() / max_scroll).clamp(0.0, 1.0)
217    } else {
218        0.0
219    };
220
221    let thumb_offset = (usable_track_length - thumb_length) * scroll_ratio;
222
223    ScrollbarGeometry {
224        orientation,
225        track_rect,
226        button_size,
227        usable_track_length,
228        thumb_length,
229        thumb_size_ratio,
230        scroll_ratio,
231        thumb_offset,
232        max_scroll,
233        width_px: scrollbar_width_px,
234    }
235}