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