1use crate::scroll_area::ScrollAreaType;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4enum ScrollVisibilityState {
5 Hidden,
6 Scrolling,
7 Interacting,
8 Idle,
9}
10
11#[derive(Debug, Clone, Copy)]
12pub struct ScrollAreaVisibilityConfig {
13 pub scroll_hide_delay_ticks: u64,
17 pub scroll_end_debounce_ticks: u64,
19}
20
21#[derive(Debug, Clone, Copy)]
22pub struct ScrollAreaVisibilityInput {
23 pub ty: ScrollAreaType,
24 pub hovered: bool,
25 pub has_overflow: bool,
26 pub scrolled: bool,
27 pub tick: u64,
28}
29
30#[derive(Debug, Clone, Copy)]
31pub struct ScrollAreaVisibilityOutput {
32 pub visible: bool,
33 pub animating: bool,
35}
36
37#[derive(Debug, Clone)]
38pub struct ScrollAreaVisibility {
39 last_ty: Option<ScrollAreaType>,
40 hover_visible_until: Option<u64>,
41 was_hovered: bool,
42 scroll_state: ScrollVisibilityState,
43 last_scroll_tick: Option<u64>,
44 scroll_hide_deadline: Option<u64>,
45}
46
47impl Default for ScrollAreaVisibility {
48 fn default() -> Self {
49 Self {
50 last_ty: None,
51 hover_visible_until: None,
52 was_hovered: false,
53 scroll_state: ScrollVisibilityState::Hidden,
54 last_scroll_tick: None,
55 scroll_hide_deadline: None,
56 }
57 }
58}
59
60impl ScrollAreaVisibility {
61 pub fn update(
62 &mut self,
63 input: ScrollAreaVisibilityInput,
64 config: ScrollAreaVisibilityConfig,
65 ) -> ScrollAreaVisibilityOutput {
66 if self.last_ty != Some(input.ty) {
67 self.reset_for_type(input.ty);
68 }
69
70 if !input.has_overflow {
71 self.reset_for_type(input.ty);
72 return ScrollAreaVisibilityOutput {
73 visible: false,
74 animating: false,
75 };
76 }
77
78 match input.ty {
79 ScrollAreaType::Always | ScrollAreaType::Auto => ScrollAreaVisibilityOutput {
80 visible: true,
81 animating: false,
82 },
83 ScrollAreaType::Hover => self.update_hover(input, config),
84 ScrollAreaType::Scroll => self.update_scroll(input, config),
85 }
86 }
87
88 fn reset_for_type(&mut self, ty: ScrollAreaType) {
89 self.last_ty = Some(ty);
90 self.hover_visible_until = None;
91 self.was_hovered = false;
92 self.scroll_state = ScrollVisibilityState::Hidden;
93 self.last_scroll_tick = None;
94 self.scroll_hide_deadline = None;
95 }
96
97 fn update_hover(
98 &mut self,
99 input: ScrollAreaVisibilityInput,
100 config: ScrollAreaVisibilityConfig,
101 ) -> ScrollAreaVisibilityOutput {
102 if input.hovered {
103 self.was_hovered = true;
104 self.hover_visible_until = None;
105 return ScrollAreaVisibilityOutput {
106 visible: true,
107 animating: false,
108 };
109 }
110
111 if self.was_hovered {
112 self.was_hovered = false;
113 self.hover_visible_until =
114 Some(input.tick.saturating_add(config.scroll_hide_delay_ticks));
115 }
116
117 let Some(deadline) = self.hover_visible_until else {
118 return ScrollAreaVisibilityOutput {
119 visible: false,
120 animating: false,
121 };
122 };
123
124 let visible = input.tick < deadline;
125 if !visible {
126 self.hover_visible_until = None;
127 }
128
129 ScrollAreaVisibilityOutput {
130 visible,
131 animating: visible,
132 }
133 }
134
135 fn update_scroll(
136 &mut self,
137 input: ScrollAreaVisibilityInput,
138 config: ScrollAreaVisibilityConfig,
139 ) -> ScrollAreaVisibilityOutput {
140 if input.scrolled {
141 self.last_scroll_tick = Some(input.tick);
142 self.scroll_hide_deadline = None;
143 match self.scroll_state {
144 ScrollVisibilityState::Hidden | ScrollVisibilityState::Idle => {
145 self.scroll_state = ScrollVisibilityState::Scrolling;
146 }
147 ScrollVisibilityState::Scrolling | ScrollVisibilityState::Interacting => {}
148 }
149 }
150
151 if input.hovered {
152 match self.scroll_state {
153 ScrollVisibilityState::Scrolling | ScrollVisibilityState::Idle => {
154 self.scroll_state = ScrollVisibilityState::Interacting;
155 self.scroll_hide_deadline = None;
156 }
157 ScrollVisibilityState::Hidden | ScrollVisibilityState::Interacting => {}
158 }
159 } else if self.scroll_state == ScrollVisibilityState::Interacting {
160 self.scroll_state = ScrollVisibilityState::Idle;
161 self.scroll_hide_deadline =
162 Some(input.tick.saturating_add(config.scroll_hide_delay_ticks));
163 }
164
165 if self.scroll_state == ScrollVisibilityState::Scrolling
166 && let Some(last) = self.last_scroll_tick
167 && input.tick.saturating_sub(last) >= config.scroll_end_debounce_ticks
168 {
169 self.scroll_state = ScrollVisibilityState::Idle;
170 self.scroll_hide_deadline =
171 Some(input.tick.saturating_add(config.scroll_hide_delay_ticks));
172 }
173
174 if self.scroll_state == ScrollVisibilityState::Idle
175 && let Some(deadline) = self.scroll_hide_deadline
176 && input.tick >= deadline
177 {
178 self.scroll_state = ScrollVisibilityState::Hidden;
179 self.scroll_hide_deadline = None;
180 }
181
182 let visible = self.scroll_state != ScrollVisibilityState::Hidden;
183 let animating = match self.scroll_state {
184 ScrollVisibilityState::Hidden | ScrollVisibilityState::Interacting => false,
185 ScrollVisibilityState::Scrolling | ScrollVisibilityState::Idle => true,
186 };
187
188 ScrollAreaVisibilityOutput { visible, animating }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 const HIDE: u64 = 10;
197 const DEBOUNCE: u64 = 2;
198
199 fn cfg() -> ScrollAreaVisibilityConfig {
200 ScrollAreaVisibilityConfig {
201 scroll_hide_delay_ticks: HIDE,
202 scroll_end_debounce_ticks: DEBOUNCE,
203 }
204 }
205
206 #[test]
207 fn hover_hides_after_delay() {
208 let mut vis = ScrollAreaVisibility::default();
209
210 let out_init = vis.update(
211 ScrollAreaVisibilityInput {
212 ty: ScrollAreaType::Hover,
213 hovered: false,
214 has_overflow: true,
215 scrolled: false,
216 tick: 0,
217 },
218 cfg(),
219 );
220 assert!(!out_init.visible);
221 assert!(!out_init.animating);
222
223 let out0 = vis.update(
224 ScrollAreaVisibilityInput {
225 ty: ScrollAreaType::Hover,
226 hovered: true,
227 has_overflow: true,
228 scrolled: false,
229 tick: 1,
230 },
231 cfg(),
232 );
233 assert!(out0.visible);
234 assert!(!out0.animating);
235
236 let out1 = vis.update(
237 ScrollAreaVisibilityInput {
238 ty: ScrollAreaType::Hover,
239 hovered: false,
240 has_overflow: true,
241 scrolled: false,
242 tick: 2,
243 },
244 cfg(),
245 );
246 assert!(out1.visible);
247 assert!(out1.animating);
248
249 let out2 = vis.update(
250 ScrollAreaVisibilityInput {
251 ty: ScrollAreaType::Hover,
252 hovered: false,
253 has_overflow: true,
254 scrolled: false,
255 tick: 2 + HIDE,
256 },
257 cfg(),
258 );
259 assert!(!out2.visible);
260 assert!(!out2.animating);
261
262 let out3 = vis.update(
263 ScrollAreaVisibilityInput {
264 ty: ScrollAreaType::Hover,
265 hovered: false,
266 has_overflow: true,
267 scrolled: false,
268 tick: 3 + HIDE,
269 },
270 cfg(),
271 );
272 assert!(
273 !out3.visible,
274 "hover mode should remain hidden after the delay"
275 );
276 assert!(!out3.animating);
277 }
278
279 #[test]
280 fn scroll_shows_while_scrolling_then_hides() {
281 let mut vis = ScrollAreaVisibility::default();
282
283 let out0 = vis.update(
284 ScrollAreaVisibilityInput {
285 ty: ScrollAreaType::Scroll,
286 hovered: false,
287 has_overflow: true,
288 scrolled: false,
289 tick: 1,
290 },
291 cfg(),
292 );
293 assert!(!out0.visible);
294
295 let out1 = vis.update(
296 ScrollAreaVisibilityInput {
297 ty: ScrollAreaType::Scroll,
298 hovered: false,
299 has_overflow: true,
300 scrolled: true,
301 tick: 2,
302 },
303 cfg(),
304 );
305 assert!(out1.visible);
306 assert!(out1.animating);
307
308 let out2 = vis.update(
309 ScrollAreaVisibilityInput {
310 ty: ScrollAreaType::Scroll,
311 hovered: false,
312 has_overflow: true,
313 scrolled: false,
314 tick: 2 + DEBOUNCE,
315 },
316 cfg(),
317 );
318 assert!(out2.visible);
319 assert!(out2.animating);
320
321 let out3 = vis.update(
322 ScrollAreaVisibilityInput {
323 ty: ScrollAreaType::Scroll,
324 hovered: false,
325 has_overflow: true,
326 scrolled: false,
327 tick: 2 + DEBOUNCE + HIDE,
328 },
329 cfg(),
330 );
331 assert!(!out3.visible);
332 assert!(!out3.animating);
333 }
334
335 #[test]
336 fn scroll_interaction_keeps_visible_until_leave() {
337 let mut vis = ScrollAreaVisibility::default();
338
339 let _ = vis.update(
340 ScrollAreaVisibilityInput {
341 ty: ScrollAreaType::Scroll,
342 hovered: false,
343 has_overflow: true,
344 scrolled: true,
345 tick: 1,
346 },
347 cfg(),
348 );
349
350 let out0 = vis.update(
351 ScrollAreaVisibilityInput {
352 ty: ScrollAreaType::Scroll,
353 hovered: true,
354 has_overflow: true,
355 scrolled: false,
356 tick: 2,
357 },
358 cfg(),
359 );
360 assert!(out0.visible);
361 assert!(!out0.animating);
362
363 let out1 = vis.update(
364 ScrollAreaVisibilityInput {
365 ty: ScrollAreaType::Scroll,
366 hovered: false,
367 has_overflow: true,
368 scrolled: false,
369 tick: 3,
370 },
371 cfg(),
372 );
373 assert!(out1.visible);
374 assert!(out1.animating);
375 }
376}