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 append_items: Box<dyn Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext)>,
49 padding_top: f32,
50 padding_bottom: f32,
51 get_width_from_item: Option<usize>,
52}
53
54impl UniformList {
55 pub fn new<F, V>(
56 state: UniformListState,
57 item_count: usize,
58 cx: &mut RenderContext<V>,
59 append_items: F,
60 ) -> Self
61 where
62 V: View,
63 F: 'static + Fn(&mut V, Range<usize>, &mut Vec<ElementBox>, &mut RenderContext<V>),
64 {
65 let handle = cx.handle();
66 Self {
67 state,
68 item_count,
69 append_items: Box::new(move |range, items, cx| {
70 if let Some(handle) = handle.upgrade(cx) {
71 cx.render(&handle, |view, cx| {
72 append_items(view, range, items, cx);
73 });
74 }
75 }),
76 padding_top: 0.,
77 padding_bottom: 0.,
78 get_width_from_item: None,
79 }
80 }
81
82 pub fn with_width_from_item(mut self, item_ix: Option<usize>) -> Self {
83 self.get_width_from_item = item_ix;
84 self
85 }
86
87 pub fn with_padding_top(mut self, padding: f32) -> Self {
88 self.padding_top = padding;
89 self
90 }
91
92 pub fn with_padding_bottom(mut self, padding: f32) -> Self {
93 self.padding_bottom = padding;
94 self
95 }
96
97 fn scroll(
98 &self,
99 _: Vector2F,
100 mut delta: Vector2F,
101 precise: bool,
102 scroll_max: f32,
103 cx: &mut EventContext,
104 ) -> bool {
105 if !precise {
106 delta *= 20.;
107 }
108
109 let mut state = self.state.0.borrow_mut();
110 state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
111 cx.notify();
112
113 true
114 }
115
116 fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
117 let mut state = self.state.0.borrow_mut();
118
119 if let Some(scroll_to) = state.scroll_to.take() {
120 let item_ix;
121 let center;
122 match scroll_to {
123 ScrollTarget::Show(ix) => {
124 item_ix = ix;
125 center = false;
126 }
127 ScrollTarget::Center(ix) => {
128 item_ix = ix;
129 center = true;
130 }
131 }
132
133 let item_top = self.padding_top + item_ix as f32 * item_height;
134 let item_bottom = item_top + item_height;
135 if center {
136 let item_center = item_top + item_height / 2.;
137 state.scroll_top = (item_center - list_height / 2.).max(0.);
138 } else {
139 let scroll_bottom = state.scroll_top + list_height;
140 if item_top < state.scroll_top {
141 state.scroll_top = item_top;
142 } else if item_bottom > scroll_bottom {
143 state.scroll_top = item_bottom - list_height;
144 }
145 }
146 }
147
148 if state.scroll_top > scroll_max {
149 state.scroll_top = scroll_max;
150 }
151 }
152
153 fn scroll_top(&self) -> f32 {
154 self.state.0.borrow().scroll_top
155 }
156}
157
158impl Element for UniformList {
159 type LayoutState = LayoutState;
160 type PaintState = ();
161
162 fn layout(
163 &mut self,
164 constraint: SizeConstraint,
165 cx: &mut LayoutContext,
166 ) -> (Vector2F, Self::LayoutState) {
167 if constraint.max.y().is_infinite() {
168 unimplemented!(
169 "UniformList does not support being rendered with an unconstrained height"
170 );
171 }
172
173 let no_items = (
174 constraint.min,
175 LayoutState {
176 item_height: 0.,
177 scroll_max: 0.,
178 items: Default::default(),
179 },
180 );
181
182 if self.item_count == 0 {
183 return no_items;
184 }
185
186 let mut items = Vec::new();
187 let mut size = constraint.max;
188 let mut item_size;
189 let sample_item_ix;
190 let sample_item;
191 if let Some(sample_ix) = self.get_width_from_item {
192 (self.append_items)(sample_ix..sample_ix + 1, &mut items, cx);
193 sample_item_ix = sample_ix;
194
195 if let Some(mut item) = items.pop() {
196 item_size = item.layout(constraint, cx);
197 size.set_x(item_size.x());
198 sample_item = item;
199 } else {
200 return no_items;
201 }
202 } else {
203 (self.append_items)(0..1, &mut items, cx);
204 sample_item_ix = 0;
205 if let Some(mut item) = items.pop() {
206 item_size = item.layout(
207 SizeConstraint::new(
208 vec2f(constraint.max.x(), 0.0),
209 vec2f(constraint.max.x(), f32::INFINITY),
210 ),
211 cx,
212 );
213 item_size.set_x(size.x());
214 sample_item = item
215 } else {
216 return no_items;
217 }
218 }
219
220 let item_constraint = SizeConstraint {
221 min: item_size,
222 max: vec2f(constraint.max.x(), item_size.y()),
223 };
224 let item_height = item_size.y();
225
226 let scroll_height = self.item_count as f32 * item_height;
227 if scroll_height < size.y() {
228 size.set_y(size.y().min(scroll_height).max(constraint.min.y()));
229 }
230
231 let scroll_height =
232 item_height * self.item_count as f32 + self.padding_top + self.padding_bottom;
233 let scroll_max = (scroll_height - size.y()).max(0.);
234 self.autoscroll(scroll_max, size.y(), item_height);
235
236 let start = cmp::min(
237 ((self.scroll_top() - self.padding_top) / item_height.max(1.)) as usize,
238 self.item_count,
239 );
240 let end = cmp::min(
241 self.item_count,
242 start + (size.y() / item_height.max(1.)).ceil() as usize + 1,
243 );
244
245 if (start..end).contains(&sample_item_ix) {
246 if sample_item_ix > start {
247 (self.append_items)(start..sample_item_ix, &mut items, cx);
248 }
249
250 items.push(sample_item);
251
252 if sample_item_ix < end {
253 (self.append_items)(sample_item_ix + 1..end, &mut items, cx);
254 }
255 } else {
256 (self.append_items)(start..end, &mut items, cx);
257 }
258
259 for item in &mut items {
260 let item_size = item.layout(item_constraint, cx);
261 if item_size.x() > size.x() {
262 size.set_x(item_size.x());
263 }
264 }
265
266 (
267 size,
268 LayoutState {
269 item_height,
270 scroll_max,
271 items,
272 },
273 )
274 }
275
276 fn paint(
277 &mut self,
278 bounds: RectF,
279 visible_bounds: RectF,
280 layout: &mut Self::LayoutState,
281 cx: &mut PaintContext,
282 ) -> Self::PaintState {
283 cx.scene.push_layer(Some(bounds));
284
285 let mut item_origin = bounds.origin()
286 - vec2f(
287 0.,
288 (self.state.scroll_top() - self.padding_top) % layout.item_height,
289 );
290
291 for item in &mut layout.items {
292 item.paint(item_origin, visible_bounds, cx);
293 item_origin += vec2f(0.0, layout.item_height);
294 }
295
296 cx.scene.pop_layer();
297 }
298
299 fn dispatch_event(
300 &mut self,
301 event: &Event,
302 bounds: RectF,
303 _: RectF,
304 layout: &mut Self::LayoutState,
305 _: &mut Self::PaintState,
306 cx: &mut EventContext,
307 ) -> bool {
308 let mut handled = false;
309 for item in &mut layout.items {
310 handled = item.dispatch_event(event, cx) || handled;
311 }
312
313 match event {
314 Event::ScrollWheel(ScrollWheelEvent {
315 position,
316 delta,
317 precise,
318 }) => {
319 if bounds.contains_point(*position) {
320 if self.scroll(*position, *delta, *precise, layout.scroll_max, cx) {
321 handled = true;
322 }
323 }
324 }
325 _ => {}
326 }
327
328 handled
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}