1use crate::{
2 point, px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
3 DispatchPhase, Element, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style,
4 StyleRefinement, Styled, WindowContext,
5};
6use collections::VecDeque;
7use refineable::Refineable as _;
8use std::{cell::RefCell, ops::Range, rc::Rc};
9use sum_tree::{Bias, SumTree};
10
11pub fn list(state: ListState) -> List {
12 List {
13 state,
14 style: StyleRefinement::default(),
15 }
16}
17
18pub struct List {
19 state: ListState,
20 style: StyleRefinement,
21}
22
23#[derive(Clone)]
24pub struct ListState(Rc<RefCell<StateInner>>);
25
26struct StateInner {
27 last_layout_bounds: Option<Bounds<Pixels>>,
28 render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
29 items: SumTree<ListItem>,
30 logical_scroll_top: Option<ListOffset>,
31 alignment: ListAlignment,
32 overdraw: Pixels,
33 #[allow(clippy::type_complexity)]
34 scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum ListAlignment {
39 Top,
40 Bottom,
41}
42
43pub struct ListScrollEvent {
44 pub visible_range: Range<usize>,
45 pub count: usize,
46}
47
48#[derive(Clone)]
49enum ListItem {
50 Unrendered,
51 Rendered { height: Pixels },
52}
53
54#[derive(Clone, Debug, Default, PartialEq)]
55struct ListItemSummary {
56 count: usize,
57 rendered_count: usize,
58 unrendered_count: usize,
59 height: Pixels,
60}
61
62#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
63struct Count(usize);
64
65#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
66struct RenderedCount(usize);
67
68#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
69struct UnrenderedCount(usize);
70
71#[derive(Clone, Debug, Default)]
72struct Height(Pixels);
73
74impl ListState {
75 pub fn new<F>(
76 element_count: usize,
77 orientation: ListAlignment,
78 overdraw: Pixels,
79 render_item: F,
80 ) -> Self
81 where
82 F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
83 {
84 let mut items = SumTree::new();
85 items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
86 Self(Rc::new(RefCell::new(StateInner {
87 last_layout_bounds: None,
88 render_item: Box::new(render_item),
89 items,
90 logical_scroll_top: None,
91 alignment: orientation,
92 overdraw,
93 scroll_handler: None,
94 })))
95 }
96
97 pub fn reset(&self, element_count: usize) {
98 let state = &mut *self.0.borrow_mut();
99 state.logical_scroll_top = None;
100 state.items = SumTree::new();
101 state
102 .items
103 .extend((0..element_count).map(|_| ListItem::Unrendered), &());
104 }
105
106 pub fn item_count(&self) -> usize {
107 self.0.borrow().items.summary().count
108 }
109
110 pub fn splice(&self, old_range: Range<usize>, count: usize) {
111 let state = &mut *self.0.borrow_mut();
112
113 if let Some(ListOffset {
114 item_ix,
115 offset_in_item,
116 }) = state.logical_scroll_top.as_mut()
117 {
118 if old_range.contains(item_ix) {
119 *item_ix = old_range.start;
120 *offset_in_item = px(0.);
121 } else if old_range.end <= *item_ix {
122 *item_ix = *item_ix - (old_range.end - old_range.start) + count;
123 }
124 }
125
126 let mut old_heights = state.items.cursor::<Count>();
127 let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
128 old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
129
130 new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
131 new_heights.append(old_heights.suffix(&()), &());
132 drop(old_heights);
133 state.items = new_heights;
134 }
135
136 pub fn set_scroll_handler(
137 &self,
138 handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
139 ) {
140 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
141 }
142
143 pub fn logical_scroll_top(&self) -> ListOffset {
144 self.0.borrow().logical_scroll_top()
145 }
146
147 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
148 let state = &mut *self.0.borrow_mut();
149 let item_count = state.items.summary().count;
150 if scroll_top.item_ix >= item_count {
151 scroll_top.item_ix = item_count;
152 scroll_top.offset_in_item = px(0.);
153 }
154 state.logical_scroll_top = Some(scroll_top);
155 }
156
157 pub fn scroll_to_reveal_item(&self, ix: usize) {
158 let state = &mut *self.0.borrow_mut();
159 let mut scroll_top = state.logical_scroll_top();
160 let height = state
161 .last_layout_bounds
162 .map_or(px(0.), |bounds| bounds.size.height);
163
164 if ix <= scroll_top.item_ix {
165 scroll_top.item_ix = ix;
166 scroll_top.offset_in_item = px(0.);
167 } else {
168 let mut cursor = state.items.cursor::<ListItemSummary>();
169 cursor.seek(&Count(ix + 1), Bias::Right, &());
170 let bottom = cursor.start().height;
171 let goal_top = px(0.).max(bottom - height);
172
173 cursor.seek(&Height(goal_top), Bias::Left, &());
174 let start_ix = cursor.start().count;
175 let start_item_top = cursor.start().height;
176
177 if start_ix >= scroll_top.item_ix {
178 scroll_top.item_ix = start_ix;
179 scroll_top.offset_in_item = goal_top - start_item_top;
180 }
181 }
182
183 state.logical_scroll_top = Some(scroll_top);
184 }
185
186 /// Get the bounds for the given item in window coordinates.
187 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
188 let state = &*self.0.borrow();
189 let bounds = state.last_layout_bounds.unwrap_or_default();
190 let scroll_top = state.logical_scroll_top();
191
192 if ix < scroll_top.item_ix {
193 return None;
194 }
195
196 let mut cursor = state.items.cursor::<(Count, Height)>();
197 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
198
199 let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
200
201 cursor.seek_forward(&Count(ix), Bias::Right, &());
202 if let Some(&ListItem::Rendered { height }) = cursor.item() {
203 let &(Count(count), Height(top)) = cursor.start();
204 if count == ix {
205 let top = bounds.top() + top - scroll_top;
206 return Some(Bounds::from_corners(
207 point(bounds.left(), top),
208 point(bounds.right(), top + height),
209 ));
210 }
211 }
212 None
213 }
214}
215
216impl StateInner {
217 fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
218 let mut cursor = self.items.cursor::<ListItemSummary>();
219 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
220 let start_y = cursor.start().height + scroll_top.offset_in_item;
221 cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
222 scroll_top.item_ix..cursor.start().count + 1
223 }
224
225 fn scroll(
226 &mut self,
227 scroll_top: &ListOffset,
228 height: Pixels,
229 delta: Point<Pixels>,
230 cx: &mut WindowContext,
231 ) {
232 let scroll_max = (self.items.summary().height - height).max(px(0.));
233 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
234 .max(px(0.))
235 .min(scroll_max);
236
237 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
238 self.logical_scroll_top = None;
239 } else {
240 let mut cursor = self.items.cursor::<ListItemSummary>();
241 cursor.seek(&Height(new_scroll_top), Bias::Right, &());
242 let item_ix = cursor.start().count;
243 let offset_in_item = new_scroll_top - cursor.start().height;
244 self.logical_scroll_top = Some(ListOffset {
245 item_ix,
246 offset_in_item,
247 });
248 }
249
250 if self.scroll_handler.is_some() {
251 let visible_range = self.visible_range(height, scroll_top);
252 self.scroll_handler.as_mut().unwrap()(
253 &ListScrollEvent {
254 visible_range,
255 count: self.items.summary().count,
256 },
257 cx,
258 );
259 }
260
261 cx.notify();
262 }
263
264 fn logical_scroll_top(&self) -> ListOffset {
265 self.logical_scroll_top
266 .unwrap_or_else(|| match self.alignment {
267 ListAlignment::Top => ListOffset {
268 item_ix: 0,
269 offset_in_item: px(0.),
270 },
271 ListAlignment::Bottom => ListOffset {
272 item_ix: self.items.summary().count,
273 offset_in_item: px(0.),
274 },
275 })
276 }
277
278 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
279 let mut cursor = self.items.cursor::<ListItemSummary>();
280 cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
281 cursor.start().height + logical_scroll_top.offset_in_item
282 }
283}
284
285impl std::fmt::Debug for ListItem {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 match self {
288 Self::Unrendered => write!(f, "Unrendered"),
289 Self::Rendered { height, .. } => {
290 f.debug_struct("Rendered").field("height", height).finish()
291 }
292 }
293 }
294}
295
296#[derive(Debug, Clone, Copy, Default)]
297pub struct ListOffset {
298 pub item_ix: usize,
299 pub offset_in_item: Pixels,
300}
301
302impl Element for List {
303 type State = ();
304
305 fn request_layout(
306 &mut self,
307 _state: Option<Self::State>,
308 cx: &mut crate::WindowContext,
309 ) -> (crate::LayoutId, Self::State) {
310 let mut style = Style::default();
311 style.refine(&self.style);
312 let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
313 cx.request_layout(&style, None)
314 });
315 (layout_id, ())
316 }
317
318 fn paint(
319 &mut self,
320 bounds: Bounds<crate::Pixels>,
321 _state: &mut Self::State,
322 cx: &mut crate::WindowContext,
323 ) {
324 let state = &mut *self.state.0.borrow_mut();
325
326 // If the width of the list has changed, invalidate all cached item heights
327 if state.last_layout_bounds.map_or(true, |last_bounds| {
328 last_bounds.size.width != bounds.size.width
329 }) {
330 state.items = SumTree::from_iter(
331 (0..state.items.summary().count).map(|_| ListItem::Unrendered),
332 &(),
333 )
334 }
335
336 let old_items = state.items.clone();
337 let mut measured_items = VecDeque::new();
338 let mut item_elements = VecDeque::new();
339 let mut rendered_height = px(0.);
340 let mut scroll_top = state.logical_scroll_top();
341
342 let available_item_space = Size {
343 width: AvailableSpace::Definite(bounds.size.width),
344 height: AvailableSpace::MinContent,
345 };
346
347 // Render items after the scroll top, including those in the trailing overdraw
348 let mut cursor = old_items.cursor::<Count>();
349 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
350 for (ix, item) in cursor.by_ref().enumerate() {
351 let visible_height = rendered_height - scroll_top.offset_in_item;
352 if visible_height >= bounds.size.height + state.overdraw {
353 break;
354 }
355
356 // Use the previously cached height if available
357 let mut height = if let ListItem::Rendered { height } = item {
358 Some(*height)
359 } else {
360 None
361 };
362
363 // If we're within the visible area or the height wasn't cached, render and measure the item's element
364 if visible_height < bounds.size.height || height.is_none() {
365 let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
366 let element_size = element.measure(available_item_space, cx);
367 height = Some(element_size.height);
368 if visible_height < bounds.size.height {
369 item_elements.push_back(element);
370 }
371 }
372
373 let height = height.unwrap();
374 rendered_height += height;
375 measured_items.push_back(ListItem::Rendered { height });
376 }
377
378 // Prepare to start walking upward from the item at the scroll top.
379 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
380
381 // If the rendered items do not fill the visible region, then adjust
382 // the scroll top upward.
383 if rendered_height - scroll_top.offset_in_item < bounds.size.height {
384 while rendered_height < bounds.size.height {
385 cursor.prev(&());
386 if cursor.item().is_some() {
387 let mut element = (state.render_item)(cursor.start().0, cx);
388 let element_size = element.measure(available_item_space, cx);
389
390 rendered_height += element_size.height;
391 measured_items.push_front(ListItem::Rendered {
392 height: element_size.height,
393 });
394 item_elements.push_front(element)
395 } else {
396 break;
397 }
398 }
399
400 scroll_top = ListOffset {
401 item_ix: cursor.start().0,
402 offset_in_item: rendered_height - bounds.size.height,
403 };
404
405 match state.alignment {
406 ListAlignment::Top => {
407 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
408 state.logical_scroll_top = Some(scroll_top);
409 }
410 ListAlignment::Bottom => {
411 scroll_top = ListOffset {
412 item_ix: cursor.start().0,
413 offset_in_item: rendered_height - bounds.size.height,
414 };
415 state.logical_scroll_top = None;
416 }
417 };
418 }
419
420 // Measure items in the leading overdraw
421 let mut leading_overdraw = scroll_top.offset_in_item;
422 while leading_overdraw < state.overdraw {
423 cursor.prev(&());
424 if let Some(item) = cursor.item() {
425 let height = if let ListItem::Rendered { height } = item {
426 *height
427 } else {
428 let mut element = (state.render_item)(cursor.start().0, cx);
429 element.measure(available_item_space, cx).height
430 };
431
432 leading_overdraw += height;
433 measured_items.push_front(ListItem::Rendered { height });
434 } else {
435 break;
436 }
437 }
438
439 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
440 let mut cursor = old_items.cursor::<Count>();
441 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
442 new_items.extend(measured_items, &());
443 cursor.seek(&Count(measured_range.end), Bias::Right, &());
444 new_items.append(cursor.suffix(&()), &());
445
446 // Paint the visible items
447 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
448 let mut item_origin = bounds.origin;
449 item_origin.y -= scroll_top.offset_in_item;
450 for item_element in &mut item_elements {
451 let item_height = item_element.measure(available_item_space, cx).height;
452 item_element.draw(item_origin, available_item_space, cx);
453 item_origin.y += item_height;
454 }
455 });
456
457 state.items = new_items;
458 state.last_layout_bounds = Some(bounds);
459
460 let list_state = self.state.clone();
461 let height = bounds.size.height;
462 cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
463 if phase == DispatchPhase::Bubble
464 && bounds.contains(&event.position)
465 && cx.was_top_layer(&event.position, cx.stacking_order())
466 {
467 list_state.0.borrow_mut().scroll(
468 &scroll_top,
469 height,
470 event.delta.pixel_delta(px(20.)),
471 cx,
472 )
473 }
474 });
475 }
476}
477
478impl IntoElement for List {
479 type Element = Self;
480
481 fn element_id(&self) -> Option<crate::ElementId> {
482 None
483 }
484
485 fn into_element(self) -> Self::Element {
486 self
487 }
488}
489
490impl Styled for List {
491 fn style(&mut self) -> &mut StyleRefinement {
492 &mut self.style
493 }
494}
495
496impl sum_tree::Item for ListItem {
497 type Summary = ListItemSummary;
498
499 fn summary(&self) -> Self::Summary {
500 match self {
501 ListItem::Unrendered => ListItemSummary {
502 count: 1,
503 rendered_count: 0,
504 unrendered_count: 1,
505 height: px(0.),
506 },
507 ListItem::Rendered { height } => ListItemSummary {
508 count: 1,
509 rendered_count: 1,
510 unrendered_count: 0,
511 height: *height,
512 },
513 }
514 }
515}
516
517impl sum_tree::Summary for ListItemSummary {
518 type Context = ();
519
520 fn add_summary(&mut self, summary: &Self, _: &()) {
521 self.count += summary.count;
522 self.rendered_count += summary.rendered_count;
523 self.unrendered_count += summary.unrendered_count;
524 self.height += summary.height;
525 }
526}
527
528impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
529 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
530 self.0 += summary.count;
531 }
532}
533
534impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
535 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
536 self.0 += summary.rendered_count;
537 }
538}
539
540impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
541 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
542 self.0 += summary.unrendered_count;
543 }
544}
545
546impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
547 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
548 self.0 += summary.height;
549 }
550}
551
552impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
553 fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
554 self.0.partial_cmp(&other.count).unwrap()
555 }
556}
557
558impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
559 fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
560 self.0.partial_cmp(&other.height).unwrap()
561 }
562}