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