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