1use crate::{
2 point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
3 ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
4 Point, Size, StyleRefinement, Styled, ViewContext,
5};
6use smallvec::SmallVec;
7use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
8use taffy::style::Overflow;
9
10/// uniform_list provides lazy rendering for a set of items that are of uniform height.
11/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
12/// uniform_list will only render the visibile subset of items.
13pub fn uniform_list<I, V, C>(
14 id: I,
15 item_count: usize,
16 f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
17) -> UniformList<V>
18where
19 I: Into<ElementId>,
20 V: 'static,
21 C: Component<V>,
22{
23 let id = id.into();
24 let mut style = StyleRefinement::default();
25 style.overflow.y = Some(Overflow::Hidden);
26
27 UniformList {
28 id: id.clone(),
29 style,
30 item_count,
31 item_to_measure_index: 0,
32 render_items: Box::new(move |view, visible_range, cx| {
33 f(view, visible_range, cx)
34 .into_iter()
35 .map(|component| component.render())
36 .collect()
37 }),
38 interactivity: Interactivity {
39 element_id: Some(id.into()),
40 ..Default::default()
41 },
42 scroll_handle: None,
43 }
44}
45
46pub struct UniformList<V: 'static> {
47 id: ElementId,
48 style: StyleRefinement,
49 item_count: usize,
50 item_to_measure_index: usize,
51 render_items: Box<
52 dyn for<'a> Fn(
53 &'a mut V,
54 Range<usize>,
55 &'a mut ViewContext<V>,
56 ) -> SmallVec<[AnyElement<V>; 64]>,
57 >,
58 interactivity: Interactivity<V>,
59 scroll_handle: Option<UniformListScrollHandle>,
60}
61
62#[derive(Clone, Default)]
63pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
64
65#[derive(Clone, Debug)]
66struct ScrollHandleState {
67 item_height: Pixels,
68 list_height: Pixels,
69 scroll_offset: Rc<RefCell<Point<Pixels>>>,
70}
71
72impl UniformListScrollHandle {
73 pub fn new() -> Self {
74 Self(Rc::new(RefCell::new(None)))
75 }
76
77 pub fn scroll_to_item(&self, ix: usize) {
78 if let Some(state) = &*self.0.borrow() {
79 let mut scroll_offset = state.scroll_offset.borrow_mut();
80 let item_top = state.item_height * ix;
81 let item_bottom = item_top + state.item_height;
82 let scroll_top = -scroll_offset.y;
83 if item_top < scroll_top {
84 scroll_offset.y = -item_top;
85 } else if item_bottom > scroll_top + state.list_height {
86 scroll_offset.y = -(item_bottom - state.list_height);
87 }
88 }
89 }
90}
91
92impl<V: 'static> Styled for UniformList<V> {
93 fn style(&mut self) -> &mut StyleRefinement {
94 &mut self.style
95 }
96}
97
98#[derive(Default)]
99pub struct UniformListState {
100 interactive: InteractiveElementState,
101 item_size: Size<Pixels>,
102}
103
104impl<V: 'static> Element<V> for UniformList<V> {
105 type ElementState = UniformListState;
106
107 fn element_id(&self) -> Option<crate::ElementId> {
108 Some(self.id.clone())
109 }
110
111 fn initialize(
112 &mut self,
113 view_state: &mut V,
114 element_state: Option<Self::ElementState>,
115 cx: &mut ViewContext<V>,
116 ) -> Self::ElementState {
117 if let Some(mut element_state) = element_state {
118 element_state.interactive = self
119 .interactivity
120 .initialize(Some(element_state.interactive), cx);
121 element_state
122 } else {
123 let item_size = self.measure_item(view_state, None, cx);
124 UniformListState {
125 interactive: self.interactivity.initialize(None, cx),
126 item_size,
127 }
128 }
129 }
130
131 fn layout(
132 &mut self,
133 _view_state: &mut V,
134 element_state: &mut Self::ElementState,
135 cx: &mut ViewContext<V>,
136 ) -> LayoutId {
137 let max_items = self.item_count;
138 let item_size = element_state.item_size;
139 let rem_size = cx.rem_size();
140
141 self.interactivity
142 .layout(&mut element_state.interactive, cx, |style, cx| {
143 cx.request_measured_layout(
144 style,
145 rem_size,
146 move |known_dimensions: Size<Option<Pixels>>,
147 available_space: Size<AvailableSpace>| {
148 let desired_height = item_size.height * max_items;
149 let width = known_dimensions
150 .width
151 .unwrap_or(match available_space.width {
152 AvailableSpace::Definite(x) => x,
153 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
154 item_size.width
155 }
156 });
157 let height = match available_space.height {
158 AvailableSpace::Definite(x) => desired_height.min(x),
159 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
160 desired_height
161 }
162 };
163 size(width, height)
164 },
165 )
166 })
167 }
168
169 fn paint(
170 &mut self,
171 bounds: Bounds<crate::Pixels>,
172 view_state: &mut V,
173 element_state: &mut Self::ElementState,
174 cx: &mut ViewContext<V>,
175 ) {
176 let style =
177 self.interactivity
178 .compute_style(Some(bounds), &mut element_state.interactive, cx);
179 let border = style.border_widths.to_pixels(cx.rem_size());
180 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
181
182 let padded_bounds = Bounds::from_corners(
183 bounds.origin + point(border.left + padding.left, border.top + padding.top),
184 bounds.lower_right()
185 - point(border.right + padding.right, border.bottom + padding.bottom),
186 );
187
188 let item_size = element_state.item_size;
189 let content_size = Size {
190 width: padded_bounds.size.width,
191 height: item_size.height * self.item_count,
192 };
193
194 let mut interactivity = mem::take(&mut self.interactivity);
195 let shared_scroll_offset = element_state
196 .interactive
197 .scroll_offset
198 .get_or_insert_with(Rc::default)
199 .clone();
200
201 interactivity.paint(
202 bounds,
203 content_size,
204 &mut element_state.interactive,
205 cx,
206 |style, scroll_offset, cx| {
207 let border = style.border_widths.to_pixels(cx.rem_size());
208 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
209
210 let padded_bounds = Bounds::from_corners(
211 bounds.origin + point(border.left + padding.left, border.top + padding.top),
212 bounds.lower_right()
213 - point(border.right + padding.right, border.bottom + padding.bottom),
214 );
215
216 cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
217 style.paint(bounds, cx);
218
219 if self.item_count > 0 {
220 let item_height = self
221 .measure_item(view_state, Some(padded_bounds.size.width), cx)
222 .height;
223 if let Some(scroll_handle) = self.scroll_handle.clone() {
224 scroll_handle.0.borrow_mut().replace(ScrollHandleState {
225 item_height,
226 list_height: padded_bounds.size.height,
227 scroll_offset: shared_scroll_offset,
228 });
229 }
230 let visible_item_count = if item_height > px(0.) {
231 (padded_bounds.size.height / item_height).ceil() as usize + 1
232 } else {
233 0
234 };
235
236 let first_visible_element_ix =
237 (-scroll_offset.y / item_height).floor() as usize;
238 let visible_range = first_visible_element_ix
239 ..cmp::min(
240 first_visible_element_ix + visible_item_count,
241 self.item_count,
242 );
243
244 let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
245 cx.with_z_index(1, |cx| {
246 for (item, ix) in items.iter_mut().zip(visible_range) {
247 let item_origin = padded_bounds.origin
248 + point(px(0.), item_height * ix + scroll_offset.y);
249 let available_space = size(
250 AvailableSpace::Definite(padded_bounds.size.width),
251 AvailableSpace::Definite(item_height),
252 );
253 item.draw(item_origin, available_space, view_state, cx);
254 }
255 });
256 }
257 })
258 },
259 );
260 self.interactivity = interactivity;
261 }
262}
263
264impl<V> UniformList<V> {
265 pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
266 self.item_to_measure_index = item_index.unwrap_or(0);
267 self
268 }
269
270 fn measure_item(
271 &self,
272 view_state: &mut V,
273 list_width: Option<Pixels>,
274 cx: &mut ViewContext<V>,
275 ) -> Size<Pixels> {
276 if self.item_count == 0 {
277 return Size::default();
278 }
279
280 let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
281 let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx);
282 let mut item_to_measure = items.pop().unwrap();
283 let available_space = size(
284 list_width.map_or(AvailableSpace::MinContent, |width| {
285 AvailableSpace::Definite(width)
286 }),
287 AvailableSpace::MinContent,
288 );
289 item_to_measure.measure(available_space, view_state, cx)
290 }
291
292 pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
293 self.scroll_handle = Some(handle);
294 self
295 }
296}
297
298impl<V> InteractiveComponent<V> for UniformList<V> {
299 fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
300 &mut self.interactivity
301 }
302}
303
304impl<V: 'static> Component<V> for UniformList<V> {
305 fn render(self) -> AnyElement<V> {
306 AnyElement::new(self)
307 }
308}