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.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}