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