sticky_items.rs

  1use std::ops::Range;
  2
  3use gpui::{
  4    AnyElement, App, AvailableSpace, Bounds, Context, Entity, Pixels, Render, UniformListTopSlot,
  5    Window, point, size,
  6};
  7use smallvec::SmallVec;
  8
  9pub trait StickyCandidate {
 10    fn depth(&self) -> usize;
 11}
 12
 13pub struct StickyItems<T> {
 14    compute_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<T>>,
 15    render_fn: Box<dyn Fn(T, &mut Window, &mut App) -> SmallVec<[AnyElement; 8]>>,
 16    last_item_is_drifting: bool,
 17    anchor_index: Option<usize>,
 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>) -> Vec<T> + 'static,
 23    render_fn: impl Fn(&mut V, T, &mut Window, &mut Context<V>) -> SmallVec<[AnyElement; 8]> + 'static,
 24) -> StickyItems<T>
 25where
 26    V: Render,
 27    T: StickyCandidate + Clone + 'static,
 28{
 29    let entity_compute = entity.clone();
 30    let entity_render = entity.clone();
 31
 32    let compute_fn = Box::new(
 33        move |range: Range<usize>, window: &mut Window, cx: &mut App| -> Vec<T> {
 34            entity_compute.update(cx, |view, cx| compute_fn(view, range, window, cx))
 35        },
 36    );
 37    let render_fn = Box::new(
 38        move |entry: T, window: &mut Window, cx: &mut App| -> SmallVec<[AnyElement; 8]> {
 39            entity_render.update(cx, |view, cx| render_fn(view, entry, window, cx))
 40        },
 41    );
 42    StickyItems {
 43        compute_fn,
 44        render_fn,
 45        last_item_is_drifting: false,
 46        anchor_index: None,
 47    }
 48}
 49
 50impl<T> UniformListTopSlot for StickyItems<T>
 51where
 52    T: StickyCandidate + Clone + 'static,
 53{
 54    fn compute(
 55        &mut self,
 56        visible_range: Range<usize>,
 57        window: &mut Window,
 58        cx: &mut App,
 59    ) -> SmallVec<[AnyElement; 8]> {
 60        let entries = (self.compute_fn)(visible_range.clone(), window, cx);
 61
 62        let mut anchor_entry = None;
 63
 64        let mut iter = entries.iter().enumerate().peekable();
 65        while let Some((ix, current_entry)) = iter.next() {
 66            let current_depth = current_entry.depth();
 67            let index_in_range = ix;
 68
 69            if current_depth < index_in_range {
 70                anchor_entry = Some(current_entry.clone());
 71                break;
 72            }
 73
 74            if let Some(&(_next_ix, next_entry)) = iter.peek() {
 75                let next_depth = next_entry.depth();
 76
 77                if next_depth < current_depth && next_depth < index_in_range {
 78                    self.last_item_is_drifting = true;
 79                    self.anchor_index = Some(visible_range.start + ix);
 80                    anchor_entry = Some(current_entry.clone());
 81                    break;
 82                }
 83            }
 84        }
 85
 86        if let Some(anchor_entry) = anchor_entry {
 87            (self.render_fn)(anchor_entry, window, cx)
 88        } else {
 89            SmallVec::new()
 90        }
 91    }
 92
 93    fn prepaint(
 94        &self,
 95        items: &mut SmallVec<[AnyElement; 8]>,
 96        bounds: Bounds<Pixels>,
 97        item_height: Pixels,
 98        scroll_offset: gpui::Point<Pixels>,
 99        padding: gpui::Edges<Pixels>,
100        can_scroll_horizontally: bool,
101        window: &mut Window,
102        cx: &mut App,
103    ) {
104        let items_count = items.len();
105
106        for (ix, item) in items.iter_mut().enumerate() {
107            let mut item_y_offset = None;
108            if ix == items_count - 1 && self.last_item_is_drifting {
109                if let Some(anchor_index) = self.anchor_index {
110                    let scroll_top = -scroll_offset.y;
111                    let anchor_top = item_height * anchor_index;
112                    let sticky_area_height = item_height * items_count;
113                    item_y_offset =
114                        Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO));
115                };
116            }
117
118            let sticky_origin = bounds.origin
119                + point(
120                    if can_scroll_horizontally {
121                        scroll_offset.x + padding.left
122                    } else {
123                        scroll_offset.x
124                    },
125                    item_height * ix + padding.top + item_y_offset.unwrap_or(Pixels::ZERO),
126                );
127
128            let available_width = if can_scroll_horizontally {
129                bounds.size.width + scroll_offset.x.abs()
130            } else {
131                bounds.size.width
132            };
133
134            let available_space = size(
135                AvailableSpace::Definite(available_width),
136                AvailableSpace::Definite(item_height),
137            );
138
139            item.layout_as_root(available_space, window, cx);
140            item.prepaint_at(sticky_origin, window, cx);
141        }
142    }
143
144    fn paint(&self, items: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App) {
145        // reverse so that last item is bottom most among sticky items
146        for item in items.iter_mut().rev() {
147            item.paint(window, cx);
148        }
149    }
150}