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, Window,
35};
36
37use super::Content;
38use crate::devtools::ProbedAny;
39use crate::reactive::Signal;
40use crate::style::{surface, Variant};
41use crate::theme::{theme, Size};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum DataViewEvent {
48 Selected(usize),
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum DataViewLayout {
54 #[default]
56 List,
57 Grid(usize),
59}
60
61type ItemBuilder<T> = Box<dyn Fn(&T, usize, &mut Window, &mut App) -> AnyElement + 'static>;
62type FilterFn<T> = Box<dyn Fn(&T) -> bool + 'static>;
63type SortFn<T> = Box<dyn Fn(&T, &T) -> Ordering + 'static>;
64type FilterRef<'a, T> = Option<&'a dyn Fn(&T) -> bool>;
65type SortRef<'a, T> = Option<&'a dyn Fn(&T, &T) -> Ordering>;
66
67pub struct DataView<T: 'static> {
70 source: Signal<Vec<T>>,
71 item: Option<ItemBuilder<T>>,
72 filter: Option<FilterFn<T>>,
73 sort: Option<SortFn<T>>,
74 layout: DataViewLayout,
75 gap: Size,
76 empty: Option<Content>,
77 selectable: bool,
78 selected: Option<usize>,
79 height: Option<f32>,
80}
81
82impl<T: 'static> EventEmitter<DataViewEvent> for DataView<T> {}
83
84impl<T: 'static> DataView<T> {
85 pub fn new(cx: &mut Context<Self>, source: &Signal<Vec<T>>) -> Self {
88 cx.observe(source.entity(), |this, source, cx| {
89 let len = source.read(cx).len();
93 if this.selected.is_some_and(|i| i >= len) {
94 this.selected = None;
95 }
96 cx.notify();
97 })
98 .detach();
99 DataView {
100 source: source.clone(),
101 item: None,
102 filter: None,
103 sort: None,
104 layout: DataViewLayout::List,
105 gap: Size::Sm,
106 empty: None,
107 selectable: false,
108 selected: None,
109 height: None,
110 }
111 }
112
113 pub fn height(mut self, height: f32) -> Self {
117 self.height = Some(height.max(0.0));
118 self
119 }
120
121 pub fn item<E>(
124 mut self,
125 template: impl Fn(&T, usize, &mut Window, &mut App) -> E + 'static,
126 ) -> Self
127 where
128 E: IntoElement,
129 {
130 self.item = Some(Box::new(move |item, ix, window, cx| {
131 template(item, ix, window, cx).into_any_element()
132 }));
133 self
134 }
135
136 pub fn filter(mut self, pred: impl Fn(&T) -> bool + 'static) -> Self {
139 self.filter = Some(Box::new(pred));
140 self
141 }
142
143 pub fn sort_by(mut self, cmp: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
146 self.sort = Some(Box::new(cmp));
147 self
148 }
149
150 pub fn layout(mut self, layout: DataViewLayout) -> Self {
151 self.layout = layout;
152 self
153 }
154
155 pub fn gap(mut self, gap: Size) -> Self {
157 self.gap = gap;
158 self
159 }
160
161 pub fn empty<E>(mut self, content: impl Fn(&mut Window, &mut App) -> E + 'static) -> Self
164 where
165 E: IntoElement,
166 {
167 self.empty = Some(Box::new(move |window, cx| {
168 content(window, cx).into_any_element()
169 }));
170 self
171 }
172
173 pub fn selectable(mut self) -> Self {
176 self.selectable = true;
177 self
178 }
179
180 pub fn selected_index(&self) -> Option<usize> {
182 self.selected
183 }
184}
185
186fn projection<T>(items: &[T], filter: FilterRef<'_, T>, sort: SortRef<'_, T>) -> Vec<usize> {
188 let mut order: Vec<usize> = (0..items.len())
189 .filter(|&i| filter.is_none_or(|keep| keep(&items[i])))
190 .collect();
191 if let Some(cmp) = sort {
192 order.sort_by(|&a, &b| cmp(&items[a], &items[b]));
193 }
194 order
195}
196
197impl<T: 'static> DataView<T> {
198 fn build_cells(
202 &mut self,
203 display: Range<usize>,
204 window: &mut Window,
205 cx: &mut Context<Self>,
206 ) -> Vec<AnyElement> {
207 let t = theme(cx);
208 let radius = t.radius(t.default_radius);
209 let hover_bg = t.surface_hover().hsla();
210 let sel = surface(t, t.primary_color, Variant::Light);
213 let (selected_bg, selected_fg) = (sel.bg, sel.fg);
214
215 let template = self.item.as_ref();
216 let filter = self.filter.as_deref();
217 let sort = self.sort.as_deref();
218 let entity = self.source.entity().clone();
219 let built: Vec<(usize, AnyElement)> = entity.update(cx, |items, cx| {
220 let order = projection(items, filter, sort);
221 match template {
222 Some(build) => order
223 .into_iter()
224 .skip(display.start)
225 .take(display.len())
226 .map(|i| (i, build(&items[i], i, window, cx)))
227 .collect(),
228 None => Vec::new(),
229 }
230 });
231
232 let selectable = self.selectable;
233 let selected = self.selected;
236
237 built
238 .into_iter()
239 .map(|(source_ix, element)| {
240 if !selectable {
241 return element;
242 }
243 let is_selected = selected == Some(source_ix);
244 let mut cell = div()
245 .id(("guise-dataview-item", source_ix))
246 .px(px(10.0))
247 .py(px(8.0))
248 .rounded(px(radius))
249 .cursor_pointer()
250 .child(element)
251 .on_click(cx.listener(move |this, _ev, _window, cx| {
252 this.selected = Some(source_ix);
253 cx.emit(DataViewEvent::Selected(source_ix));
254 cx.notify();
255 }));
256 cell = if is_selected {
257 cell.bg(selected_bg).text_color(selected_fg)
258 } else {
259 cell.hover(move |s| s.bg(hover_bg))
260 };
261 cell.into_any_element()
262 })
263 .collect()
264 }
265
266 fn build_grid_row(
268 &mut self,
269 row_ix: usize,
270 cols: usize,
271 gap: f32,
272 window: &mut Window,
273 cx: &mut Context<Self>,
274 ) -> AnyElement {
275 let start = row_ix * cols;
276 let cells = self.build_cells(start..start + cols, window, cx);
277 let mut wrapped: Vec<_> = cells
278 .into_iter()
279 .map(|cell| div().flex_1().min_w(px(0.0)).child(cell))
280 .collect();
281 while wrapped.len() < cols {
282 wrapped.push(div().flex_1().min_w(px(0.0)));
283 }
284 div()
285 .flex()
286 .gap(px(gap))
287 .pb(px(gap))
288 .children(wrapped)
289 .into_any_element()
290 }
291
292 fn projected_len(&mut self, cx: &mut Context<Self>) -> usize {
294 let filter = self.filter.as_deref();
295 let sort = self.sort.as_deref();
296 let entity = self.source.entity().clone();
297 entity.update(cx, |items, _| projection(items, filter, sort).len())
298 }
299}
300
301impl<T: 'static> Render for DataView<T> {
302 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
303 let t = theme(cx);
304 let gap = t.spacing(self.gap);
305 let dimmed = t.dimmed().hsla();
306 let font_sm = t.font_size(Size::Sm);
307
308 let count = if self.item.is_some() {
309 self.projected_len(cx)
310 } else {
311 0
312 };
313
314 if count == 0 {
315 let content = match &self.empty {
316 Some(build) => build(window, cx),
317 None => div()
318 .text_size(px(font_sm))
319 .text_color(dimmed)
320 .child(SharedString::new_static("Nothing to show"))
321 .into_any_element(),
322 };
323 return div()
324 .w_full()
325 .flex()
326 .justify_center()
327 .py(px(16.0))
328 .child(content)
329 .into_any_element();
330 }
331
332 if let Some(height) = self.height {
335 let list = match self.layout {
336 DataViewLayout::List => uniform_list(
337 "guise-dataview-body",
338 count,
339 cx.processor(move |this, range: Range<usize>, window, cx| {
340 this
341 .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}