1use crate::{
2 point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement,
3 InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size,
4 StyleRefinement, Styled, View, ViewContext, WindowContext,
5};
6use smallvec::SmallVec;
7use std::{cell::RefCell, cmp, 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, R, V>(
14 view: View<V>,
15 id: I,
16 item_count: usize,
17 f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
18) -> UniformList
19where
20 I: Into<ElementId>,
21 R: RenderOnce,
22 V: Render,
23{
24 let id = id.into();
25 let mut style = StyleRefinement::default();
26 style.overflow.y = Some(Overflow::Hidden);
27
28 let render_range = move |range, cx: &mut WindowContext| {
29 view.update(cx, |this, cx| {
30 f(this, range, cx)
31 .into_iter()
32 .map(|component| component.render_into_any())
33 .collect()
34 })
35 };
36
37 UniformList {
38 id: id.clone(),
39 style,
40 item_count,
41 item_to_measure_index: 0,
42 render_items: Box::new(render_range),
43 interactivity: Interactivity {
44 element_id: Some(id.into()),
45 ..Default::default()
46 },
47 scroll_handle: None,
48 }
49}
50
51pub struct UniformList {
52 id: ElementId,
53 style: StyleRefinement,
54 item_count: usize,
55 item_to_measure_index: usize,
56 render_items:
57 Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
58 interactivity: Interactivity,
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 Styled for UniformList {
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 Element for UniformList {
105 type State = UniformListState;
106
107 fn layout(
108 &mut self,
109 state: Option<Self::State>,
110 cx: &mut WindowContext,
111 ) -> (LayoutId, Self::State) {
112 let max_items = self.item_count;
113 let rem_size = cx.rem_size();
114 let item_size = state
115 .as_ref()
116 .map(|s| s.item_size)
117 .unwrap_or_else(|| self.measure_item(None, cx));
118
119 let (layout_id, interactive) =
120 self.interactivity
121 .layout(state.map(|s| s.interactive), cx, |style, cx| {
122 cx.request_measured_layout(
123 style,
124 rem_size,
125 move |known_dimensions: Size<Option<Pixels>>,
126 available_space: Size<AvailableSpace>| {
127 let desired_height = item_size.height * max_items;
128 let width =
129 known_dimensions
130 .width
131 .unwrap_or(match available_space.width {
132 AvailableSpace::Definite(x) => x,
133 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
134 item_size.width
135 }
136 });
137 let height = match available_space.height {
138 AvailableSpace::Definite(x) => desired_height.min(x),
139 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
140 desired_height
141 }
142 };
143 size(width, height)
144 },
145 )
146 });
147
148 let element_state = UniformListState {
149 interactive,
150 item_size,
151 };
152
153 (layout_id, element_state)
154 }
155
156 fn paint(
157 self,
158 bounds: Bounds<crate::Pixels>,
159 element_state: &mut Self::State,
160 cx: &mut WindowContext,
161 ) {
162 let style =
163 self.interactivity
164 .compute_style(Some(bounds), &mut element_state.interactive, cx);
165 let border = style.border_widths.to_pixels(cx.rem_size());
166 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
167
168 let padded_bounds = Bounds::from_corners(
169 bounds.origin + point(border.left + padding.left, border.top + padding.top),
170 bounds.lower_right()
171 - point(border.right + padding.right, border.bottom + padding.bottom),
172 );
173
174 let item_size = element_state.item_size;
175 let content_size = Size {
176 width: padded_bounds.size.width,
177 height: item_size.height * self.item_count,
178 };
179
180 let shared_scroll_offset = element_state
181 .interactive
182 .scroll_offset
183 .get_or_insert_with(Rc::default)
184 .clone();
185
186 let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
187
188 self.interactivity.paint(
189 bounds,
190 content_size,
191 &mut element_state.interactive,
192 cx,
193 |style, scroll_offset, cx| {
194 let border = style.border_widths.to_pixels(cx.rem_size());
195 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
196
197 let padded_bounds = Bounds::from_corners(
198 bounds.origin + point(border.left + padding.left, border.top + padding.top),
199 bounds.lower_right()
200 - point(border.right + padding.right, border.bottom + padding.bottom),
201 );
202
203 cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
204 style.paint(bounds, cx);
205
206 if self.item_count > 0 {
207 if let Some(scroll_handle) = self.scroll_handle.clone() {
208 scroll_handle.0.borrow_mut().replace(ScrollHandleState {
209 item_height,
210 list_height: padded_bounds.size.height,
211 scroll_offset: shared_scroll_offset,
212 });
213 }
214 let visible_item_count = if item_height > px(0.) {
215 (padded_bounds.size.height / item_height).ceil() as usize + 1
216 } else {
217 0
218 };
219
220 let first_visible_element_ix =
221 (-scroll_offset.y / item_height).floor() as usize;
222 let visible_range = first_visible_element_ix
223 ..cmp::min(
224 first_visible_element_ix + visible_item_count,
225 self.item_count,
226 );
227
228 let items = (self.render_items)(visible_range.clone(), cx);
229 cx.with_z_index(1, |cx| {
230 for (item, ix) in items.into_iter().zip(visible_range) {
231 let item_origin = padded_bounds.origin
232 + point(px(0.), item_height * ix + scroll_offset.y);
233 let available_space = size(
234 AvailableSpace::Definite(padded_bounds.size.width),
235 AvailableSpace::Definite(item_height),
236 );
237 item.draw(item_origin, available_space, cx);
238 }
239 });
240 }
241 })
242 },
243 );
244 }
245}
246
247impl RenderOnce for UniformList {
248 type Element = Self;
249
250 fn element_id(&self) -> Option<crate::ElementId> {
251 Some(self.id.clone())
252 }
253
254 fn render_once(self) -> Self::Element {
255 self
256 }
257}
258
259impl UniformList {
260 pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
261 self.item_to_measure_index = item_index.unwrap_or(0);
262 self
263 }
264
265 fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
266 if self.item_count == 0 {
267 return Size::default();
268 }
269
270 let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
271 let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
272 let mut item_to_measure = items.pop().unwrap();
273 let available_space = size(
274 list_width.map_or(AvailableSpace::MinContent, |width| {
275 AvailableSpace::Definite(width)
276 }),
277 AvailableSpace::MinContent,
278 );
279 item_to_measure.measure(available_space, cx)
280 }
281
282 pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
283 self.scroll_handle = Some(handle);
284 self
285 }
286}
287
288impl InteractiveElement for UniformList {
289 fn interactivity(&mut self) -> &mut crate::Interactivity {
290 &mut self.interactivity
291 }
292}