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(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 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 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)
115pub 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).
138pub 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}