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