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

            
12
use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
13
use 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(Debug, Clone, Default)]
18
#[repr(C)]
19
pub 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

            
31
impl ScrollbarRequirements {
32
    /// Checks if the presence of scrollbars reduces the available inner size,
33
    /// which would necessitate a reflow of the content.
34
10080
    pub fn needs_reflow(&self) -> bool {
35
10080
        self.scrollbar_width > 0.0 || self.scrollbar_height > 0.0
36
10080
    }
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
28525
    pub fn shrink_size(&self, size: LogicalSize) -> LogicalSize {
44
28525
        LogicalSize {
45
28525
            width: (size.width - self.scrollbar_width).max(0.0),
46
28525
            height: (size.height - self.scrollbar_height).max(0.0),
47
28525
        }
48
28525
    }
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)]
59
pub 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

            
83
impl 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
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
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
    match orientation {
148
        ScrollbarOrientation::Vertical => {
149
            // Track runs along the right edge of inner_rect
150
            let track_total = if has_other_scrollbar {
151
                inner_rect.size.height - scrollbar_width_px
152
            } else {
153
                inner_rect.size.height
154
            };
155

            
156
            let track_rect = LogicalRect {
157
                origin: LogicalPosition::new(
158
                    inner_rect.origin.x + inner_rect.size.width - scrollbar_width_px,
159
                    inner_rect.origin.y,
160
                ),
161
                size: LogicalSize::new(scrollbar_width_px, track_total),
162
            };
163

            
164
            let usable_track_length = (track_total - 2.0 * button_size).max(0.0);
165
            let viewport_length = inner_rect.size.height;
166
            let content_length = content_size.height;
167

            
168
            let thumb_size_ratio = if content_length > 0.0 {
169
                (viewport_length / content_length).min(1.0)
170
            } else {
171
                1.0
172
            };
173
            let thumb_length = (usable_track_length * thumb_size_ratio)
174
                .max(scrollbar_width_px * 2.0)
175
                .min(usable_track_length);
176

            
177
            let max_scroll = (content_length - viewport_length).max(0.0);
178
            let scroll_ratio = if max_scroll > 0.0 {
179
                (scroll_offset.abs() / max_scroll).clamp(0.0, 1.0)
180
            } else {
181
                0.0
182
            };
183

            
184
            let thumb_offset = (usable_track_length - thumb_length) * scroll_ratio;
185

            
186
            ScrollbarGeometry {
187
                orientation,
188
                track_rect,
189
                button_size,
190
                usable_track_length,
191
                thumb_length,
192
                thumb_size_ratio,
193
                scroll_ratio,
194
                thumb_offset,
195
                max_scroll,
196
                width_px: scrollbar_width_px,
197
            }
198
        }
199
        ScrollbarOrientation::Horizontal => {
200
            // Track runs along the bottom edge of inner_rect
201
            let track_total = if has_other_scrollbar {
202
                inner_rect.size.width - scrollbar_width_px
203
            } else {
204
                inner_rect.size.width
205
            };
206

            
207
            let track_rect = LogicalRect {
208
                origin: LogicalPosition::new(
209
                    inner_rect.origin.x,
210
                    inner_rect.origin.y + inner_rect.size.height - scrollbar_width_px,
211
                ),
212
                size: LogicalSize::new(track_total, scrollbar_width_px),
213
            };
214

            
215
            let usable_track_length = (track_total - 2.0 * button_size).max(0.0);
216
            let viewport_length = inner_rect.size.width;
217
            let content_length = content_size.width;
218

            
219
            let thumb_size_ratio = if content_length > 0.0 {
220
                (viewport_length / content_length).min(1.0)
221
            } else {
222
                1.0
223
            };
224
            let thumb_length = (usable_track_length * thumb_size_ratio)
225
                .max(scrollbar_width_px * 2.0)
226
                .min(usable_track_length);
227

            
228
            let max_scroll = (content_length - viewport_length).max(0.0);
229
            let scroll_ratio = if max_scroll > 0.0 {
230
                (scroll_offset.abs() / max_scroll).clamp(0.0, 1.0)
231
            } else {
232
                0.0
233
            };
234

            
235
            let thumb_offset = (usable_track_length - thumb_length) * scroll_ratio;
236

            
237
            ScrollbarGeometry {
238
                orientation,
239
                track_rect,
240
                button_size,
241
                usable_track_length,
242
                thumb_length,
243
                thumb_size_ratio,
244
                scroll_ratio,
245
                thumb_offset,
246
                max_scroll,
247
                width_px: scrollbar_width_px,
248
            }
249
        }
250
    }
251
}