1use crate::{
2 point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
3 ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
4 Pixels, Point, Render, Size, 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 visible 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: IntoElement,
22 V: Render,
23{
24 let id = id.into();
25 let mut base_style = StyleRefinement::default();
26 base_style.overflow.y = Some(Overflow::Scroll);
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.into_any_element())
33 .collect()
34 })
35 };
36
37 UniformList {
38 id: id.clone(),
39 item_count,
40 item_to_measure_index: 0,
41 render_items: Box::new(render_range),
42 interactivity: Interactivity {
43 element_id: Some(id.into()),
44 base_style,
45 ..Default::default()
46 },
47 scroll_handle: None,
48 }
49}
50
51pub struct UniformList {
52 id: ElementId,
53 item_count: usize,
54 item_to_measure_index: usize,
55 render_items:
56 Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
57 interactivity: Interactivity,
58 scroll_handle: Option<UniformListScrollHandle>,
59}
60
61#[derive(Clone, Default)]
62pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
63
64#[derive(Clone, Debug)]
65struct ScrollHandleState {
66 item_height: Pixels,
67 list_height: Pixels,
68 scroll_offset: Rc<RefCell<Point<Pixels>>>,
69}
70
71impl UniformListScrollHandle {
72 pub fn new() -> Self {
73 Self(Rc::new(RefCell::new(None)))
74 }
75
76 pub fn scroll_to_item(&self, ix: usize) {
77 if let Some(state) = &*self.0.borrow() {
78 let mut scroll_offset = state.scroll_offset.borrow_mut();
79 let item_top = state.item_height * ix;
80 let item_bottom = item_top + state.item_height;
81 let scroll_top = -scroll_offset.y;
82 if item_top < scroll_top {
83 scroll_offset.y = -item_top;
84 } else if item_bottom > scroll_top + state.list_height {
85 scroll_offset.y = -(item_bottom - state.list_height);
86 }
87 }
88 }
89}
90
91impl Styled for UniformList {
92 fn style(&mut self) -> &mut StyleRefinement {
93 &mut self.interactivity.base_style
94 }
95}
96
97#[derive(Default)]
98pub struct UniformListState {
99 interactive: InteractiveElementState,
100 item_size: Size<Pixels>,
101}
102
103impl Element for UniformList {
104 type State = UniformListState;
105
106 fn layout(
107 &mut self,
108 state: Option<Self::State>,
109 cx: &mut WindowContext,
110 ) -> (LayoutId, Self::State) {
111 let max_items = self.item_count;
112 let rem_size = cx.rem_size();
113 let item_size = state
114 .as_ref()
115 .map(|s| s.item_size)
116 .unwrap_or_else(|| self.measure_item(None, cx));
117
118 let (layout_id, interactive) =
119 self.interactivity
120 .layout(state.map(|s| s.interactive), cx, |style, cx| {
121 cx.request_measured_layout(
122 style,
123 rem_size,
124 move |known_dimensions: Size<Option<Pixels>>,
125 available_space: Size<AvailableSpace>| {
126 let desired_height = item_size.height * max_items;
127 let width =
128 known_dimensions
129 .width
130 .unwrap_or(match available_space.width {
131 AvailableSpace::Definite(x) => x,
132 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
133 item_size.width
134 }
135 });
136 let height = match available_space.height {
137 AvailableSpace::Definite(x) => desired_height.min(x),
138 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
139 desired_height
140 }
141 };
142 size(width, height)
143 },
144 )
145 });
146
147 let element_state = UniformListState {
148 interactive,
149 item_size,
150 };
151
152 (layout_id, element_state)
153 }
154
155 fn paint(
156 self,
157 bounds: Bounds<crate::Pixels>,
158 element_state: &mut Self::State,
159 cx: &mut WindowContext,
160 ) {
161 let style =
162 self.interactivity
163 .compute_style(Some(bounds), &mut element_state.interactive, cx);
164 let border = style.border_widths.to_pixels(cx.rem_size());
165 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
166
167 let padded_bounds = Bounds::from_corners(
168 bounds.origin + point(border.left + padding.left, border.top + padding.top),
169 bounds.lower_right()
170 - point(border.right + padding.right, border.bottom + padding.bottom),
171 );
172
173 let item_size = element_state.item_size;
174 let content_size = Size {
175 width: padded_bounds.size.width,
176 height: item_size.height * self.item_count,
177 };
178
179 let shared_scroll_offset = element_state
180 .interactive
181 .scroll_offset
182 .get_or_insert_with(Rc::default)
183 .clone();
184
185 let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
186
187 self.interactivity.paint(
188 bounds,
189 content_size,
190 &mut element_state.interactive,
191 cx,
192 |style, scroll_offset, cx| {
193 let border = style.border_widths.to_pixels(cx.rem_size());
194 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
195
196 let padded_bounds = Bounds::from_corners(
197 bounds.origin + point(border.left + padding.left, border.top + padding.top),
198 bounds.lower_right()
199 - point(border.right + padding.right, border.bottom + padding.bottom),
200 );
201
202 cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
203 style.paint(bounds, cx);
204
205 if self.item_count > 0 {
206 if let Some(scroll_handle) = self.scroll_handle.clone() {
207 scroll_handle.0.borrow_mut().replace(ScrollHandleState {
208 item_height,
209 list_height: padded_bounds.size.height,
210 scroll_offset: shared_scroll_offset,
211 });
212 }
213
214 let first_visible_element_ix =
215 (-scroll_offset.y / item_height).floor() as usize;
216 let last_visible_element_ix =
217 ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
218 as usize;
219 let visible_range = first_visible_element_ix
220 ..cmp::min(last_visible_element_ix, self.item_count);
221
222 let items = (self.render_items)(visible_range.clone(), cx);
223 cx.with_z_index(1, |cx| {
224 let content_mask = ContentMask {
225 bounds: padded_bounds,
226 };
227 cx.with_content_mask(Some(content_mask), |cx| {
228 for (item, ix) in items.into_iter().zip(visible_range) {
229 let item_origin = padded_bounds.origin
230 + point(px(0.), item_height * ix + scroll_offset.y);
231 let available_space = size(
232 AvailableSpace::Definite(padded_bounds.size.width),
233 AvailableSpace::Definite(item_height),
234 );
235 item.draw(item_origin, available_space, cx);
236 }
237 });
238 });
239 }
240 })
241 },
242 );
243 }
244}
245
246impl IntoElement for UniformList {
247 type Element = Self;
248
249 fn element_id(&self) -> Option<crate::ElementId> {
250 Some(self.id.clone())
251 }
252
253 fn into_element(self) -> Self::Element {
254 self
255 }
256}
257
258impl UniformList {
259 pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
260 self.item_to_measure_index = item_index.unwrap_or(0);
261 self
262 }
263
264 fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
265 if self.item_count == 0 {
266 return Size::default();
267 }
268
269 let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
270 let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
271 let mut item_to_measure = items.pop().unwrap();
272 let available_space = size(
273 list_width.map_or(AvailableSpace::MinContent, |width| {
274 AvailableSpace::Definite(width)
275 }),
276 AvailableSpace::MinContent,
277 );
278 item_to_measure.measure(available_space, cx)
279 }
280
281 pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
282 self.scroll_handle = Some(handle);
283 self
284 }
285}
286
287impl InteractiveElement for UniformList {
288 fn interactivity(&mut self) -> &mut crate::Interactivity {
289 &mut self.interactivity
290 }
291}