1use gpui::{
3 Animation, AnimationExt, AnyElement, App, ElementId, Empty, FocusHandle, IntoElement,
4 ListAlignment, ListOffset, ListState, MouseButton, Pixels, RenderOnce, SharedString, Window,
5 prelude::*, px,
6};
7use std::{
8 cell::{Cell, RefCell},
9 ops::Range,
10 rc::Rc,
11 sync::atomic::{AtomicUsize, Ordering},
12};
13
14#[derive(Clone)]
18pub struct VariableList<K> {
19 id: usize,
20 activity: Rc<Cell<ScrollbarActivity>>,
21 hover: Rc<Cell<crate::scroll::Hover>>,
22 pub state: ListState,
23 keys: Rc<RefCell<Vec<K>>>,
24 visible: Rc<RefCell<Range<usize>>>,
25 grab: Rc<Cell<Option<(Pixels, Pixels)>>>,
26 end_inset: Rc<Cell<Pixels>>,
27}
28impl<K: Clone + Eq + 'static> Default for VariableList<K> {
29 fn default() -> Self {
30 let state = ListState::new(0, ListAlignment::Top, px(500.));
31 state.set_follow_mode(gpui::FollowMode::Tail);
32 Self {
33 id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
34 state,
35 activity: Default::default(),
36 hover: Default::default(),
37 keys: Default::default(),
38 visible: Default::default(),
39 grab: Default::default(),
40 end_inset: Default::default(),
41 }
42 }
43}
44impl<K: Clone + Eq + 'static> VariableList<K> {
45 pub fn sync(&self, next: Vec<K>) {
47 let mut keys = self.keys.borrow_mut();
48 if *keys == next {
49 return;
50 }
51 let top = self.state.logical_scroll_top();
52 let anchor = keys.get(top.item_ix).cloned();
53 let following = self.state.is_following_tail();
54 let prefix = keys.iter().zip(&next).take_while(|(a, b)| a == b).count();
55 let suffix = keys[prefix..]
56 .iter()
57 .rev()
58 .zip(next[prefix..].iter().rev())
59 .take_while(|(a, b)| a == b)
60 .count();
61 self.state
62 .splice(prefix..keys.len() - suffix, next.len() - prefix - suffix);
63 if !following
64 && let Some(ix) = anchor.and_then(|key| next.iter().position(|item| *item == key))
65 {
66 self.state.scroll_to(ListOffset {
67 item_ix: ix,
68 offset_in_item: top.offset_in_item,
69 });
70 }
71 *keys = next;
72 }
73 pub fn invalidate(&self, key: &K) {
74 if let Some(ix) = self.keys.borrow().iter().position(|item| item == key) {
75 self.state.remeasure_items(ix..ix + 1);
76 }
77 }
78 pub fn set_end_inset(&self, inset: Pixels) {
80 self.end_inset.set(inset);
81 }
82
83 pub fn visible_range(&self) -> Range<usize> {
84 self.visible.borrow().clone()
85 }
86 pub fn scroll_to(&self, index: usize) {
87 self.state.pause_following_tail();
88 self.state.scroll_to(ListOffset {
89 item_ix: index,
90 offset_in_item: px(0.),
91 });
92 }
93 pub fn focus_item(&self, index: usize, focus: Option<FocusHandle>) {
95 let top = self.state.logical_scroll_top();
96 let following = self.state.is_following_tail();
97 self.state.splice_focusable(index..index + 1, [focus]);
98 if !following {
99 self.state.scroll_to(top);
100 }
101 }
102 pub fn render(
103 &self,
104 render: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
105 on_visible: impl Fn(Range<usize>, &mut Window, &mut App) + 'static,
106 ) -> gpui::Div {
107 let bar = self.scrollbar();
108 let state = self.state.clone();
109 let visible = self.visible.clone();
110 gpui::div()
111 .size_full()
112 .relative()
113 .child(gpui::list(self.state.clone(), render).size_full())
114 .child(
115 gpui::canvas(
116 move |_, window, cx| {
117 let start = state.logical_scroll_top().item_ix.min(state.item_count());
118 let mut end = start;
119 let viewport = state.viewport_bounds();
120 while end < state.item_count() {
121 if let Some(bounds) = state.bounds_for_item(end) {
122 if bounds.top() >= viewport.bottom() {
123 break;
124 }
125 end += 1;
126 } else {
127 break;
128 }
129 }
130 let range = start..end;
131 if *visible.borrow() != range {
132 *visible.borrow_mut() = range.clone();
133 on_visible(range, window, cx);
134 }
135 },
136 |_, _, _, _| {},
137 )
138 .absolute()
139 .size_full(),
140 )
141 .child(bar)
142 }
143 fn scrollbar(&self) -> Scrollbar {
144 Scrollbar {
145 id: self.id,
146 state: self.state.clone(),
147 grab: self.grab.clone(),
148 activity: self.activity.clone(),
149 hover: self.hover.clone(),
150 end_inset: self.end_inset.get(),
151 }
152 }
153}
154
155static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
157
158#[derive(Clone, Copy, Default)]
159struct ScrollbarActivity {
160 offset: Pixels,
161 max: Pixels,
162 generation: u64,
163}
164
165#[derive(IntoElement)]
173struct Scrollbar {
174 id: usize,
175 state: ListState,
176 grab: Rc<Cell<Option<(Pixels, Pixels)>>>,
177 activity: Rc<Cell<ScrollbarActivity>>,
178 hover: Rc<Cell<crate::scroll::Hover>>,
179 end_inset: Pixels,
180}
181
182impl RenderOnce for Scrollbar {
183 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
184 let mode = crate::scroll::visibility(cx);
185 if mode == crate::scroll::Visibility::Never {
186 return Empty.into_any_element();
187 }
188 let always = mode == crate::scroll::Visibility::Always || cx.reduce_motion();
189 let viewport = self.state.viewport_bounds().size.height;
190 let max = self.state.max_offset_for_scrollbar().y;
191 let offset = self.state.scroll_px_offset_for_scrollbar().y;
192 let track = (viewport - self.end_inset).max(px(0.));
193
194 let mut held = self.activity.get();
197 if (offset - held.offset).abs() > px(0.5) || (max - held.max).abs() > px(0.5) {
198 held.offset = offset;
199 held.max = max;
200 held.generation += 1;
201 self.activity.set(held);
202 }
203 let generation = held.generation;
204
205 let range = crate::scroll::thumb_in_track(viewport, max, offset, track);
206 let travel = range
207 .as_ref()
208 .map(|range| track - (range.end - range.start))
209 .unwrap_or(px(0.));
210 let thumb = range.map(|range| {
211 let down_state = self.state.clone();
212 let down_grab = self.grab.clone();
213 let thumb = gpui::div()
214 .id(SharedString::from(format!("list-{}-thumb", self.id)))
215 .absolute()
216 .top(range.start)
217 .h(range.end - range.start)
218 .w(px(crate::scroll::THUMB))
219 .rounded_full()
220 .bg(theme::Theme::of(cx).text_muted.opacity(0.4))
221 .on_mouse_down(MouseButton::Left, move |event, window, cx| {
222 down_state.scrollbar_drag_started();
223 down_grab.set(Some((
224 event.position.y,
225 down_state.scroll_px_offset_for_scrollbar().y,
226 )));
227 cx.stop_propagation();
228 window.refresh();
229 });
230 if always {
231 return thumb.into_any_element();
232 }
233 let anim_hover = self.hover.clone();
239 let anim_grab = self.grab.clone();
240 thumb
241 .with_animation(
242 ElementId::from(SharedString::from(format!(
243 "list-{}-fade-{generation}",
244 self.id
245 ))),
246 Animation::new(crate::scroll::TRANSIENT_IDLE),
247 move |el, progress| {
248 if anim_hover.get().held() || anim_grab.get().is_some() {
249 el
250 } else if progress < 1.0 {
251 el.opacity(1.0 - progress)
252 } else {
253 el.hidden()
254 }
255 },
256 )
257 .into_any_element()
258 });
259
260 let activity = self.activity.clone();
261 let state = self.state.clone();
262 let grab = self.grab.clone();
263 let strip = gpui::div()
264 .id(SharedString::from(format!("list-{}-track", self.id)))
265 .absolute()
266 .right_0()
267 .top_0()
268 .bottom(self.end_inset)
269 .w(px(10.))
270 .flex()
271 .justify_center();
272 crate::scroll::hover_track(strip, self.hover, move |window, _| {
273 let mut held = activity.get();
274 held.generation += 1;
275 activity.set(held);
276 window.refresh();
277 })
278 .children(thumb)
279 .child(
280 gpui::canvas(
281 move |bounds, window, _| {
282 if (bounds.size.height - track).abs() > px(0.5) {
286 window.request_animation_frame();
287 }
288 },
289 move |_, _, window, _| {
290 let moved_state = state.clone();
294 let moved_grab = grab.clone();
295 window.on_mouse_event(move |event: &gpui::MouseMoveEvent, phase, window, _| {
296 if phase == gpui::DispatchPhase::Bubble
297 && let Some((start, offset)) = moved_grab.get()
298 && travel > px(0.)
299 {
300 moved_state.set_offset_from_scrollbar(gpui::point(
301 px(0.),
302 (offset - (event.position.y - start) / travel * max)
303 .clamp(-max, px(0.)),
304 ));
305 window.refresh();
306 }
307 });
308 let up_state = state.clone();
309 let up_grab = grab.clone();
310 window.on_mouse_event(move |event: &gpui::MouseUpEvent, _, window, _| {
311 if event.button == MouseButton::Left && up_grab.take().is_some() {
312 up_state.scrollbar_drag_ended();
313 window.refresh();
314 }
315 });
316 },
317 )
318 .absolute()
319 .size_full(),
320 )
321 .into_any_element()
322 }
323}