1use std::cmp::Ordering;
30use std::ops::Range;
31
32use gpui::prelude::*;
33use gpui::{
34 div, px, uniform_list, AnyElement, App, Context, EventEmitter, IntoElement, SharedString,
35 Window,
36};
37
38use super::Content;
39use crate::devtools::ProbedAny;
40use crate::reactive::Signal;
41use crate::style::{surface, Variant};
42use crate::theme::{theme, Size};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DataViewEvent {
49 Selected(usize),
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub enum DataViewLayout {
55 #[default]
57 List,
58 Grid(usize),
60}
61
62type ItemBuilder<T> = Box<dyn Fn(&T, usize, &mut Window, &mut App) -> AnyElement + 'static>;
63type FilterFn<T> = Box<dyn Fn(&T) -> bool + 'static>;
64type SortFn<T> = Box<dyn Fn(&T, &T) -> Ordering + 'static>;
65type FilterRef<'a, T> = Option<&'a dyn Fn(&T) -> bool>;
66type SortRef<'a, T> = Option<&'a dyn Fn(&T, &T) -> Ordering>;
67
68pub struct DataView<T: 'static> {
71 source: Signal<Vec<T>>,
72 item: Option<ItemBuilder<T>>,
73 filter: Option<FilterFn<T>>,
74 sort: Option<SortFn<T>>,
75 layout: DataViewLayout,
76 gap: Size,
77 empty: Option<Content>,
78 selectable: bool,
79 selected: Option<usize>,
80 height: Option<f32>,
81}
82
83impl<T: 'static> EventEmitter<DataViewEvent> for DataView<T> {}
84
85impl<T: 'static> DataView<T> {
86 pub fn new(cx: &mut Context<Self>, source: &Signal<Vec<T>>) -> Self {
89 cx.observe(source.entity(), |this, source, cx| {
90 let len = source.read(cx).len();
94 if this.selected.is_some_and(|i| i >= len) {
95 this.selected = None;
96 }
97 cx.notify();
98 })
99 .detach();
100 DataView {
101 source: source.clone(),
102 item: None,
103 filter: None,
104 sort: None,
105 layout: DataViewLayout::List,
106 gap: Size::Sm,
107 empty: None,
108 selectable: false,
109 selected: None,
110 height: None,
111 }
112 }
113
114 pub fn height(mut self, height: f32) -> Self {
118 self.height = Some(height.max(0.0));
119 self
120 }
121
122 pub fn item<E>(
125 mut self,
126 template: impl Fn(&T, usize, &mut Window, &mut App) -> E + 'static,
127 ) -> Self
128 where
129 E: IntoElement,
130 {
131 self.item = Some(Box::new(move |item, ix, window, cx| {
132 template(item, ix, window, cx).into_any_element()
133 }));
134 self
135 }
136
137 pub fn filter(mut self, pred: impl Fn(&T) -> bool + 'static) -> Self {
140 self.filter = Some(Box::new(pred));
141 self
142 }
143
144 pub fn sort_by(mut self, cmp: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
147 self.sort = Some(Box::new(cmp));
148 self
149 }
150
151 pub fn layout(mut self, layout: DataViewLayout) -> Self {
152 self.layout = layout;
153 self
154 }
155
156 pub fn gap(mut self, gap: Size) -> Self {
158 self.gap = gap;
159 self
160 }
161
162 pub fn empty<E>(mut self, content: impl Fn(&mut Window, &mut App) -> E + 'static) -> Self
165 where
166 E: IntoElement,
167 {
168 self.empty = Some(Box::new(move |window, cx| {
169 content(window, cx).into_any_element()
170 }));
171 self
172 }
173
174 pub fn selectable(mut self) -> Self {
177 self.selectable = true;
178 self
179 }
180
181 pub fn selected_index(&self) -> Option<usize> {
183 self.selected
184 }
185}
186
187fn projection<T>(items: &[T], filter: FilterRef<'_, T>, sort: SortRef<'_, T>) -> Vec<usize> {
189 let mut order: Vec<usize> = (0..items.len())
190 .filter(|&i| filter.is_none_or(|keep| keep(&items[i])))
191 .collect();
192 if let Some(cmp) = sort {
193 order.sort_by(|&a, &b| cmp(&items[a], &items[b]));
194 }
195 order
196}
197
198impl<T: 'static> DataView<T> {
199 fn build_cells(
203 &mut self,
204 display: Range<usize>,
205 window: &mut Window,
206 cx: &mut Context<Self>,
207 ) -> Vec<AnyElement> {
208 let t = theme(cx);
209 let radius = t.radius(t.default_radius);
210 let hover_bg = t.surface_hover().hsla();
211 let sel = surface(t, t.primary_color, Variant::Light);
214 let (selected_bg, selected_fg) = (sel.bg, sel.fg);
215
216 let template = self.item.as_ref();
217 let filter = self.filter.as_deref();
218 let sort = self.sort.as_deref();
219 let entity = self.source.entity().clone();
220 let built: Vec<(usize, AnyElement)> = entity.update(cx, |items, cx| {
221 let order = projection(items, filter, sort);
222 match template {
223 Some(build) => order
224 .into_iter()
225 .skip(display.start)
226 .take(display.len())
227 .map(|i| (i, build(&items[i], i, window, cx)))
228 .collect(),
229 None => Vec::new(),
230 }
231 });
232
233 let selectable = self.selectable;
234 let selected = self.selected;
237
238 built
239 .into_iter()
240 .map(|(source_ix, element)| {
241 if !selectable {
242 return element;
243 }
244 let is_selected = selected == Some(source_ix);
245 let mut cell = div()
246 .id(("guise-dataview-item", source_ix))
247 .px(px(10.0))
248 .py(px(8.0))
249 .rounded(px(radius))
250 .cursor_pointer()
251 .child(element)
252 .on_click(cx.listener(move |this, _ev, _window, cx| {
253 this.selected = Some(source_ix);
254 cx.emit(DataViewEvent::Selected(source_ix));
255 cx.notify();
256 }));
257 cell = if is_selected {
258 cell.bg(selected_bg).text_color(selected_fg)
259 } else {
260 cell.hover(move |s| s.bg(hover_bg))
261 };
262 cell.into_any_element()
263 })
264 .collect()
265 }
266
267 fn build_grid_row(
269 &mut self,
270 row_ix: usize,
271 cols: usize,
272 gap: f32,
273 window: &mut Window,
274 cx: &mut Context<Self>,
275 ) -> AnyElement {
276 let start = row_ix * cols;
277 let cells = self.build_cells(start..start + cols, window, cx);
278 let mut wrapped: Vec<_> = cells
279 .into_iter()
280 .map(|cell| div().flex_1().min_w(px(0.0)).child(cell))
281 .collect();
282 while wrapped.len() < cols {
283 wrapped.push(div().flex_1().min_w(px(0.0)));
284 }
285 div()
286 .flex()
287 .gap(px(gap))
288 .pb(px(gap))
289 .children(wrapped)
290 .into_any_element()
291 }
292
293 fn projected_len(&mut self, cx: &mut Context<Self>) -> usize {
295 let filter = self.filter.as_deref();
296 let sort = self.sort.as_deref();
297 let entity = self.source.entity().clone();
298 entity.update(cx, |items, _| projection(items, filter, sort).len())
299 }
300}
301
302impl<T: 'static> Render for DataView<T> {
303 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
304 let t = theme(cx);
305 let gap = t.spacing(self.gap);
306 let dimmed = t.dimmed().hsla();
307 let font_sm = t.font_size(Size::Sm);
308
309 let count = if self.item.is_some() {
310 self.projected_len(cx)
311 } else {
312 0
313 };
314
315 if count == 0 {
316 let content = match &self.empty {
317 Some(build) => build(window, cx),
318 None => div()
319 .text_size(px(font_sm))
320 .text_color(dimmed)
321 .child(SharedString::new_static("Nothing to show"))
322 .into_any_element(),
323 };
324 return div()
325 .w_full()
326 .flex()
327 .justify_center()
328 .py(px(16.0))
329 .child(content)
330 .into_any_element();
331 }
332
333 if let Some(height) = self.height {
336 let list = match self.layout {
337 DataViewLayout::List => uniform_list(
338 "guise-dataview-body",
339 count,
340 cx.processor(move |this, range: Range<usize>, window, cx| {
341 this.build_cells(range, window, cx)
342 .into_iter()
343 .map(|cell| div().pb(px(gap)).child(cell).into_any_element())
344 .collect::<Vec<_>>()
345 }),
346 ),
347 DataViewLayout::Grid(cols) => {
348 let cols = cols.max(1);
349 let rows = count.div_ceil(cols);
350 uniform_list(
351 "guise-dataview-body",
352 rows,
353 cx.processor(move |this, range: Range<usize>, window, cx| {
354 range
355 .map(|row_ix| this.build_grid_row(row_ix, cols, gap, window, cx))
356 .collect::<Vec<_>>()
357 }),
358 )
359 }
360 };
361 return div()
362 .w_full()
363 .child(list.h(px(height)).w_full())
364 .into_any_element();
365 }
366
367 let cells = self.build_cells(0..count, window, cx);
368 let root = div().w_full().flex().flex_col().gap(px(gap));
369
370 let element = match self.layout {
371 DataViewLayout::List => root.children(cells).into_any_element(),
372 DataViewLayout::Grid(cols) => {
373 let cols = cols.max(1);
374 let mut rows = Vec::new();
375 let mut row = Vec::new();
376 for (i, cell) in cells.into_iter().enumerate() {
377 row.push(div().flex_1().min_w(px(0.0)).child(cell));
378 if row.len() == cols || i + 1 == count {
379 while row.len() < cols {
381 row.push(div().flex_1().min_w(px(0.0)));
382 }
383 rows.push(div().flex().gap(px(gap)).children(std::mem::take(&mut row)));
384 }
385 }
386 root.children(rows).into_any_element()
387 }
388 };
389
390 element.probe_any("DataView").into_any_element()
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn identity_without_projections() {
400 assert_eq!(projection(&[10, 20, 30], None, None), vec![0, 1, 2]);
401 assert_eq!(projection::<i32>(&[], None, None), Vec::<usize>::new());
402 }
403
404 #[test]
405 fn filter_keeps_source_indices() {
406 let even = |n: &i32| n % 2 == 0;
407 let order = projection(&[1, 2, 3, 4, 5, 6], Some(&even), None);
408 assert_eq!(order, vec![1, 3, 5]);
409 }
410
411 #[test]
412 fn sort_orders_indices_without_moving_items() {
413 let cmp = |a: &i32, b: &i32| a.cmp(b);
414 let items = [30, 10, 20];
415 let order = projection(&items, None, Some(&cmp));
416 assert_eq!(order, vec![1, 2, 0]);
417 assert_eq!(items, [30, 10, 20]);
419 }
420
421 #[test]
422 fn filter_then_sort_compose() {
423 let over_two = |n: &i32| *n > 2;
424 let desc = |a: &i32, b: &i32| b.cmp(a);
425 let order = projection(&[1, 4, 3, 2, 5], Some(&over_two), Some(&desc));
426 assert_eq!(order, vec![4, 1, 2]); }
428
429 #[test]
430 fn sort_is_stable_for_equal_keys() {
431 let by_len = |a: &&str, b: &&str| a.len().cmp(&b.len());
432 let items = ["bb", "aa", "c", "dd"];
433 let order = projection(&items, None, Some(&by_len));
434 assert_eq!(order, vec![2, 0, 1, 3]);
436 }
437}