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;
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 fn paint(
111 &mut self,
112 _id: Option<&GlobalElementId>,
113 _inspector_id: Option<&InspectorElementId>,
114 _bounds: Bounds<Pixels>,
115 _request_layout: &mut Self::RequestLayoutState,
116 _prepaint: &mut Self::PrepaintState,
117 window: &mut Window,
118 cx: &mut App,
119 ) {
120 if let Some(ref mut drifting_element) = self.drifting_element {
121 drifting_element.paint(window, cx);
122 }
123 if let Some(ref mut drifting_decoration) = self.drifting_decoration {
124 drifting_decoration.paint(window, cx);
125 }
126 for item in self.rest_elements.iter_mut().rev() {
127 item.paint(window, cx);
128 }
129 for item in self.rest_decorations.iter_mut() {
130 item.paint(window, cx);
131 }
132 }
133}
134
135impl<T> UniformListDecoration for StickyItems<T>
136where
137 T: StickyCandidate + Clone + 'static,
138{
139 fn compute(
140 &self,
141 visible_range: Range<usize>,
142 bounds: Bounds<Pixels>,
143 scroll_offset: Point<Pixels>,
144 item_height: Pixels,
145 _item_count: usize,
146 window: &mut Window,
147 cx: &mut App,
148 ) -> AnyElement {
149 let entries = (self.compute_fn)(visible_range.clone(), window, cx);
150
151 let Some(sticky_anchor) = find_sticky_anchor(&entries, visible_range.start) else {
152 return StickyItemsElement {
153 drifting_element: None,
154 drifting_decoration: None,
155 rest_elements: SmallVec::new(),
156 rest_decorations: SmallVec::new(),
157 }
158 .into_any_element();
159 };
160
161 let anchor_depth = sticky_anchor.entry.depth();
162 let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx);
163 let items_count = elements.len();
164
165 let indents: SmallVec<[usize; 8]> = (0..items_count)
166 .map(|ix| anchor_depth.saturating_sub(items_count.saturating_sub(ix)))
167 .collect();
168
169 let mut last_decoration_element = None;
170 let mut rest_decoration_elements = SmallVec::new();
171
172 let expanded_width = bounds.size.width + scroll_offset.x.abs();
173
174 let decor_available_space = size(
175 AvailableSpace::Definite(expanded_width),
176 AvailableSpace::Definite(bounds.size.height),
177 );
178
179 let drifting_y_offset = if sticky_anchor.drifting {
180 let scroll_top = -scroll_offset.y;
181 let anchor_top = item_height * (sticky_anchor.index + 1);
182 let sticky_area_height = item_height * items_count;
183 (anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)
184 } else {
185 Pixels::ZERO
186 };
187
188 let (drifting_indent, rest_indents) = if sticky_anchor.drifting && !indents.is_empty() {
189 let last = indents[indents.len() - 1];
190 let rest: SmallVec<[usize; 8]> = indents[..indents.len() - 1].iter().copied().collect();
191 (Some(last), rest)
192 } else {
193 (None, indents)
194 };
195
196 let base_origin = bounds.origin - point(px(0.), scroll_offset.y);
197
198 for decoration in &self.decorations {
199 if let Some(drifting_indent) = drifting_indent {
200 let drifting_indent_vec: SmallVec<[usize; 8]> =
201 [drifting_indent].into_iter().collect();
202
203 let sticky_origin = base_origin
204 + point(px(0.), item_height * rest_indents.len() + drifting_y_offset);
205 let decoration_bounds = Bounds::new(sticky_origin, bounds.size);
206
207 let mut drifting_dec = decoration.as_ref().compute(
208 &drifting_indent_vec,
209 decoration_bounds,
210 scroll_offset,
211 item_height,
212 window,
213 cx,
214 );
215 drifting_dec.layout_as_root(decor_available_space, window, cx);
216 drifting_dec.prepaint_at(sticky_origin, window, cx);
217 last_decoration_element = Some(drifting_dec);
218 }
219
220 if !rest_indents.is_empty() {
221 let decoration_bounds = Bounds::new(base_origin, bounds.size);
222 let mut rest_dec = decoration.as_ref().compute(
223 &rest_indents,
224 decoration_bounds,
225 scroll_offset,
226 item_height,
227 window,
228 cx,
229 );
230 rest_dec.layout_as_root(decor_available_space, window, cx);
231 rest_dec.prepaint_at(bounds.origin, window, cx);
232 rest_decoration_elements.push(rest_dec);
233 }
234 }
235
236 let (mut drifting_element, mut rest_elements) =
237 if sticky_anchor.drifting && !elements.is_empty() {
238 let last = elements.pop().unwrap();
239 (Some(last), elements)
240 } else {
241 (None, elements)
242 };
243
244 let element_available_space = size(
245 AvailableSpace::Definite(expanded_width),
246 AvailableSpace::Definite(item_height),
247 );
248
249 // order of prepaint is important here
250 // mouse events checks hitboxes in reverse insertion order
251 if let Some(ref mut drifting_element) = drifting_element {
252 let sticky_origin = base_origin
253 + point(
254 px(0.),
255 item_height * rest_elements.len() + drifting_y_offset,
256 );
257
258 drifting_element.layout_as_root(element_available_space, window, cx);
259 drifting_element.prepaint_at(sticky_origin, window, cx);
260 }
261
262 for (ix, element) in rest_elements.iter_mut().enumerate() {
263 let sticky_origin = base_origin + point(px(0.), item_height * ix);
264
265 element.layout_as_root(element_available_space, window, cx);
266 element.prepaint_at(sticky_origin, window, cx);
267 }
268
269 StickyItemsElement {
270 drifting_element,
271 drifting_decoration: last_decoration_element,
272 rest_elements,
273 rest_decorations: rest_decoration_elements,
274 }
275 .into_any_element()
276 }
277}
278
279struct StickyAnchor<T> {
280 entry: T,
281 index: usize,
282 drifting: bool,
283}
284
285fn find_sticky_anchor<T: StickyCandidate + Clone>(
286 entries: &SmallVec<[T; 8]>,
287 visible_range_start: usize,
288) -> Option<StickyAnchor<T>> {
289 let mut iter = entries.iter().enumerate().peekable();
290 while let Some((ix, current_entry)) = iter.next() {
291 let depth = current_entry.depth();
292
293 if depth < ix {
294 return Some(StickyAnchor {
295 entry: current_entry.clone(),
296 index: visible_range_start + ix,
297 drifting: false,
298 });
299 }
300
301 if let Some(&(_next_ix, next_entry)) = iter.peek() {
302 let next_depth = next_entry.depth();
303 let next_item_outdented = next_depth + 1 == depth;
304
305 let depth_same_as_index = depth == ix;
306 let depth_greater_than_index = depth == ix + 1;
307
308 if next_item_outdented && (depth_same_as_index || depth_greater_than_index) {
309 return Some(StickyAnchor {
310 entry: current_entry.clone(),
311 index: visible_range_start + ix,
312 drifting: depth_greater_than_index,
313 });
314 }
315 }
316 }
317
318 None
319}
320
321/// A decoration for a [`StickyItems`]. This can be used for various things,
322/// such as rendering indent guides, or other visual effects.
323pub trait StickyItemsDecoration {
324 /// Compute the decoration element, given the visible range of list items,
325 /// the bounds of the list, and the height of each item.
326 fn compute(
327 &self,
328 indents: &SmallVec<[usize; 8]>,
329 bounds: Bounds<Pixels>,
330 scroll_offset: Point<Pixels>,
331 item_height: Pixels,
332 window: &mut Window,
333 cx: &mut App,
334 ) -> AnyElement;
335}