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