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, size,
  7};
  8use smallvec::SmallVec;
  9
 10pub trait StickyCandidate {
 11    fn depth(&self) -> usize;
 12}
 13
 14#[derive(Clone)]
 15pub struct StickyItems<T> {
 16    compute_fn: Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[T; 8]>>,
 17    render_fn: Rc<dyn Fn(T, &mut Window, &mut App) -> SmallVec<[AnyElement; 8]>>,
 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    }
 48}
 49
 50struct StickyItemsElement {
 51    elements: SmallVec<[AnyElement; 8]>,
 52}
 53
 54impl IntoElement for StickyItemsElement {
 55    type Element = Self;
 56
 57    fn into_element(self) -> Self::Element {
 58        self
 59    }
 60}
 61
 62impl Element for StickyItemsElement {
 63    type RequestLayoutState = ();
 64    type PrepaintState = ();
 65
 66    fn id(&self) -> Option<ElementId> {
 67        None
 68    }
 69
 70    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
 71        None
 72    }
 73
 74    fn request_layout(
 75        &mut self,
 76        _id: Option<&GlobalElementId>,
 77        _inspector_id: Option<&InspectorElementId>,
 78        window: &mut Window,
 79        cx: &mut App,
 80    ) -> (LayoutId, Self::RequestLayoutState) {
 81        (window.request_layout(Style::default(), [], cx), ())
 82    }
 83
 84    fn prepaint(
 85        &mut self,
 86        _id: Option<&GlobalElementId>,
 87        _inspector_id: Option<&InspectorElementId>,
 88        _bounds: Bounds<Pixels>,
 89        _request_layout: &mut Self::RequestLayoutState,
 90        _window: &mut Window,
 91        _cx: &mut App,
 92    ) -> Self::PrepaintState {
 93        ()
 94    }
 95
 96    fn paint(
 97        &mut self,
 98        _id: Option<&GlobalElementId>,
 99        _inspector_id: Option<&InspectorElementId>,
100        _bounds: Bounds<Pixels>,
101        _request_layout: &mut Self::RequestLayoutState,
102        _prepaint: &mut Self::PrepaintState,
103        window: &mut Window,
104        cx: &mut App,
105    ) {
106        // reverse so that last item is bottom most among sticky items
107        for item in self.elements.iter_mut().rev() {
108            item.paint(window, cx);
109        }
110    }
111}
112
113impl<T> UniformListDecoration for StickyItems<T>
114where
115    T: StickyCandidate + Clone + 'static,
116{
117    fn compute(
118        &self,
119        visible_range: Range<usize>,
120        bounds: Bounds<Pixels>,
121        scroll_offset: Point<Pixels>,
122        item_height: Pixels,
123        _item_count: usize,
124        window: &mut Window,
125        cx: &mut App,
126    ) -> AnyElement {
127        let entries = (self.compute_fn)(visible_range.clone(), window, cx);
128        let mut elements = SmallVec::new();
129
130        let mut anchor_entry = None;
131        let mut last_item_is_drifting = false;
132        let mut anchor_index = None;
133
134        let mut iter = entries.iter().enumerate().peekable();
135        while let Some((ix, current_entry)) = iter.next() {
136            let current_depth = current_entry.depth();
137            let index_in_range = ix;
138
139            if current_depth < index_in_range {
140                anchor_entry = Some(current_entry.clone());
141                break;
142            }
143
144            if let Some(&(_next_ix, next_entry)) = iter.peek() {
145                let next_depth = next_entry.depth();
146
147                if next_depth < current_depth && next_depth < index_in_range {
148                    last_item_is_drifting = true;
149                    anchor_index = Some(visible_range.start + ix);
150                    anchor_entry = Some(current_entry.clone());
151                    break;
152                }
153            }
154        }
155
156        if let Some(anchor_entry) = anchor_entry {
157            elements = (self.render_fn)(anchor_entry, window, cx);
158            let items_count = elements.len();
159
160            for (ix, element) in elements.iter_mut().enumerate() {
161                let mut item_y_offset = None;
162                if ix == items_count - 1 && last_item_is_drifting {
163                    if let Some(anchor_index) = anchor_index {
164                        let scroll_top = -scroll_offset.y;
165                        let anchor_top = item_height * anchor_index;
166                        let sticky_area_height = item_height * items_count;
167                        item_y_offset =
168                            Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO));
169                    };
170                }
171
172                let sticky_origin = bounds.origin
173                    + point(
174                        -scroll_offset.x,
175                        -scroll_offset.y + item_height * ix + item_y_offset.unwrap_or(Pixels::ZERO),
176                    );
177
178                let available_space = size(
179                    AvailableSpace::Definite(bounds.size.width),
180                    AvailableSpace::Definite(item_height),
181                );
182                element.layout_as_root(available_space, window, cx);
183                element.prepaint_at(sticky_origin, window, cx);
184            }
185        }
186
187        StickyItemsElement { elements }.into_any_element()
188    }
189}