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}