blitz_dom/node/
scrollbar.rs1use blitz_traits::node_id::NodeId;
6use kurbo::Rect as KurboRect;
7use taffy::AbsoluteAxis;
8use web_time::Duration;
9
10use super::Node;
11
12pub(crate) const FADE_DELAY: Duration = Duration::from_millis(500);
16pub(crate) const FADE_DURATION: Duration = Duration::from_millis(200);
17
18pub(crate) fn opacity_at(elapsed: Duration) -> f32 {
22 match elapsed.checked_sub(FADE_DELAY) {
23 None => 1.0,
24 Some(fading) => 1.0 - (fading.as_secs_f32() / FADE_DURATION.as_secs_f32()).min(1.0),
25 }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct ScrollbarRef {
31 pub node_id: NodeId,
32 pub axis: AbsoluteAxis,
33}
34
35pub use style::properties::generated::longhands::scrollbar_width::computed_value::T as ScrollbarWidth;
37
38#[derive(Clone, Debug, Default, PartialEq)]
41pub enum ScrollbarColor {
42 #[default]
43 Auto,
44 Colors {
45 thumb: style::color::AbsoluteColor,
46 track: style::color::AbsoluteColor,
47 },
48}
49
50impl Node {
51 pub fn scrollbar_width(&self) -> ScrollbarWidth {
53 self.primary_styles()
54 .map(|style| style.clone_scrollbar_width())
55 .unwrap_or(ScrollbarWidth::Auto)
56 }
57
58 pub fn scrollbar_color(&self) -> ScrollbarColor {
61 let Some(style) = self.primary_styles() else {
62 return ScrollbarColor::Auto;
63 };
64 let current_color = style.clone_color();
65 match style.clone_scrollbar_color() {
66 style::values::computed::ScrollbarColor::Auto => ScrollbarColor::Auto,
67 style::values::computed::ScrollbarColor::Colors { thumb, track } => {
68 ScrollbarColor::Colors {
69 thumb: thumb.resolve_to_absolute(¤t_color),
70 track: track.resolve_to_absolute(¤t_color),
71 }
72 }
73 }
74 }
75
76 pub fn wants_scrollbar(&self, axis: AbsoluteAxis) -> bool {
81 use style::values::computed::Overflow;
82 let Some(style) = self.primary_styles() else {
83 return false;
84 };
85 if self.scrollbar_width() == ScrollbarWidth::None {
86 return false;
87 }
88 let (overflow, scroll_extent) = match axis {
89 AbsoluteAxis::Horizontal => (
90 style.clone_overflow_x(),
91 self.final_layout().scroll_width() as f64,
92 ),
93 AbsoluteAxis::Vertical => (
94 style.clone_overflow_y(),
95 self.final_layout().scroll_height() as f64,
96 ),
97 };
98 match overflow {
99 Overflow::Scroll => true,
100 Overflow::Auto => scroll_extent > 0.5,
101 _ => false,
102 }
103 }
104
105 fn scrollport(&self) -> KurboRect {
109 let layout = self.final_layout();
110 KurboRect::new(
111 layout.border.left as f64,
112 layout.border.top as f64,
113 layout.size.width as f64 - layout.border.right as f64,
114 layout.size.height as f64 - layout.border.bottom as f64,
115 )
116 }
117
118 pub fn scrollbar_thumb(&self, axis: AbsoluteAxis) -> Option<KurboRect> {
122 const THUMB_THICKNESS: f64 = 10.0;
124 const THIN_THUMB_THICKNESS: f64 = 6.0;
125 const THUMB_MARGIN: f64 = 2.0;
126 const MIN_THUMB_LENGTH: f64 = 32.0;
127
128 let layout = self.final_layout();
129 let scroll_extent = match axis {
130 AbsoluteAxis::Horizontal => layout.scroll_width() as f64,
131 AbsoluteAxis::Vertical => layout.scroll_height() as f64,
132 };
133 if scroll_extent <= 0.5 {
134 return None;
135 }
136
137 let thickness = match self.scrollbar_width() {
138 ScrollbarWidth::Thin => THIN_THUMB_THICKNESS,
139 _ => THUMB_THICKNESS,
140 };
141
142 let port = self.scrollport();
143 let (viewport_len, scroll_offset) = match axis {
144 AbsoluteAxis::Horizontal => (port.width(), self.scroll_offset().x),
145 AbsoluteAxis::Vertical => (port.height(), self.scroll_offset().y),
146 };
147 let thumb_len = (viewport_len * viewport_len / (viewport_len + scroll_extent))
148 .max(MIN_THUMB_LENGTH)
149 .min(viewport_len);
150 let progress = (scroll_offset / scroll_extent).clamp(0.0, 1.0);
151 let thumb_start = match progress * (viewport_len - thumb_len) {
154 start if start > 0.0 && start < 1.0 => 1.0,
155 start => start,
156 };
157
158 Some(match axis {
159 AbsoluteAxis::Horizontal => KurboRect::new(
160 port.x0 + thumb_start,
161 port.y1 - THUMB_MARGIN - thickness,
162 port.x0 + thumb_start + thumb_len,
163 port.y1 - THUMB_MARGIN,
164 ),
165 AbsoluteAxis::Vertical => KurboRect::new(
166 port.x1 - THUMB_MARGIN - thickness,
167 port.y0 + thumb_start,
168 port.x1 - THUMB_MARGIN,
169 port.y0 + thumb_start + thumb_len,
170 ),
171 })
172 }
173
174 pub fn scrollbar_drag_ratio(&self, axis: AbsoluteAxis) -> f64 {
176 let Some(thumb) = self.scrollbar_thumb(axis) else {
177 return 0.0;
178 };
179 let port = self.scrollport();
180 let (scroll_extent, viewport_len, thumb_len) = match axis {
181 AbsoluteAxis::Horizontal => (
182 self.final_layout().scroll_width() as f64,
183 port.width(),
184 thumb.width(),
185 ),
186 AbsoluteAxis::Vertical => (
187 self.final_layout().scroll_height() as f64,
188 port.height(),
189 thumb.height(),
190 ),
191 };
192 let track_play = viewport_len - thumb_len;
193 if track_play <= 0.0 {
194 return 0.0;
195 }
196 scroll_extent / track_play
197 }
198
199 pub(crate) fn scrollbar_at_local(&self, x: f64, y: f64) -> Option<ScrollbarRef> {
204 if !cfg!(feature = "scrollbars") {
205 return None;
206 }
207 for axis in [AbsoluteAxis::Vertical, AbsoluteAxis::Horizontal] {
208 if !self.wants_scrollbar(axis) {
209 continue;
210 }
211 if let Some(thumb) = self.scrollbar_thumb(axis)
212 && thumb.contains(kurbo::Point::new(x, y))
213 {
214 return Some(ScrollbarRef {
215 node_id: self.id,
216 axis,
217 });
218 }
219 }
220 None
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn opacity_holds_through_the_fade_delay_then_fades_out() {
230 assert_eq!(opacity_at(Duration::ZERO), 1.0);
231 assert_eq!(opacity_at(FADE_DELAY), 1.0);
232 let mid_fade = opacity_at(FADE_DELAY + FADE_DURATION / 2);
233 assert!((mid_fade - 0.5).abs() < 0.01, "got {mid_fade}");
234 assert_eq!(opacity_at(FADE_DELAY + FADE_DURATION), 0.0);
235 assert_eq!(opacity_at(Duration::from_secs(3600)), 0.0);
236 }
237}