1use crate::draggable::{DragAxis, DragState, drag_handle, reorder_indices};
2use gpui::{
3 AnyElement, App, Bounds, Context, Entity, IntoElement, ListAlignment, ListState, MouseButton,
4 MouseMoveEvent, Pixels, Point, Render, Size, Window, deferred, div, list, prelude::*, px,
5};
6use std::cell::RefCell;
7use std::rc::Rc;
8use std::sync::Arc;
9
10type RenderItem = dyn Fn(usize, &mut Window, &mut App) -> AnyElement + 'static;
11type ReorderCallback = dyn Fn(usize, usize, &mut Window, &mut App) + 'static;
12
13pub struct VirtualizedList {
19 item_count: usize,
20 list_state: ListState,
21 render_item: Arc<RenderItem>,
22 overdraw: Pixels,
23 item_spacing: Pixels,
24 height: Option<Pixels>,
25 measure_all_items: bool,
26 order: Vec<usize>,
27 draggable: bool,
28 drag_state: DragState,
29 item_bounds: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
30 drag_reference_bounds: Vec<Option<Bounds<Pixels>>>,
31 on_reorder: Option<Arc<ReorderCallback>>,
32}
33
34impl VirtualizedList {
35 pub fn new(
36 item_count: usize,
37 _cx: &mut Context<Self>,
38 render_item: impl Fn(usize, &mut Window, &mut App) -> AnyElement + 'static,
39 ) -> Self {
40 let overdraw = px(640.0);
41 Self {
42 item_count,
43 list_state: ListState::new(item_count, ListAlignment::Top, overdraw),
44 render_item: Arc::new(render_item),
45 overdraw,
46 item_spacing: px(0.0),
47 height: None,
48 measure_all_items: false,
49 order: (0..item_count).collect(),
50 draggable: false,
51 drag_state: DragState::default(),
52 item_bounds: Rc::new(RefCell::new(vec![None; item_count])),
53 drag_reference_bounds: vec![None; item_count],
54 on_reorder: None,
55 }
56 }
57
58 pub fn entity(
59 item_count: usize,
60 cx: &mut App,
61 render_item: impl Fn(usize, &mut Window, &mut App) -> AnyElement + 'static,
62 ) -> Entity<Self> {
63 cx.new(|cx| Self::new(item_count, cx, render_item))
64 }
65
66 pub fn list_state(&self) -> ListState {
67 self.list_state.clone()
68 }
69
70 pub fn set_item_count(&mut self, item_count: usize) {
71 if self.item_count == item_count {
72 return;
73 }
74 self.item_count = item_count;
75 self.order = (0..item_count).collect();
76 self.drag_state.cancel();
77 *self.item_bounds.borrow_mut() = vec![None; item_count];
78 self.drag_reference_bounds = vec![None; item_count];
79 self.list_state = Self::new_list_state(item_count, self.overdraw, self.measure_all_items);
80 }
81
82 pub fn set_render_item(
83 &mut self,
84 render_item: impl Fn(usize, &mut Window, &mut App) -> AnyElement + 'static,
85 ) {
86 self.render_item = Arc::new(render_item);
87 }
88
89 pub fn set_item_spacing(&mut self, spacing: impl Into<Pixels>) {
90 let spacing = spacing.into();
91 if self.item_spacing == spacing {
92 return;
93 }
94 self.item_spacing = spacing;
95 self.list_state.reset(self.item_count);
96 }
97
98 pub fn set_overdraw(&mut self, overdraw: impl Into<Pixels>) {
99 let overdraw = overdraw.into();
100 if self.overdraw == overdraw {
101 return;
102 }
103 self.overdraw = overdraw;
104 self.list_state = Self::new_list_state(self.item_count, overdraw, self.measure_all_items);
105 }
106
107 pub fn set_height(&mut self, height: Option<Pixels>) {
108 if self.height == height {
109 return;
110 }
111 self.height = height;
112 self.list_state.reset(self.item_count);
113 }
114
115 pub fn set_draggable(&mut self, draggable: bool) {
116 self.draggable = draggable;
117 if !draggable {
118 self.drag_state.cancel();
119 self.drag_reference_bounds.clear();
120 }
121 }
122
123 pub fn set_on_reorder(
124 &mut self,
125 callback: impl Fn(usize, usize, &mut Window, &mut App) + 'static,
126 ) {
127 self.on_reorder = Some(Arc::new(callback));
128 }
129
130 pub fn order(&self) -> &[usize] {
131 &self.order
132 }
133
134 fn start_drag(&mut self, index: usize, position: gpui::Point<Pixels>, cx: &mut Context<Self>) {
135 if !self.draggable {
136 return;
137 }
138 let bounds = self.item_bounds.borrow().get(index).copied().flatten();
139 self.drag_reference_bounds = self.item_bounds.borrow().clone();
140 self.drag_state.start_at(index, position, bounds);
141 cx.notify();
142 }
143
144 fn update_drag_target_from_position(
145 &mut self,
146 position: Point<Pixels>,
147 cx: &mut Context<Self>,
148 ) {
149 let Some(active) = self.drag_state.active_index() else {
150 return;
151 };
152 if self.drag_reference_bounds.is_empty() {
153 return;
154 }
155
156 let mut target = active.min(self.drag_reference_bounds.len().saturating_sub(1));
157 let mut nearest_distance = Pixels::MAX;
158 for (index, item_bounds) in self.drag_reference_bounds.iter().enumerate() {
159 let Some(item_bounds) = item_bounds else {
160 continue;
161 };
162 if item_bounds.contains(&position) {
163 target = index;
164 break;
165 }
166
167 let distance = (position.y - item_bounds.center().y).abs();
168 if distance < nearest_distance {
169 nearest_distance = distance;
170 target = index;
171 }
172 }
173
174 if self.drag_state.over_index() != Some(target) {
175 self.drag_state.set_over(target);
176 self.list_state.reset(self.item_count);
177 cx.notify();
178 }
179 }
180
181 fn hover_drag(
182 &mut self,
183 index: usize,
184 event: &MouseMoveEvent,
185 _window: &mut Window,
186 cx: &mut Context<Self>,
187 ) {
188 self.drag_state.update_position(event.position);
189 let Some(active) = self.drag_state.active_index() else {
190 return;
191 };
192 if event.pressed_button != Some(MouseButton::Left) {
193 return;
194 }
195 if index >= self.order.len() || index == active {
196 return;
197 }
198 self.update_drag_target_from_position(event.position, cx);
199 }
200
201 fn update_drag_position(
202 &mut self,
203 event: &MouseMoveEvent,
204 window: &mut Window,
205 cx: &mut Context<Self>,
206 ) {
207 if self.drag_state.active_index().is_none() {
208 return;
209 }
210 if event.pressed_button != Some(MouseButton::Left) {
211 self.finish_drag(0, window, cx);
212 return;
213 }
214 self.drag_state.update_position(event.position);
215 self.update_drag_target_from_position(event.position, cx);
216 cx.notify();
217 }
218
219 fn finish_drag(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
220 let Some((from, to)) = self.drag_state.finish() else {
221 return;
222 };
223 self.drag_reference_bounds.clear();
224 if from != to {
225 if reorder_indices(&mut self.order, from, to) {
226 self.list_state.reset(self.item_count);
227 }
228 if let Some(callback) = self.on_reorder.clone() {
229 callback(from, to, window, cx);
230 }
231 }
232 let _ = index;
233 cx.notify();
234 }
235
236 pub fn measure_all_items_for_scrollbar(&mut self) {
243 if self.measure_all_items {
244 return;
245 }
246 self.measure_all_items = true;
247 self.list_state = self.list_state.clone().measure_all();
248 }
249
250 fn new_list_state(item_count: usize, overdraw: Pixels, measure_all_items: bool) -> ListState {
251 let state = ListState::new(item_count, ListAlignment::Top, overdraw);
252 if measure_all_items {
253 state.measure_all()
254 } else {
255 state
256 }
257 }
258
259 pub fn remeasure(&self) {
264 self.list_state.reset(self.item_count);
265 }
266
267 pub fn remeasure_items(&self, range: std::ops::Range<usize>) {
269 self.list_state.splice(range.clone(), range.len());
270 }
271}
272
273impl Render for VirtualizedList {
274 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
275 let render_item = self.render_item.clone();
276 let spacing = self.item_spacing;
277 let mut display_order = self.order.clone();
278 let draggable = self.draggable;
279 let drag_state = self.drag_state.clone();
280 let drag_active = drag_state.active_index().is_some();
281 let active_item = drag_state
282 .origin_index()
283 .and_then(|index| self.order.get(index).copied());
284 if let (Some(active), Some(over)) = (drag_state.active_index(), drag_state.over_index()) {
285 reorder_indices(&mut display_order, active, over);
286 }
287 let drag_reference_bounds = self.drag_reference_bounds.clone();
288 let active_size = drag_state
289 .origin_index()
290 .and_then(|index| drag_reference_bounds.get(index).copied().flatten())
291 .map(|bounds| bounds.size);
292 let item_bounds_store = self.item_bounds.clone();
293 let entity = cx.entity().clone();
294
295 div()
296 .relative()
297 .size_full()
298 .when_some(self.height, |el, height| el.h(height))
299 .when(draggable, |el| {
300 let move_entity = entity.clone();
301 let up_entity = entity.clone();
302 let out_entity = entity.clone();
303 el.on_mouse_move(move |event, window, cx| {
304 move_entity.update(cx, |list, cx| list.update_drag_position(event, window, cx));
305 })
306 .on_mouse_up(MouseButton::Left, move |_, window, cx| {
307 up_entity.update(cx, |list, cx| list.finish_drag(0, window, cx));
308 })
309 .on_mouse_up_out(MouseButton::Left, move |_, window, cx| {
310 out_entity.update(cx, |list, cx| list.finish_drag(0, window, cx));
311 })
312 })
313 .child(
314 list(self.list_state.clone(), move |index, window, cx| {
315 let item_index = display_order.get(index).copied().unwrap_or(index);
316 let item = (render_item)(item_index, window, cx);
317 let is_dragging = active_item == Some(item_index);
318 let is_over = drag_state.is_over(index);
319 let item_entity = entity.clone();
320 let move_entity = entity.clone();
321 let up_entity = entity.clone();
322 let out_entity = entity.clone();
323 let mut shell = div()
324 .flex()
325 .flex_row()
326 .items_center()
327 .rounded_md()
328 .border_1()
329 .border_color(if is_dragging {
330 gpui::rgb(0xcbd5e1).into()
331 } else if is_over {
332 gpui::blue()
333 } else {
334 gpui::transparent_black()
335 })
336 .opacity(1.0)
337 .when(is_dragging, |s| s.shadow_lg());
338 if draggable && !is_dragging {
339 let up_entity = up_entity.clone();
340 let out_entity = out_entity.clone();
341 shell = shell
342 .on_mouse_move(move |event, window, cx| {
343 move_entity.update(cx, |list, cx| {
344 list.hover_drag(index, event, window, cx)
345 });
346 })
347 .on_mouse_up(MouseButton::Left, move |_, window, cx| {
348 up_entity
349 .update(cx, |list, cx| list.finish_drag(index, window, cx));
350 cx.stop_propagation();
351 })
352 .on_mouse_up_out(MouseButton::Left, move |_, window, cx| {
353 out_entity
354 .update(cx, |list, cx| list.finish_drag(index, window, cx));
355 })
356 .child(
357 drag_handle(gpui::rgb(0x94a3b8).into(), false, px(32.0))
358 .on_mouse_down(MouseButton::Left, move |event, _, cx| {
359 item_entity.update(cx, |list, cx| {
360 list.start_drag(index, event.position, cx)
361 });
362 cx.stop_propagation();
363 }),
364 )
365 .child(item);
366 } else if draggable {
367 shell = shell
368 .child(drag_handle(gpui::rgb(0x94a3b8).into(), true, px(32.0)))
369 .child(item);
370 } else {
371 shell = shell.child(item);
372 }
373
374 let row_content = if is_dragging {
375 let up_entity = up_entity.clone();
376 let out_entity = out_entity.clone();
377 let (drag_dx, drag_dy) = drag_state.offset_from_bounds(
378 DragAxis::Vertical,
379 drag_reference_bounds.get(index).copied().flatten(),
380 );
381 drag_placeholder(active_size)
382 .child(
383 deferred(
384 shell
385 .absolute()
386 .left(drag_dx)
387 .top(drag_dy)
388 .on_mouse_up(MouseButton::Left, move |_, window, cx| {
389 up_entity.update(cx, |list, cx| {
390 list.finish_drag(index, window, cx)
391 });
392 cx.stop_propagation();
393 })
394 .on_mouse_up_out(
395 MouseButton::Left,
396 move |_, window, cx| {
397 out_entity.update(cx, |list, cx| {
398 list.finish_drag(index, window, cx)
399 });
400 },
401 ),
402 )
403 .with_priority(1000),
404 )
405 .into_any_element()
406 } else {
407 shell.into_any_element()
408 };
409
410 let bounds_store = item_bounds_store.clone();
411 let row = div()
412 .child(row_content)
413 .on_children_prepainted(move |bounds, _, _| {
414 if drag_active {
415 return;
416 }
417 let Some(bounds) = bounds.into_iter().next() else {
418 return;
419 };
420 let mut item_bounds = bounds_store.borrow_mut();
421 if item_bounds.len() <= index {
422 item_bounds.resize(index + 1, None);
423 }
424 item_bounds[index] = Some(bounds);
425 })
426 .into_any_element();
427
428 if spacing > px(0.0) {
429 div().pb(spacing).child(row).into_any_element()
430 } else {
431 row
432 }
433 })
434 .size_full(),
435 )
436 .child(crate::VirtualScrollbar::new(self.list_state.clone()))
437 }
438}
439
440fn drag_placeholder(size: Option<Size<Pixels>>) -> gpui::Div {
441 div()
442 .relative()
443 .flex_none()
444 .when_some(size, |s, size| s.w(size.width).h(size.height))
445 .rounded_md()
446 .border_1()
447 .border_color(gpui::rgb(0xcbd5e1))
448 .bg(gpui::transparent_black())
449}
450
451#[cfg(test)]
452mod tests {
453 #[test]
454 fn virtualized_list_owns_list_state_and_uses_liora_scrollbar() {
455 let source = include_str!("virtualized_list.rs");
456
457 assert!(source.contains("pub struct VirtualizedList"));
458 assert!(source.contains("ListState::new"));
459 assert!(source.contains("list(self.list_state.clone()"));
460 assert!(source.contains("VirtualScrollbar::new"));
461 assert!(source.contains("set_item_spacing"));
462 assert!(source.contains("set_render_item"));
463 assert!(source.contains("measure_all_items_for_scrollbar"));
464 assert!(source.contains("set_draggable"));
465 assert!(source.contains("set_on_reorder"));
466 assert!(source.contains("drag_handle"));
467 assert!(source.contains("DragState"));
468 assert!(source.contains("display_order"));
469 assert!(source.contains("drag_placeholder"));
470 assert!(source.contains("on_children_prepainted"));
471 assert!(source.contains("drag_reference_bounds"));
472 }
473
474 #[test]
475 fn virtualized_list_resets_state_when_count_or_overdraw_changes() {
476 let source = include_str!("virtualized_list.rs");
477
478 assert!(source.contains("set_item_count"));
479 assert!(source.contains("set_overdraw"));
480 assert!(source.contains("Self::new_list_state"));
481 assert!(source.contains("pub fn remeasure(&self)"));
482 assert!(source.contains("pub fn remeasure_items"));
483 }
484}