1use crate::{
2 geometry::{
3 rect::RectF,
4 vector::{vec2f, Vector2F},
5 },
6 json::json,
7 sum_tree::{self, Bias, SumTree},
8 DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
9 RenderContext, SizeConstraint, View,
10};
11use std::{cell::RefCell, ops::Range, rc::Rc};
12
13pub struct List {
14 state: ListState,
15}
16
17#[derive(Clone)]
18pub struct ListState(Rc<RefCell<StateInner>>);
19
20#[derive(Eq, PartialEq)]
21pub enum Orientation {
22 Top,
23 Bottom,
24}
25
26struct StateInner {
27 last_layout_width: Option<f32>,
28 elements: Vec<Option<ElementRc>>,
29 heights: SumTree<ElementHeight>,
30 scroll_position: f32,
31 orientation: Orientation,
32 scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
33}
34
35#[derive(Clone, Debug)]
36enum ElementHeight {
37 Pending,
38 Ready(f32),
39}
40
41#[derive(Clone, Debug, Default, PartialEq)]
42struct ElementHeightSummary {
43 count: usize,
44 pending_count: usize,
45 height: f32,
46}
47
48#[derive(Clone, Debug, Default)]
49struct Count(usize);
50
51#[derive(Clone, Debug, Default)]
52struct PendingCount(usize);
53
54#[derive(Clone, Debug, Default)]
55struct Height(f32);
56
57impl List {
58 pub fn new<F, I, V>(state: ListState, cx: &RenderContext<V>, build_items: F) -> Self
59 where
60 F: Fn(Range<usize>) -> I,
61 I: IntoIterator<Item = ElementBox>,
62 V: View,
63 {
64 {
65 let state = &mut *state.0.borrow_mut();
66 if cx.refreshing {
67 let elements = (build_items)(0..state.elements.len());
68 state.last_layout_width = None;
69 state.elements.clear();
70 state
71 .elements
72 .extend(elements.into_iter().map(|e| Some(e.into())));
73 state.heights = SumTree::new();
74 state.heights.extend(
75 (0..state.elements.len()).map(|_| ElementHeight::Pending),
76 &(),
77 );
78 } else {
79 let mut cursor = state.heights.cursor::<PendingCount, Count>();
80 cursor.seek(&PendingCount(1), sum_tree::Bias::Left, &());
81
82 while cursor.item().is_some() {
83 let start_ix = cursor.sum_start().0;
84 while cursor.item().map_or(false, |h| h.is_pending()) {
85 cursor.next(&());
86 }
87 let end_ix = cursor.sum_start().0;
88 if end_ix > start_ix {
89 state.elements.splice(
90 start_ix..end_ix,
91 (build_items)(start_ix..end_ix)
92 .into_iter()
93 .map(|e| Some(e.into())),
94 );
95 }
96
97 cursor.seek(&PendingCount(cursor.seek_start().0 + 1), Bias::Left, &());
98 }
99 }
100 }
101
102 Self { state }
103 }
104}
105
106impl Element for List {
107 type LayoutState = Vec<ElementRc>;
108
109 type PaintState = ();
110
111 fn layout(
112 &mut self,
113 constraint: SizeConstraint,
114 cx: &mut LayoutContext,
115 ) -> (Vector2F, Self::LayoutState) {
116 let state = &mut *self.state.0.borrow_mut();
117 let mut item_constraint = constraint;
118 item_constraint.min.set_y(0.);
119 item_constraint.max.set_y(f32::INFINITY);
120
121 let size = constraint.max;
122
123 let visible_top = state.scroll_top(size.y());
124 let visible_bottom = visible_top + size.y();
125
126 if state.last_layout_width == Some(constraint.max.x()) {
127 let mut old_heights = state.heights.cursor::<PendingCount, ElementHeightSummary>();
128 let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &());
129
130 while let Some(height) = old_heights.item() {
131 if height.is_pending() {
132 let element = &mut state.elements[old_heights.sum_start().count];
133 let element_size = element.as_mut().unwrap().layout(item_constraint, cx);
134 new_heights.push(ElementHeight::Ready(element_size.y()), &());
135
136 // Adjust scroll position to keep visible elements stable
137 match state.orientation {
138 Orientation::Top => {
139 if new_heights.summary().height < visible_top {
140 state.scroll_position += element_size.y();
141 }
142 }
143 Orientation::Bottom => {
144 if new_heights.summary().height - element_size.y() > visible_bottom {
145 state.scroll_position += element_size.y();
146 }
147 }
148 }
149
150 old_heights.next(&());
151 } else {
152 new_heights.push_tree(
153 old_heights.slice(
154 &PendingCount(old_heights.sum_start().pending_count + 1),
155 Bias::Left,
156 &(),
157 ),
158 &(),
159 );
160 }
161 }
162
163 drop(old_heights);
164 state.heights = new_heights;
165 } else {
166 state.heights = SumTree::new();
167 for element in &mut state.elements {
168 let element = element.as_mut().unwrap();
169 let size = element.layout(item_constraint, cx);
170 state.heights.push(ElementHeight::Ready(size.y()), &());
171 }
172 state.last_layout_width = Some(constraint.max.x());
173 }
174
175 let visible_elements = state.elements[state.visible_range(size.y())]
176 .iter()
177 .map(|e| e.clone().unwrap())
178 .collect();
179 (size, visible_elements)
180 }
181
182 fn paint(
183 &mut self,
184 bounds: RectF,
185 visible_elements: &mut Self::LayoutState,
186 cx: &mut PaintContext,
187 ) {
188 cx.scene.push_layer(Some(bounds));
189 let state = &mut *self.state.0.borrow_mut();
190 let visible_range = state.visible_range(bounds.height());
191
192 let mut item_top = {
193 let mut cursor = state.heights.cursor::<Count, Height>();
194 cursor.seek(&Count(visible_range.start), Bias::Right, &());
195 cursor.sum_start().0
196 };
197 if state.orientation == Orientation::Bottom
198 && bounds.height() > state.heights.summary().height
199 {
200 item_top += bounds.height() - state.heights.summary().height;
201 }
202 let scroll_top = state.scroll_top(bounds.height());
203
204 for element in visible_elements {
205 let origin = bounds.origin() + vec2f(0., item_top - scroll_top);
206 element.paint(origin, cx);
207 item_top += element.size().y();
208 }
209 cx.scene.pop_layer();
210 }
211
212 fn dispatch_event(
213 &mut self,
214 event: &Event,
215 bounds: RectF,
216 visible_elements: &mut Self::LayoutState,
217 _: &mut (),
218 cx: &mut EventContext,
219 ) -> bool {
220 let mut handled = false;
221
222 let mut state = self.state.0.borrow_mut();
223 for element in visible_elements {
224 handled = element.dispatch_event(event, cx) || handled;
225 }
226
227 match event {
228 Event::ScrollWheel {
229 position,
230 delta,
231 precise,
232 } => {
233 if bounds.contains_point(*position) {
234 if state.scroll(*position, *delta, *precise, bounds.height(), cx) {
235 handled = true;
236 }
237 }
238 }
239 _ => {}
240 }
241
242 handled
243 }
244
245 fn debug(
246 &self,
247 bounds: RectF,
248 visible_elements: &Self::LayoutState,
249 _: &(),
250 cx: &DebugContext,
251 ) -> serde_json::Value {
252 let state = self.state.0.borrow_mut();
253 let visible_range = state.visible_range(bounds.height());
254 let visible_elements = visible_elements
255 .iter()
256 .map(|e| e.debug(cx))
257 .collect::<Vec<_>>();
258 json!({
259 "visible_range": visible_range,
260 "visible_elements": visible_elements,
261 "scroll_position": state.scroll_position,
262 })
263 }
264}
265
266impl ListState {
267 pub fn new(element_count: usize, orientation: Orientation) -> Self {
268 let mut heights = SumTree::new();
269 heights.extend((0..element_count).map(|_| ElementHeight::Pending), &());
270 Self(Rc::new(RefCell::new(StateInner {
271 last_layout_width: None,
272 elements: (0..element_count).map(|_| None).collect(),
273 heights,
274 scroll_position: 0.,
275 orientation,
276 scroll_handler: None,
277 })))
278 }
279
280 pub fn splice(&self, old_range: Range<usize>, count: usize) {
281 let state = &mut *self.0.borrow_mut();
282
283 let mut old_heights = state.heights.cursor::<Count, ()>();
284 let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
285 old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
286
287 let old_elements = state.elements.splice(old_range, (0..count).map(|_| None));
288 drop(old_elements);
289
290 new_heights.extend((0..count).map(|_| ElementHeight::Pending), &());
291 new_heights.push_tree(old_heights.suffix(&()), &());
292 drop(old_heights);
293 state.heights = new_heights;
294 }
295
296 pub fn set_scroll_handler(
297 &mut self,
298 handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
299 ) {
300 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
301 }
302}
303
304impl StateInner {
305 fn visible_range(&self, height: f32) -> Range<usize> {
306 let mut cursor = self.heights.cursor::<Height, Count>();
307 cursor.seek(&Height(self.scroll_top(height)), Bias::Right, &());
308 let start_ix = cursor.sum_start().0;
309 cursor.seek(&Height(self.scroll_top(height) + height), Bias::Left, &());
310 let end_ix = cursor.sum_start().0;
311 start_ix..self.elements.len().min(end_ix + 1)
312 }
313
314 fn scroll(
315 &mut self,
316 _: Vector2F,
317 delta: Vector2F,
318 precise: bool,
319 height: f32,
320 cx: &mut EventContext,
321 ) -> bool {
322 if !precise {
323 todo!("still need to handle non-precise scroll events from a mouse wheel");
324 }
325
326 let scroll_max = (self.heights.summary().height - height).max(0.);
327 let delta_y = match self.orientation {
328 Orientation::Top => -delta.y(),
329 Orientation::Bottom => delta.y(),
330 };
331 self.scroll_position = (self.scroll_position + delta_y).max(0.).min(scroll_max);
332
333 if self.scroll_handler.is_some() {
334 let range = self.visible_range(height);
335 self.scroll_handler.as_mut().unwrap()(range, cx);
336 }
337 cx.notify();
338
339 true
340 }
341
342 fn scroll_top(&self, height: f32) -> f32 {
343 match self.orientation {
344 Orientation::Top => self.scroll_position,
345 Orientation::Bottom => {
346 (self.heights.summary().height - height - self.scroll_position).max(0.)
347 }
348 }
349 }
350}
351
352impl ElementHeight {
353 fn is_pending(&self) -> bool {
354 matches!(self, ElementHeight::Pending)
355 }
356}
357
358impl sum_tree::Item for ElementHeight {
359 type Summary = ElementHeightSummary;
360
361 fn summary(&self) -> Self::Summary {
362 match self {
363 ElementHeight::Pending => ElementHeightSummary {
364 count: 1,
365 pending_count: 1,
366 height: 0.,
367 },
368 ElementHeight::Ready(height) => ElementHeightSummary {
369 count: 1,
370 pending_count: 0,
371 height: *height,
372 },
373 }
374 }
375}
376
377impl sum_tree::Summary for ElementHeightSummary {
378 type Context = ();
379
380 fn add_summary(&mut self, summary: &Self, _: &()) {
381 self.count += summary.count;
382 self.pending_count += summary.pending_count;
383 self.height += summary.height;
384 }
385}
386
387impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for ElementHeightSummary {
388 fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
389 sum_tree::Summary::add_summary(self, summary, &());
390 }
391}
392
393impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Count {
394 fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
395 self.0 += summary.count;
396 }
397}
398
399impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Count {
400 fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
401 self.0.cmp(&other.0)
402 }
403}
404
405impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for PendingCount {
406 fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
407 self.0 += summary.pending_count;
408 }
409}
410
411impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for PendingCount {
412 fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
413 self.0.cmp(&other.0)
414 }
415}
416
417impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Height {
418 fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
419 self.0 += summary.height;
420 }
421}
422
423impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Height {
424 fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
425 self.0.partial_cmp(&other.0).unwrap()
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use crate::{elements::*, geometry::vector::vec2f, Entity};
433
434 #[crate::test(self)]
435 fn test_layout(cx: &mut crate::MutableAppContext) {
436 let mut presenter = cx.build_presenter(0, 0.);
437
438 let mut elements = vec![20., 30., 10.];
439 let state = ListState::new(elements.len(), Orientation::Top);
440
441 let mut list = List::new(
442 state.clone(),
443 &cx.build_render_context::<TestView>(0, 0, 0., false),
444 |range| elements[range].iter().copied().map(item),
445 )
446 .boxed();
447 let size = list.layout(
448 SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
449 &mut presenter.build_layout_context(cx),
450 );
451 assert_eq!(size, vec2f(100., 40.));
452 assert_eq!(
453 state.0.borrow().heights.summary(),
454 ElementHeightSummary {
455 count: 3,
456 pending_count: 0,
457 height: 60.
458 }
459 );
460
461 elements.splice(1..2, vec![40., 50.]);
462 elements.push(60.);
463 state.splice(1..2, 2);
464 state.splice(4..4, 1);
465 assert_eq!(
466 state.0.borrow().heights.summary(),
467 ElementHeightSummary {
468 count: 5,
469 pending_count: 3,
470 height: 30.
471 }
472 );
473
474 let mut list = List::new(
475 state.clone(),
476 &cx.build_render_context::<TestView>(0, 0, 0., false),
477 |range| elements[range].iter().copied().map(item),
478 )
479 .boxed();
480 let size = list.layout(
481 SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
482 &mut presenter.build_layout_context(cx),
483 );
484 assert_eq!(size, vec2f(100., 40.));
485 assert_eq!(
486 state.0.borrow().heights.summary(),
487 ElementHeightSummary {
488 count: 5,
489 pending_count: 0,
490 height: 180.
491 }
492 );
493 }
494
495 fn item(height: f32) -> ElementBox {
496 ConstrainedBox::new(Empty::new().boxed())
497 .with_height(height)
498 .with_width(100.)
499 .boxed()
500 }
501
502 struct TestView;
503
504 impl Entity for TestView {
505 type Event = ();
506 }
507
508 impl View for TestView {
509 fn ui_name() -> &'static str {
510 "TestView"
511 }
512
513 fn render(&self, _: &mut RenderContext<'_, Self>) -> ElementBox {
514 unimplemented!()
515 }
516 }
517}