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