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