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 layout(
112 &mut self,
113 view_state: &mut V,
114 element_state: Option<Self::ElementState>,
115 cx: &mut ViewContext<V>,
116 ) -> (LayoutId, Self::ElementState) {
117 let max_items = self.item_count;
118 let rem_size = cx.rem_size();
119 let item_size = element_state
120 .as_ref()
121 .map(|s| s.item_size)
122 .unwrap_or_else(|| self.measure_item(view_state, None, cx));
123
124 let (layout_id, interactive) =
125 self.interactivity
126 .layout(element_state.map(|s| s.interactive), cx, |style, cx| {
127 cx.request_measured_layout(
128 style,
129 rem_size,
130 move |known_dimensions: Size<Option<Pixels>>,
131 available_space: Size<AvailableSpace>| {
132 let desired_height = item_size.height * max_items;
133 let width =
134 known_dimensions
135 .width
136 .unwrap_or(match available_space.width {
137 AvailableSpace::Definite(x) => x,
138 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
139 item_size.width
140 }
141 });
142 let height = match available_space.height {
143 AvailableSpace::Definite(x) => desired_height.min(x),
144 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
145 desired_height
146 }
147 };
148 size(width, height)
149 },
150 )
151 });
152
153 let element_state = UniformListState {
154 interactive,
155 item_size,
156 };
157
158 (layout_id, element_state)
159 }
160
161 fn paint(
162 &mut self,
163 bounds: Bounds<crate::Pixels>,
164 view_state: &mut V,
165 element_state: &mut Self::ElementState,
166 cx: &mut ViewContext<V>,
167 ) {
168 let style =
169 self.interactivity
170 .compute_style(Some(bounds), &mut element_state.interactive, cx);
171 let border = style.border_widths.to_pixels(cx.rem_size());
172 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
173
174 let padded_bounds = Bounds::from_corners(
175 bounds.origin + point(border.left + padding.left, border.top + padding.top),
176 bounds.lower_right()
177 - point(border.right + padding.right, border.bottom + padding.bottom),
178 );
179
180 let item_size = element_state.item_size;
181 let content_size = Size {
182 width: padded_bounds.size.width,
183 height: item_size.height * self.item_count,
184 };
185
186 let mut interactivity = mem::take(&mut self.interactivity);
187 let shared_scroll_offset = element_state
188 .interactive
189 .scroll_offset
190 .get_or_insert_with(Rc::default)
191 .clone();
192
193 interactivity.paint(
194 bounds,
195 content_size,
196 &mut element_state.interactive,
197 cx,
198 |style, scroll_offset, cx| {
199 let border = style.border_widths.to_pixels(cx.rem_size());
200 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
201
202 let padded_bounds = Bounds::from_corners(
203 bounds.origin + point(border.left + padding.left, border.top + padding.top),
204 bounds.lower_right()
205 - point(border.right + padding.right, border.bottom + padding.bottom),
206 );
207
208 cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
209 style.paint(bounds, cx);
210
211 if self.item_count > 0 {
212 let item_height = self
213 .measure_item(view_state, Some(padded_bounds.size.width), cx)
214 .height;
215 if let Some(scroll_handle) = self.scroll_handle.clone() {
216 scroll_handle.0.borrow_mut().replace(ScrollHandleState {
217 item_height,
218 list_height: padded_bounds.size.height,
219 scroll_offset: shared_scroll_offset,
220 });
221 }
222 let visible_item_count = if item_height > px(0.) {
223 (padded_bounds.size.height / item_height).ceil() as usize + 1
224 } else {
225 0
226 };
227
228 let first_visible_element_ix =
229 (-scroll_offset.y / item_height).floor() as usize;
230 let visible_range = first_visible_element_ix
231 ..cmp::min(
232 first_visible_element_ix + visible_item_count,
233 self.item_count,
234 );
235
236 let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
237 cx.with_z_index(1, |cx| {
238 for (item, ix) in items.iter_mut().zip(visible_range) {
239 let item_origin = padded_bounds.origin
240 + point(px(0.), item_height * ix + scroll_offset.y);
241 let available_space = size(
242 AvailableSpace::Definite(padded_bounds.size.width),
243 AvailableSpace::Definite(item_height),
244 );
245 item.draw(item_origin, available_space, view_state, cx);
246 }
247 });
248 }
249 })
250 },
251 );
252 self.interactivity = interactivity;
253 }
254}
255
256impl<V> UniformList<V> {
257 pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
258 self.item_to_measure_index = item_index.unwrap_or(0);
259 self
260 }
261
262 fn measure_item(
263 &self,
264 view_state: &mut V,
265 list_width: Option<Pixels>,
266 cx: &mut ViewContext<V>,
267 ) -> Size<Pixels> {
268 if self.item_count == 0 {
269 return Size::default();
270 }
271
272 let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
273 let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx);
274 let mut item_to_measure = items.pop().unwrap();
275 let available_space = size(
276 list_width.map_or(AvailableSpace::MinContent, |width| {
277 AvailableSpace::Definite(width)
278 }),
279 AvailableSpace::MinContent,
280 );
281 item_to_measure.measure(available_space, view_state, cx)
282 }
283
284 pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
285 self.scroll_handle = Some(handle);
286 self
287 }
288}
289
290impl<V> InteractiveComponent<V> for UniformList<V> {
291 fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
292 &mut self.interactivity
293 }
294}
295
296impl<V: 'static> Component<V> for UniformList<V> {
297 fn render(self) -> AnyElement<V> {
298 AnyElement::new(self)
299 }
300}