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