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