Skip to main content

ui/components/
sticky_items.rs

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}