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}