1use std::{ops::Range, rc::Rc};
2
3use gpui::{
4 AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId,
5 InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration,
6 Window, point, px, size,
7};
8use smallvec::SmallVec;
9
10pub trait StickyCandidate {
11 fn depth(&self) -> usize;
12}
13
14pub struct StickyItems<T> {
15 compute_fn: Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[T; 8]>>,
16 render_fn: Rc<dyn Fn(T, &mut Window, &mut App) -> SmallVec<[AnyElement; 8]>>,
17 decorations: Vec<Box<dyn StickyItemsDecoration>>,
18}
19
20pub fn sticky_items<V, T>(
21 entity: Entity<V>,
22 compute_fn: impl Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> SmallVec<[T; 8]>
23 + 'static,
24 render_fn: impl Fn(&mut V, T, &mut Window, &mut Context<V>) -> SmallVec<[AnyElement; 8]> + 'static,
25) -> StickyItems<T>
26where
27 V: Render,
28 T: StickyCandidate + Clone + 'static,
29{
30 let entity_compute = entity.clone();
31 let entity_render = entity.clone();
32
33 let compute_fn = Rc::new(
34 move |range: Range<usize>, window: &mut Window, cx: &mut App| -> SmallVec<[T; 8]> {
35 entity_compute.update(cx, |view, cx| compute_fn(view, range, window, cx))
36 },
37 );
38 let render_fn = Rc::new(
39 move |entry: T, window: &mut Window, cx: &mut App| -> SmallVec<[AnyElement; 8]> {
40 entity_render.update(cx, |view, cx| render_fn(view, entry, window, cx))
41 },
42 );
43
44 StickyItems {
45 compute_fn,
46 render_fn,
47 decorations: Vec::new(),
48 }
49}
50
51impl<T> StickyItems<T>
52where
53 T: StickyCandidate + Clone + 'static,
54{
55 /// Adds a decoration element to the sticky items.
56 pub fn with_decoration(mut self, decoration: impl StickyItemsDecoration + 'static) -> Self {
57 self.decorations.push(Box::new(decoration));
58 self
59 }
60}
61
62struct StickyItemsElement {
63 drifting_element: Option<AnyElement>,
64 drifting_decoration: Option<AnyElement>,
65 rest_elements: SmallVec<[AnyElement; 8]>,
66 rest_decorations: SmallVec<[AnyElement; 1]>,
67}
68
69impl IntoElement for StickyItemsElement {
70 type Element = Self;
71
72 fn into_element(self) -> Self::Element {
73 self
74 }
75}
76
77impl Element for StickyItemsElement {
78 type RequestLayoutState = ();
79 type PrepaintState = ();
80
81 fn id(&self) -> Option<ElementId> {
82 None
83 }
84
85 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
86 None
87 }
88
89 fn request_layout(
90 &mut self,
91 _id: Option<&GlobalElementId>,
92 _inspector_id: Option<&InspectorElementId>,
93 window: &mut Window,
94 cx: &mut App,
95 ) -> (LayoutId, Self::RequestLayoutState) {
96 (window.request_layout(Style::default(), [], cx), ())
97 }
98
99 fn prepaint(
100 &mut self,
101 _id: Option<&GlobalElementId>,
102 _inspector_id: Option<&InspectorElementId>,
103 _bounds: Bounds<Pixels>,
104 _request_layout: &mut Self::RequestLayoutState,
105 _window: &mut Window,
106 _cx: &mut App,
107 ) -> Self::PrepaintState {
108 ()
109 }
110
111 fn paint(
112 &mut self,
113 _id: Option<&GlobalElementId>,
114 _inspector_id: Option<&InspectorElementId>,
115 _bounds: Bounds<Pixels>,
116 _request_layout: &mut Self::RequestLayoutState,
117 _prepaint: &mut Self::PrepaintState,
118 window: &mut Window,
119 cx: &mut App,
120 ) {
121 if let Some(ref mut drifting_element) = self.drifting_element {
122 drifting_element.paint(window, cx);
123 }
124 if let Some(ref mut drifting_decoration) = self.drifting_decoration {
125 drifting_decoration.paint(window, cx);
126 }
127 for item in self.rest_elements.iter_mut().rev() {
128 item.paint(window, cx);
129 }
130 for item in self.rest_decorations.iter_mut() {
131 item.paint(window, cx);
132 }
133 }
134}
135
136impl<T> UniformListDecoration for StickyItems<T>
137where
138 T: StickyCandidate + Clone + 'static,
139{
140 fn compute(
141 &self,
142 visible_range: Range<usize>,
143 bounds: Bounds<Pixels>,
144 scroll_offset: Point<Pixels>,
145 item_height: Pixels,
146 _item_count: usize,
147 window: &mut Window,
148 cx: &mut App,
149 ) -> AnyElement {
150 let entries = (self.compute_fn)(visible_range.clone(), window, cx);
151
152 struct StickyAnchor<T> {
153 entry: T,
154 index: usize,
155 }
156
157 let mut sticky_anchor = None;
158 let mut last_item_is_drifting = false;
159
160 let mut iter = entries.iter().enumerate().peekable();
161 while let Some((ix, current_entry)) = iter.next() {
162 let depth = current_entry.depth();
163
164 if depth < ix {
165 sticky_anchor = Some(StickyAnchor {
166 entry: current_entry.clone(),
167 index: visible_range.start + ix,
168 });
169 break;
170 }
171
172 if let Some(&(_next_ix, next_entry)) = iter.peek() {
173 let next_depth = next_entry.depth();
174 let next_item_outdented = next_depth + 1 == depth;
175
176 let depth_same_as_index = depth == ix;
177 let depth_greater_than_index = depth == ix + 1;
178
179 if next_item_outdented && (depth_same_as_index || depth_greater_than_index) {
180 if depth_greater_than_index {
181 last_item_is_drifting = true;
182 }
183 sticky_anchor = Some(StickyAnchor {
184 entry: current_entry.clone(),
185 index: visible_range.start + ix,
186 });
187 break;
188 }
189 }
190 }
191
192 let Some(sticky_anchor) = sticky_anchor else {
193 return StickyItemsElement {
194 drifting_element: None,
195 drifting_decoration: None,
196 rest_elements: SmallVec::new(),
197 rest_decorations: SmallVec::new(),
198 }
199 .into_any_element();
200 };
201
202 let anchor_depth = sticky_anchor.entry.depth();
203 let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx);
204 let items_count = elements.len();
205
206 let indents: SmallVec<[usize; 8]> = {
207 elements
208 .iter()
209 .enumerate()
210 .map(|(ix, _)| anchor_depth.saturating_sub(items_count.saturating_sub(ix)))
211 .collect()
212 };
213
214 let mut last_decoration_element = None;
215 let mut rest_decoration_elements = SmallVec::new();
216
217 let available_space = size(
218 AvailableSpace::Definite(bounds.size.width),
219 AvailableSpace::Definite(bounds.size.height),
220 );
221
222 let drifting_y_offset = if last_item_is_drifting {
223 let scroll_top = -scroll_offset.y;
224 let anchor_top = item_height * (sticky_anchor.index + 1);
225 let sticky_area_height = item_height * items_count;
226 (anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)
227 } else {
228 Pixels::ZERO
229 };
230
231 let (drifting_indent, rest_indents) = if last_item_is_drifting && !indents.is_empty() {
232 let last = indents[indents.len() - 1];
233 let rest: SmallVec<[usize; 8]> = indents[..indents.len() - 1].iter().copied().collect();
234 (Some(last), rest)
235 } else {
236 (None, indents)
237 };
238
239 for decoration in &self.decorations {
240 if let Some(drifting_indent) = drifting_indent {
241 let drifting_indent_vec: SmallVec<[usize; 8]> =
242 [drifting_indent].into_iter().collect();
243 let sticky_origin = bounds.origin - scroll_offset
244 + point(px(0.), item_height * rest_indents.len() + drifting_y_offset);
245 let decoration_bounds = Bounds::new(sticky_origin, bounds.size);
246
247 let mut drifting_dec = decoration.as_ref().compute(
248 &drifting_indent_vec,
249 decoration_bounds,
250 scroll_offset,
251 item_height,
252 window,
253 cx,
254 );
255 drifting_dec.layout_as_root(available_space, window, cx);
256 drifting_dec.prepaint_at(sticky_origin, window, cx);
257 last_decoration_element = Some(drifting_dec);
258 }
259
260 if !rest_indents.is_empty() {
261 let decoration_bounds = Bounds::new(bounds.origin - scroll_offset, bounds.size);
262 let mut rest_dec = decoration.as_ref().compute(
263 &rest_indents,
264 decoration_bounds,
265 scroll_offset,
266 item_height,
267 window,
268 cx,
269 );
270 rest_dec.layout_as_root(available_space, window, cx);
271 rest_dec.prepaint_at(bounds.origin, window, cx);
272 rest_decoration_elements.push(rest_dec);
273 }
274 }
275
276 let (mut drifting_element, mut rest_elements) =
277 if last_item_is_drifting && !elements.is_empty() {
278 let last = elements.pop().unwrap();
279 (Some(last), elements)
280 } else {
281 (None, elements)
282 };
283
284 for (ix, element) in rest_elements.iter_mut().enumerate() {
285 let sticky_origin = bounds.origin - scroll_offset + point(px(0.), item_height * ix);
286 let element_available_space = size(
287 AvailableSpace::Definite(bounds.size.width),
288 AvailableSpace::Definite(item_height),
289 );
290
291 element.layout_as_root(element_available_space, window, cx);
292 element.prepaint_at(sticky_origin, window, cx);
293 }
294
295 if let Some(ref mut drifting_element) = drifting_element {
296 let sticky_origin = bounds.origin - scroll_offset
297 + point(
298 px(0.),
299 item_height * rest_elements.len() + drifting_y_offset,
300 );
301 let element_available_space = size(
302 AvailableSpace::Definite(bounds.size.width),
303 AvailableSpace::Definite(item_height),
304 );
305
306 drifting_element.layout_as_root(element_available_space, window, cx);
307 drifting_element.prepaint_at(sticky_origin, window, cx);
308 }
309
310 StickyItemsElement {
311 drifting_element,
312 drifting_decoration: last_decoration_element,
313 rest_elements,
314 rest_decorations: rest_decoration_elements,
315 }
316 .into_any_element()
317 }
318}
319
320/// A decoration for a [`StickyItems`]. This can be used for various things,
321/// such as rendering indent guides, or other visual effects.
322pub trait StickyItemsDecoration {
323 /// Compute the decoration element, given the visible range of list items,
324 /// the bounds of the list, and the height of each item.
325 fn compute(
326 &self,
327 indents: &SmallVec<[usize; 8]>,
328 bounds: Bounds<Pixels>,
329 scroll_offset: Point<Pixels>,
330 item_height: Pixels,
331 window: &mut Window,
332 cx: &mut App,
333 ) -> AnyElement;
334}