1use std::sync::{Arc, Mutex};
2use std::time::Duration;
3
4use gpui::{AsyncApp, Bounds, Context, Pixels, Point, Task, WeakEntity, px};
5
6pub struct AutoScroll {
11 shared: Arc<Mutex<Option<Pixels>>>,
14 task: Option<Task<()>>,
15 pub last_drag_position: Option<Point<Pixels>>,
17}
18
19impl Default for AutoScroll {
20 fn default() -> Self {
21 Self {
22 shared: Arc::new(Mutex::new(None)),
23 task: None,
24 last_drag_position: None,
25 }
26 }
27}
28
29impl AutoScroll {
30 pub fn delta(&self) -> Option<Pixels> {
32 *self.shared.lock().unwrap()
33 }
34
35 pub fn compute_delta(y: Pixels, bounds: Bounds<Pixels>) -> Option<Pixels> {
37 const MIN_SPEED: f32 = 12.0;
38 const MAX_SPEED: f32 = 64.0;
39 const INNER_ZONE: f32 = 16.0;
42 const OUTER_RAMP: f32 = 80.0;
46
47 let bottom_trigger = bounds.bottom() - px(INNER_ZONE);
48 let top_trigger = bounds.top() + px(INNER_ZONE);
49
50 if y > bottom_trigger {
51 let t = ((y - bottom_trigger) / px(INNER_ZONE + OUTER_RAMP)).min(1.0);
52 Some(px(MIN_SPEED + t * (MAX_SPEED - MIN_SPEED)))
53 } else if y < top_trigger {
54 let t = ((top_trigger - y) / px(INNER_ZONE + OUTER_RAMP)).min(1.0);
55 Some(px(-(MIN_SPEED + t * (MAX_SPEED - MIN_SPEED))))
56 } else {
57 None
58 }
59 }
60
61 pub fn set<T, F>(&mut self, delta: Option<Pixels>, cx: &mut Context<T>, tick: F)
66 where
67 T: 'static,
68 F: Fn(Pixels, &mut T, &mut Context<T>) + Send + 'static,
69 {
70 let was_idle = self.task.is_none();
71 *self.shared.lock().unwrap() = delta;
72
73 if delta.is_none() {
74 self.task = None;
75 return;
76 }
77
78 if was_idle {
79 let shared = Arc::clone(&self.shared);
80 self.task = Some(cx.spawn(Self::task_loop(shared, tick)));
81 }
82 }
83
84 fn task_loop<T, F>(
85 shared: Arc<Mutex<Option<Pixels>>>,
86 tick: F,
87 ) -> impl AsyncFnOnce(WeakEntity<T>, &mut AsyncApp) + 'static
88 where
89 T: 'static,
90 F: Fn(Pixels, &mut T, &mut Context<T>) + Send + 'static,
91 {
92 async move |this: WeakEntity<T>, cx: &mut AsyncApp| {
93 loop {
94 cx.background_executor()
95 .timer(Duration::from_millis(16))
96 .await;
97 let Some(delta) = *shared.lock().unwrap() else {
98 break;
99 };
100 let alive = this
101 .update(cx, |state, cx| {
102 tick(delta, state, cx);
103 true
104 })
105 .unwrap_or(false);
106 if !alive {
107 break;
108 }
109 }
110 }
111 }
112
113 pub fn is_active(&self) -> bool {
115 self.delta().is_some()
116 }
117
118 pub fn stop(&mut self) {
120 *self.shared.lock().unwrap() = None;
121 self.task = None;
122 self.last_drag_position = None;
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use gpui::{Bounds, point, size};
129
130 use super::*;
131
132 #[test]
133 fn delta_uses_dead_zone_and_symmetric_edge_ramps() {
134 let bounds = Bounds::new(point(px(0.), px(100.)), size(px(200.), px(100.)));
135
136 assert_eq!(AutoScroll::compute_delta(px(150.), bounds), None);
137
138 let top = AutoScroll::compute_delta(px(80.), bounds).unwrap();
139 let bottom = AutoScroll::compute_delta(px(220.), bounds).unwrap();
140 assert_eq!(top, -bottom);
141 assert!(top < px(-12.));
142 }
143
144 #[test]
145 fn stop_clears_delta_and_drag_position() {
146 let mut scroll = AutoScroll::default();
147 *scroll.shared.lock().unwrap() = Some(px(20.));
148 scroll.last_drag_position = Some(point(px(1.), px(2.)));
149
150 scroll.stop();
151
152 assert!(!scroll.is_active());
153 assert_eq!(scroll.last_drag_position, None);
154 }
155}