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