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