1use super::{Element, EventContext, LayoutContext, PaintContext, SizeConstraint};
2use crate::{
3 geometry::{
4 rect::RectF,
5 vector::{vec2f, Vector2F},
6 },
7 json::{self, json},
8 presenter::MeasurementContext,
9 scene::MouseScrollWheel,
10 ElementBox, MouseRegion, RenderContext, ScrollWheelEvent, View,
11};
12use json::ToJson;
13use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
14
15#[derive(Clone, Default)]
16pub struct UniformListState(Rc<RefCell<StateInner>>);
17
18#[derive(Debug)]
19pub enum ScrollTarget {
20 Show(usize),
21 Center(usize),
22}
23
24impl UniformListState {
25 pub fn scroll_to(&self, scroll_to: ScrollTarget) {
26 self.0.borrow_mut().scroll_to = Some(scroll_to);
27 }
28
29 pub fn scroll_top(&self) -> f32 {
30 self.0.borrow().scroll_top
31 }
32}
33
34#[derive(Default)]
35struct StateInner {
36 scroll_top: f32,
37 scroll_to: Option<ScrollTarget>,
38}
39
40pub struct LayoutState {
41 scroll_max: f32,
42 item_height: f32,
43 items: Vec<ElementBox>,
44}
45
46pub struct UniformList {
47 state: UniformListState,
48 item_count: usize,
49 #[allow(clippy::type_complexity)]
50 append_items: Box<dyn Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext)>,
51 padding_top: f32,
52 padding_bottom: f32,
53 get_width_from_item: Option<usize>,
54 view_id: usize,
55}
56
57impl UniformList {
58 pub fn new<F, V>(
59 state: UniformListState,
60 item_count: usize,
61 cx: &mut RenderContext<V>,
62 append_items: F,
63 ) -> Self
64 where
65 V: View,
66 F: 'static + Fn(&mut V, Range<usize>, &mut Vec<ElementBox>, &mut RenderContext<V>),
67 {
68 let handle = cx.handle();
69 Self {
70 state,
71 item_count,
72 append_items: Box::new(move |range, items, cx| {
73 if let Some(handle) = handle.upgrade(cx) {
74 cx.render(&handle, |view, cx| {
75 append_items(view, range, items, cx);
76 });
77 }
78 }),
79 padding_top: 0.,
80 padding_bottom: 0.,
81 get_width_from_item: None,
82 view_id: cx.handle().id(),
83 }
84 }
85
86 pub fn with_width_from_item(mut self, item_ix: Option<usize>) -> Self {
87 self.get_width_from_item = item_ix;
88 self
89 }
90
91 pub fn with_padding_top(mut self, padding: f32) -> Self {
92 self.padding_top = padding;
93 self
94 }
95
96 pub fn with_padding_bottom(mut self, padding: f32) -> Self {
97 self.padding_bottom = padding;
98 self
99 }
100
101 fn scroll(
102 state: UniformListState,
103 _: Vector2F,
104 mut delta: Vector2F,
105 precise: bool,
106 scroll_max: f32,
107 cx: &mut EventContext,
108 ) -> bool {
109 if !precise {
110 delta *= 20.;
111 }
112
113 let mut state = state.0.borrow_mut();
114 state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
115 cx.notify();
116
117 true
118 }
119
120 fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
121 let mut state = self.state.0.borrow_mut();
122
123 if let Some(scroll_to) = state.scroll_to.take() {
124 let item_ix;
125 let center;
126 match scroll_to {
127 ScrollTarget::Show(ix) => {
128 item_ix = ix;
129 center = false;
130 }
131 ScrollTarget::Center(ix) => {
132 item_ix = ix;
133 center = true;
134 }
135 }
136
137 let item_top = self.padding_top + item_ix as f32 * item_height;
138 let item_bottom = item_top + item_height;
139 if center {
140 let item_center = item_top + item_height / 2.;
141 state.scroll_top = (item_center - list_height / 2.).max(0.);
142 } else {
143 let scroll_bottom = state.scroll_top + list_height;
144 if item_top < state.scroll_top {
145 state.scroll_top = item_top;
146 } else if item_bottom > scroll_bottom {
147 state.scroll_top = item_bottom - list_height;
148 }
149 }
150 }
151
152 if state.scroll_top > scroll_max {
153 state.scroll_top = scroll_max;
154 }
155 }
156
157 fn scroll_top(&self) -> f32 {
158 self.state.0.borrow().scroll_top
159 }
160}
161
162impl Element for UniformList {
163 type LayoutState = LayoutState;
164 type PaintState = ();
165
166 fn layout(
167 &mut self,
168 constraint: SizeConstraint,
169 cx: &mut LayoutContext,
170 ) -> (Vector2F, Self::LayoutState) {
171 if constraint.max.y().is_infinite() {
172 unimplemented!(
173 "UniformList does not support being rendered with an unconstrained height"
174 );
175 }
176
177 let no_items = (
178 constraint.min,
179 LayoutState {
180 item_height: 0.,
181 scroll_max: 0.,
182 items: Default::default(),
183 },
184 );
185
186 if self.item_count == 0 {
187 return no_items;
188 }
189
190 let mut items = Vec::new();
191 let mut size = constraint.max;
192 let mut item_size;
193 let sample_item_ix;
194 let sample_item;
195 if let Some(sample_ix) = self.get_width_from_item {
196 (self.append_items)(sample_ix..sample_ix + 1, &mut items, cx);
197 sample_item_ix = sample_ix;
198
199 if let Some(mut item) = items.pop() {
200 item_size = item.layout(constraint, cx);
201 size.set_x(item_size.x());
202 sample_item = item;
203 } else {
204 return no_items;
205 }
206 } else {
207 (self.append_items)(0..1, &mut items, cx);
208 sample_item_ix = 0;
209 if let Some(mut item) = items.pop() {
210 item_size = item.layout(
211 SizeConstraint::new(
212 vec2f(constraint.max.x(), 0.0),
213 vec2f(constraint.max.x(), f32::INFINITY),
214 ),
215 cx,
216 );
217 item_size.set_x(size.x());
218 sample_item = item
219 } else {
220 return no_items;
221 }
222 }
223
224 let item_constraint = SizeConstraint {
225 min: item_size,
226 max: vec2f(constraint.max.x(), item_size.y()),
227 };
228 let item_height = item_size.y();
229
230 let scroll_height = self.item_count as f32 * item_height;
231 if scroll_height < size.y() {
232 size.set_y(size.y().min(scroll_height).max(constraint.min.y()));
233 }
234
235 let scroll_height =
236 item_height * self.item_count as f32 + self.padding_top + self.padding_bottom;
237 let scroll_max = (scroll_height - size.y()).max(0.);
238 self.autoscroll(scroll_max, size.y(), item_height);
239
240 let start = cmp::min(
241 ((self.scroll_top() - self.padding_top) / item_height.max(1.)) as usize,
242 self.item_count,
243 );
244 let end = cmp::min(
245 self.item_count,
246 start + (size.y() / item_height.max(1.)).ceil() as usize + 1,
247 );
248
249 if (start..end).contains(&sample_item_ix) {
250 if sample_item_ix > start {
251 (self.append_items)(start..sample_item_ix, &mut items, cx);
252 }
253
254 items.push(sample_item);
255
256 if sample_item_ix < end {
257 (self.append_items)(sample_item_ix + 1..end, &mut items, cx);
258 }
259 } else {
260 (self.append_items)(start..end, &mut items, cx);
261 }
262
263 for item in &mut items {
264 let item_size = item.layout(item_constraint, cx);
265 if item_size.x() > size.x() {
266 size.set_x(item_size.x());
267 }
268 }
269
270 (
271 size,
272 LayoutState {
273 item_height,
274 scroll_max,
275 items,
276 },
277 )
278 }
279
280 fn paint(
281 &mut self,
282 bounds: RectF,
283 visible_bounds: RectF,
284 layout: &mut Self::LayoutState,
285 cx: &mut PaintContext,
286 ) -> Self::PaintState {
287 let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
288
289 cx.scene.push_layer(Some(visible_bounds));
290
291 cx.scene.push_mouse_region(
292 MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
293 let scroll_max = layout.scroll_max;
294 let state = self.state.clone();
295 move |MouseScrollWheel {
296 platform_event:
297 ScrollWheelEvent {
298 position, delta, ..
299 },
300 ..
301 },
302 cx| {
303 if !Self::scroll(
304 state.clone(),
305 position,
306 *delta.raw(),
307 delta.precise(),
308 scroll_max,
309 cx,
310 ) {
311 cx.propagate_event();
312 }
313 }
314 }),
315 );
316
317 let mut item_origin = bounds.origin()
318 - vec2f(
319 0.,
320 (self.state.scroll_top() - self.padding_top) % layout.item_height,
321 );
322
323 for item in &mut layout.items {
324 item.paint(item_origin, visible_bounds, cx);
325 item_origin += vec2f(0.0, layout.item_height);
326 }
327
328 cx.scene.pop_layer();
329 }
330
331 fn rect_for_text_range(
332 &self,
333 range: Range<usize>,
334 _: RectF,
335 _: RectF,
336 layout: &Self::LayoutState,
337 _: &Self::PaintState,
338 cx: &MeasurementContext,
339 ) -> Option<RectF> {
340 layout
341 .items
342 .iter()
343 .find_map(|child| child.rect_for_text_range(range.clone(), cx))
344 }
345
346 fn debug(
347 &self,
348 bounds: RectF,
349 layout: &Self::LayoutState,
350 _: &Self::PaintState,
351 cx: &crate::DebugContext,
352 ) -> json::Value {
353 json!({
354 "type": "UniformList",
355 "bounds": bounds.to_json(),
356 "scroll_max": layout.scroll_max,
357 "item_height": layout.item_height,
358 "items": layout.items.iter().map(|item| item.debug(cx)).collect::<Vec<json::Value>>()
359
360 })
361 }
362}