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