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}