1use std::cmp::Ordering;
30
31use gpui::prelude::*;
32use gpui::{div, px, AnyElement, App, Context, EventEmitter, IntoElement, SharedString, Window};
33
34use super::Content;
35use crate::reactive::Signal;
36use crate::style::{surface, Variant};
37use crate::theme::{theme, Size};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum DataViewEvent {
44 Selected(usize),
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum DataViewLayout {
50 #[default]
52 List,
53 Grid(usize),
55}
56
57type ItemBuilder<T> = Box<dyn Fn(&T, usize, &mut Window, &mut App) -> AnyElement + 'static>;
58type FilterFn<T> = Box<dyn Fn(&T) -> bool + 'static>;
59type SortFn<T> = Box<dyn Fn(&T, &T) -> Ordering + 'static>;
60type FilterRef<'a, T> = Option<&'a dyn Fn(&T) -> bool>;
61type SortRef<'a, T> = Option<&'a dyn Fn(&T, &T) -> Ordering>;
62
63pub struct DataView<T: 'static> {
66 source: Signal<Vec<T>>,
67 item: Option<ItemBuilder<T>>,
68 filter: Option<FilterFn<T>>,
69 sort: Option<SortFn<T>>,
70 layout: DataViewLayout,
71 gap: Size,
72 empty: Option<Content>,
73 selectable: bool,
74 selected: Option<usize>,
75}
76
77impl<T: 'static> EventEmitter<DataViewEvent> for DataView<T> {}
78
79impl<T: 'static> DataView<T> {
80 pub fn new(cx: &mut Context<Self>, source: &Signal<Vec<T>>) -> Self {
83 cx.observe(source.entity(), |this, source, cx| {
84 let len = source.read(cx).len();
88 if this.selected.is_some_and(|i| i >= len) {
89 this.selected = None;
90 }
91 cx.notify();
92 })
93 .detach();
94 DataView {
95 source: source.clone(),
96 item: None,
97 filter: None,
98 sort: None,
99 layout: DataViewLayout::List,
100 gap: Size::Sm,
101 empty: None,
102 selectable: false,
103 selected: None,
104 }
105 }
106
107 pub fn item<E>(
110 mut self,
111 template: impl Fn(&T, usize, &mut Window, &mut App) -> E + 'static,
112 ) -> Self
113 where
114 E: IntoElement,
115 {
116 self.item = Some(Box::new(move |item, ix, window, cx| {
117 template(item, ix, window, cx).into_any_element()
118 }));
119 self
120 }
121
122 pub fn filter(mut self, pred: impl Fn(&T) -> bool + 'static) -> Self {
125 self.filter = Some(Box::new(pred));
126 self
127 }
128
129 pub fn sort_by(mut self, cmp: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
132 self.sort = Some(Box::new(cmp));
133 self
134 }
135
136 pub fn layout(mut self, layout: DataViewLayout) -> Self {
137 self.layout = layout;
138 self
139 }
140
141 pub fn gap(mut self, gap: Size) -> Self {
143 self.gap = gap;
144 self
145 }
146
147 pub fn empty<E>(mut self, content: impl Fn(&mut Window, &mut App) -> E + 'static) -> Self
150 where
151 E: IntoElement,
152 {
153 self.empty = Some(Box::new(move |window, cx| {
154 content(window, cx).into_any_element()
155 }));
156 self
157 }
158
159 pub fn selectable(mut self) -> Self {
162 self.selectable = true;
163 self
164 }
165
166 pub fn selected_index(&self) -> Option<usize> {
168 self.selected
169 }
170}
171
172fn projection<T>(items: &[T], filter: FilterRef<'_, T>, sort: SortRef<'_, T>) -> Vec<usize> {
174 let mut order: Vec<usize> = (0..items.len())
175 .filter(|&i| filter.is_none_or(|keep| keep(&items[i])))
176 .collect();
177 if let Some(cmp) = sort {
178 order.sort_by(|&a, &b| cmp(&items[a], &items[b]));
179 }
180 order
181}
182
183impl<T: 'static> Render for DataView<T> {
184 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
185 let t = theme(cx);
186 let gap = t.spacing(self.gap);
187 let radius = t.radius(t.default_radius);
188 let hover_bg = t.surface_hover().hsla();
189 let dimmed = t.dimmed().hsla();
190 let font_sm = t.font_size(Size::Sm);
191 let sel = surface(t, t.primary_color, Variant::Light);
194 let (selected_bg, selected_fg) = (sel.bg, sel.fg);
195
196 let template = self.item.as_ref();
199 let filter = self.filter.as_deref();
200 let sort = self.sort.as_deref();
201 let entity = self.source.entity().clone();
202 let built: Vec<(usize, AnyElement)> = entity.update(cx, |items, cx| {
203 let order = projection(items, filter, sort);
204 match template {
205 Some(build) => order
206 .into_iter()
207 .map(|i| (i, build(&items[i], i, window, cx)))
208 .collect(),
209 None => Vec::new(),
210 }
211 });
212
213 let selectable = self.selectable;
214 let selected = self.selected;
217
218 if built.is_empty() {
219 let content = match &self.empty {
220 Some(build) => build(window, cx),
221 None => div()
222 .text_size(px(font_sm))
223 .text_color(dimmed)
224 .child(SharedString::new_static("Nothing to show"))
225 .into_any_element(),
226 };
227 return div()
228 .w_full()
229 .flex()
230 .justify_center()
231 .py(px(16.0))
232 .child(content);
233 }
234
235 let cells: Vec<AnyElement> = built
236 .into_iter()
237 .map(|(source_ix, element)| {
238 if !selectable {
239 return element;
240 }
241 let is_selected = selected == Some(source_ix);
242 let mut cell = div()
243 .id(("guise-dataview-item", source_ix))
244 .px(px(10.0))
245 .py(px(8.0))
246 .rounded(px(radius))
247 .cursor_pointer()
248 .child(element)
249 .on_click(cx.listener(move |this, _ev, _window, cx| {
250 this.selected = Some(source_ix);
251 cx.emit(DataViewEvent::Selected(source_ix));
252 cx.notify();
253 }));
254 cell = if is_selected {
255 cell.bg(selected_bg).text_color(selected_fg)
256 } else {
257 cell.hover(move |s| s.bg(hover_bg))
258 };
259 cell.into_any_element()
260 })
261 .collect();
262
263 let root = div().w_full().flex().flex_col().gap(px(gap));
264 match self.layout {
265 DataViewLayout::List => root.children(cells),
266 DataViewLayout::Grid(cols) => {
267 let cols = cols.max(1);
268 let count = cells.len();
269 let mut rows = Vec::new();
270 let mut row = Vec::new();
271 for (i, cell) in cells.into_iter().enumerate() {
272 row.push(div().flex_1().min_w(px(0.0)).child(cell));
273 if row.len() == cols || i + 1 == count {
274 while row.len() < cols {
276 row.push(div().flex_1().min_w(px(0.0)));
277 }
278 rows.push(div().flex().gap(px(gap)).children(std::mem::take(&mut row)));
279 }
280 }
281 root.children(rows)
282 }
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn identity_without_projections() {
293 assert_eq!(projection(&[10, 20, 30], None, None), vec![0, 1, 2]);
294 assert_eq!(projection::<i32>(&[], None, None), Vec::<usize>::new());
295 }
296
297 #[test]
298 fn filter_keeps_source_indices() {
299 let even = |n: &i32| n % 2 == 0;
300 let order = projection(&[1, 2, 3, 4, 5, 6], Some(&even), None);
301 assert_eq!(order, vec![1, 3, 5]);
302 }
303
304 #[test]
305 fn sort_orders_indices_without_moving_items() {
306 let cmp = |a: &i32, b: &i32| a.cmp(b);
307 let items = [30, 10, 20];
308 let order = projection(&items, None, Some(&cmp));
309 assert_eq!(order, vec![1, 2, 0]);
310 assert_eq!(items, [30, 10, 20]);
312 }
313
314 #[test]
315 fn filter_then_sort_compose() {
316 let over_two = |n: &i32| *n > 2;
317 let desc = |a: &i32, b: &i32| b.cmp(a);
318 let order = projection(&[1, 4, 3, 2, 5], Some(&over_two), Some(&desc));
319 assert_eq!(order, vec![4, 1, 2]); }
321
322 #[test]
323 fn sort_is_stable_for_equal_keys() {
324 let by_len = |a: &&str, b: &&str| a.len().cmp(&b.len());
325 let items = ["bb", "aa", "c", "dd"];
326 let order = projection(&items, None, Some(&by_len));
327 assert_eq!(order, vec![2, 0, 1, 3]);
329 }
330}