1use crate::{
2 point, px, AnyElement, AvailableSpace, BorrowAppContext, Bounds, DispatchPhase, Element,
3 IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
4 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 /// Get the bounds for the given item in window coordinates.
158 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
159 let state = &*self.0.borrow();
160 let bounds = state.last_layout_bounds.unwrap_or_default();
161 let scroll_top = state.logical_scroll_top.unwrap_or_default();
162
163 if ix < scroll_top.item_ix {
164 return None;
165 }
166
167 let mut cursor = state.items.cursor::<(Count, Height)>();
168 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
169
170 let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
171
172 cursor.seek_forward(&Count(ix), Bias::Right, &());
173 if let Some(&ListItem::Rendered { height }) = cursor.item() {
174 let &(Count(count), Height(top)) = cursor.start();
175 if count == ix {
176 let top = bounds.top() + top - scroll_top;
177 return Some(Bounds::from_corners(
178 point(bounds.left(), top),
179 point(bounds.right(), top + height),
180 ));
181 }
182 }
183 None
184 }
185}
186
187impl StateInner {
188 fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
189 let mut cursor = self.items.cursor::<ListItemSummary>();
190 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
191 let start_y = cursor.start().height + scroll_top.offset_in_item;
192 cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
193 scroll_top.item_ix..cursor.start().count + 1
194 }
195
196 fn scroll(
197 &mut self,
198 scroll_top: &ListOffset,
199 height: Pixels,
200 delta: Point<Pixels>,
201 cx: &mut WindowContext,
202 ) {
203 let scroll_max = (self.items.summary().height - height).max(px(0.));
204 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
205 .max(px(0.))
206 .min(scroll_max);
207
208 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
209 self.logical_scroll_top = None;
210 } else {
211 let mut cursor = self.items.cursor::<ListItemSummary>();
212 cursor.seek(&Height(new_scroll_top), Bias::Right, &());
213 let item_ix = cursor.start().count;
214 let offset_in_item = new_scroll_top - cursor.start().height;
215 self.logical_scroll_top = Some(ListOffset {
216 item_ix,
217 offset_in_item,
218 });
219 }
220
221 if self.scroll_handler.is_some() {
222 let visible_range = self.visible_range(height, scroll_top);
223 self.scroll_handler.as_mut().unwrap()(
224 &ListScrollEvent {
225 visible_range,
226 count: self.items.summary().count,
227 },
228 cx,
229 );
230 }
231
232 cx.notify();
233 }
234
235 fn logical_scroll_top(&self) -> ListOffset {
236 self.logical_scroll_top
237 .unwrap_or_else(|| match self.alignment {
238 ListAlignment::Top => ListOffset {
239 item_ix: 0,
240 offset_in_item: px(0.),
241 },
242 ListAlignment::Bottom => ListOffset {
243 item_ix: self.items.summary().count,
244 offset_in_item: px(0.),
245 },
246 })
247 }
248
249 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
250 let mut cursor = self.items.cursor::<ListItemSummary>();
251 cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
252 cursor.start().height + logical_scroll_top.offset_in_item
253 }
254}
255
256impl std::fmt::Debug for ListItem {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 match self {
259 Self::Unrendered => write!(f, "Unrendered"),
260 Self::Rendered { height, .. } => {
261 f.debug_struct("Rendered").field("height", height).finish()
262 }
263 }
264 }
265}
266
267#[derive(Debug, Clone, Copy, Default)]
268pub struct ListOffset {
269 pub item_ix: usize,
270 pub offset_in_item: Pixels,
271}
272
273impl Element for List {
274 type State = ();
275
276 fn layout(
277 &mut self,
278 _state: Option<Self::State>,
279 cx: &mut crate::WindowContext,
280 ) -> (crate::LayoutId, Self::State) {
281 let mut style = Style::default();
282 style.refine(&self.style);
283 let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
284 cx.request_layout(&style, None)
285 });
286 (layout_id, ())
287 }
288
289 fn paint(
290 &mut self,
291 bounds: crate::Bounds<crate::Pixels>,
292 _state: &mut Self::State,
293 cx: &mut crate::WindowContext,
294 ) {
295 let state = &mut *self.state.0.borrow_mut();
296
297 // If the width of the list has changed, invalidate all cached item heights
298 if state.last_layout_bounds.map_or(true, |last_bounds| {
299 last_bounds.size.width != bounds.size.width
300 }) {
301 state.items = SumTree::from_iter(
302 (0..state.items.summary().count).map(|_| ListItem::Unrendered),
303 &(),
304 )
305 }
306
307 let old_items = state.items.clone();
308 let mut measured_items = VecDeque::new();
309 let mut item_elements = VecDeque::new();
310 let mut rendered_height = px(0.);
311 let mut scroll_top = state.logical_scroll_top();
312
313 let available_item_space = Size {
314 width: AvailableSpace::Definite(bounds.size.width),
315 height: AvailableSpace::MinContent,
316 };
317
318 // Render items after the scroll top, including those in the trailing overdraw
319 let mut cursor = old_items.cursor::<Count>();
320 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
321 for (ix, item) in cursor.by_ref().enumerate() {
322 let visible_height = rendered_height - scroll_top.offset_in_item;
323 if visible_height >= bounds.size.height + state.overdraw {
324 break;
325 }
326
327 // Use the previously cached height if available
328 let mut height = if let ListItem::Rendered { height } = item {
329 Some(*height)
330 } else {
331 None
332 };
333
334 // If we're within the visible area or the height wasn't cached, render and measure the item's element
335 if visible_height < bounds.size.height || height.is_none() {
336 let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
337 let element_size = element.measure(available_item_space, cx);
338 height = Some(element_size.height);
339 if visible_height < bounds.size.height {
340 item_elements.push_back(element);
341 }
342 }
343
344 let height = height.unwrap();
345 rendered_height += height;
346 measured_items.push_back(ListItem::Rendered { height });
347 }
348
349 // Prepare to start walking upward from the item at the scroll top.
350 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
351
352 // If the rendered items do not fill the visible region, then adjust
353 // the scroll top upward.
354 if rendered_height - scroll_top.offset_in_item < bounds.size.height {
355 while rendered_height < bounds.size.height {
356 cursor.prev(&());
357 if cursor.item().is_some() {
358 let mut element = (state.render_item)(cursor.start().0, cx);
359 let element_size = element.measure(available_item_space, cx);
360
361 rendered_height += element_size.height;
362 measured_items.push_front(ListItem::Rendered {
363 height: element_size.height,
364 });
365 item_elements.push_front(element)
366 } else {
367 break;
368 }
369 }
370
371 scroll_top = ListOffset {
372 item_ix: cursor.start().0,
373 offset_in_item: rendered_height - bounds.size.height,
374 };
375
376 match state.alignment {
377 ListAlignment::Top => {
378 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
379 state.logical_scroll_top = Some(scroll_top);
380 }
381 ListAlignment::Bottom => {
382 scroll_top = ListOffset {
383 item_ix: cursor.start().0,
384 offset_in_item: rendered_height - bounds.size.height,
385 };
386 state.logical_scroll_top = None;
387 }
388 };
389 }
390
391 // Measure items in the leading overdraw
392 let mut leading_overdraw = scroll_top.offset_in_item;
393 while leading_overdraw < state.overdraw {
394 cursor.prev(&());
395 if let Some(item) = cursor.item() {
396 let height = if let ListItem::Rendered { height } = item {
397 *height
398 } else {
399 let mut element = (state.render_item)(cursor.start().0, cx);
400 element.measure(available_item_space, cx).height
401 };
402
403 leading_overdraw += height;
404 measured_items.push_front(ListItem::Rendered { height });
405 } else {
406 break;
407 }
408 }
409
410 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
411 let mut cursor = old_items.cursor::<Count>();
412 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
413 new_items.extend(measured_items, &());
414 cursor.seek(&Count(measured_range.end), Bias::Right, &());
415 new_items.append(cursor.suffix(&()), &());
416
417 // Paint the visible items
418 let mut item_origin = bounds.origin;
419 item_origin.y -= scroll_top.offset_in_item;
420 for item_element in &mut item_elements {
421 let item_height = item_element.measure(available_item_space, cx).height;
422 item_element.draw(item_origin, available_item_space, cx);
423 item_origin.y += item_height;
424 }
425
426 state.items = new_items;
427 state.last_layout_bounds = Some(bounds);
428
429 let list_state = self.state.clone();
430 let height = bounds.size.height;
431 cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
432 if phase == DispatchPhase::Bubble {
433 list_state.0.borrow_mut().scroll(
434 &scroll_top,
435 height,
436 event.delta.pixel_delta(px(20.)),
437 cx,
438 )
439 }
440 });
441 }
442}
443
444impl IntoElement for List {
445 type Element = Self;
446
447 fn element_id(&self) -> Option<crate::ElementId> {
448 None
449 }
450
451 fn into_element(self) -> Self::Element {
452 self
453 }
454}
455
456impl Styled for List {
457 fn style(&mut self) -> &mut StyleRefinement {
458 &mut self.style
459 }
460}
461
462impl sum_tree::Item for ListItem {
463 type Summary = ListItemSummary;
464
465 fn summary(&self) -> Self::Summary {
466 match self {
467 ListItem::Unrendered => ListItemSummary {
468 count: 1,
469 rendered_count: 0,
470 unrendered_count: 1,
471 height: px(0.),
472 },
473 ListItem::Rendered { height } => ListItemSummary {
474 count: 1,
475 rendered_count: 1,
476 unrendered_count: 0,
477 height: *height,
478 },
479 }
480 }
481}
482
483impl sum_tree::Summary for ListItemSummary {
484 type Context = ();
485
486 fn add_summary(&mut self, summary: &Self, _: &()) {
487 self.count += summary.count;
488 self.rendered_count += summary.rendered_count;
489 self.unrendered_count += summary.unrendered_count;
490 self.height += summary.height;
491 }
492}
493
494impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
495 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
496 self.0 += summary.count;
497 }
498}
499
500impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
501 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
502 self.0 += summary.rendered_count;
503 }
504}
505
506impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
507 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
508 self.0 += summary.unrendered_count;
509 }
510}
511
512impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
513 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
514 self.0 += summary.height;
515 }
516}
517
518impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
519 fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
520 self.0.partial_cmp(&other.count).unwrap()
521 }
522}
523
524impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
525 fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
526 self.0.partial_cmp(&other.height).unwrap()
527 }
528}