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