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