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, MouseRegion, 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 ViewContext<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 bounds: RectF,
276 visible_bounds: RectF,
277 layout: &mut Self::LayoutState,
278 view: &mut V,
279 cx: &mut ViewContext<V>,
280 ) -> Self::PaintState {
281 let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
282
283 cx.scene().push_layer(Some(visible_bounds));
284
285 cx.scene().push_mouse_region(
286 MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
287 let scroll_max = layout.scroll_max;
288 let state = self.state.clone();
289 move |event, _, cx| {
290 let ScrollWheelEvent {
291 position, delta, ..
292 } = event.platform_event;
293 if !Self::scroll(
294 state.clone(),
295 position,
296 *delta.raw(),
297 delta.precise(),
298 scroll_max,
299 cx,
300 ) {
301 cx.propagate_event();
302 }
303 }
304 }),
305 );
306
307 let mut item_origin = bounds.origin()
308 - vec2f(
309 0.,
310 (self.state.scroll_top() - self.padding_top) % layout.item_height,
311 );
312
313 for item in &mut layout.items {
314 item.paint(item_origin, visible_bounds, view, cx);
315 item_origin += vec2f(0.0, layout.item_height);
316 }
317
318 cx.scene().pop_layer();
319 }
320
321 fn rect_for_text_range(
322 &self,
323 range: Range<usize>,
324 _: RectF,
325 _: RectF,
326 layout: &Self::LayoutState,
327 _: &Self::PaintState,
328 view: &V,
329 cx: &ViewContext<V>,
330 ) -> Option<RectF> {
331 layout
332 .items
333 .iter()
334 .find_map(|child| child.rect_for_text_range(range.clone(), view, cx))
335 }
336
337 fn debug(
338 &self,
339 bounds: RectF,
340 layout: &Self::LayoutState,
341 _: &Self::PaintState,
342 view: &V,
343 cx: &ViewContext<V>,
344 ) -> json::Value {
345 json!({
346 "type": "UniformList",
347 "bounds": bounds.to_json(),
348 "scroll_max": layout.scroll_max,
349 "item_height": layout.item_height,
350 "items": layout.items.iter().map(|item| item.debug(view, cx)).collect::<Vec<json::Value>>()
351
352 })
353 }
354}