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