use std::cmp::Ordering;
use gpui::prelude::*;
use gpui::{div, px, AnyElement, App, Context, EventEmitter, IntoElement, SharedString, Window};
use super::Content;
use crate::reactive::Signal;
use crate::style::{surface, Variant};
use crate::theme::{theme, Size};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataViewEvent {
Selected(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DataViewLayout {
#[default]
List,
Grid(usize),
}
type ItemBuilder<T> = Box<dyn Fn(&T, usize, &mut Window, &mut App) -> AnyElement + 'static>;
type FilterFn<T> = Box<dyn Fn(&T) -> bool + 'static>;
type SortFn<T> = Box<dyn Fn(&T, &T) -> Ordering + 'static>;
type FilterRef<'a, T> = Option<&'a dyn Fn(&T) -> bool>;
type SortRef<'a, T> = Option<&'a dyn Fn(&T, &T) -> Ordering>;
pub struct DataView<T: 'static> {
source: Signal<Vec<T>>,
item: Option<ItemBuilder<T>>,
filter: Option<FilterFn<T>>,
sort: Option<SortFn<T>>,
layout: DataViewLayout,
gap: Size,
empty: Option<Content>,
selectable: bool,
selected: Option<usize>,
}
impl<T: 'static> EventEmitter<DataViewEvent> for DataView<T> {}
impl<T: 'static> DataView<T> {
pub fn new(cx: &mut Context<Self>, source: &Signal<Vec<T>>) -> Self {
cx.observe(source.entity(), |this, source, cx| {
let len = source.read(cx).len();
if this.selected.is_some_and(|i| i >= len) {
this.selected = None;
}
cx.notify();
})
.detach();
DataView {
source: source.clone(),
item: None,
filter: None,
sort: None,
layout: DataViewLayout::List,
gap: Size::Sm,
empty: None,
selectable: false,
selected: None,
}
}
pub fn item<E>(
mut self,
template: impl Fn(&T, usize, &mut Window, &mut App) -> E + 'static,
) -> Self
where
E: IntoElement,
{
self.item = Some(Box::new(move |item, ix, window, cx| {
template(item, ix, window, cx).into_any_element()
}));
self
}
pub fn filter(mut self, pred: impl Fn(&T) -> bool + 'static) -> Self {
self.filter = Some(Box::new(pred));
self
}
pub fn sort_by(mut self, cmp: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
self.sort = Some(Box::new(cmp));
self
}
pub fn layout(mut self, layout: DataViewLayout) -> Self {
self.layout = layout;
self
}
pub fn gap(mut self, gap: Size) -> Self {
self.gap = gap;
self
}
pub fn empty<E>(mut self, content: impl Fn(&mut Window, &mut App) -> E + 'static) -> Self
where
E: IntoElement,
{
self.empty = Some(Box::new(move |window, cx| {
content(window, cx).into_any_element()
}));
self
}
pub fn selectable(mut self) -> Self {
self.selectable = true;
self
}
pub fn selected_index(&self) -> Option<usize> {
self.selected
}
}
fn projection<T>(items: &[T], filter: FilterRef<'_, T>, sort: SortRef<'_, T>) -> Vec<usize> {
let mut order: Vec<usize> = (0..items.len())
.filter(|&i| filter.is_none_or(|keep| keep(&items[i])))
.collect();
if let Some(cmp) = sort {
order.sort_by(|&a, &b| cmp(&items[a], &items[b]));
}
order
}
impl<T: 'static> Render for DataView<T> {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let t = theme(cx);
let gap = t.spacing(self.gap);
let radius = t.radius(t.default_radius);
let hover_bg = t.surface_hover().hsla();
let dimmed = t.dimmed().hsla();
let font_sm = t.font_size(Size::Sm);
let sel = surface(t, t.primary_color, Variant::Light);
let (selected_bg, selected_fg) = (sel.bg, sel.fg);
let template = self.item.as_ref();
let filter = self.filter.as_deref();
let sort = self.sort.as_deref();
let entity = self.source.entity().clone();
let built: Vec<(usize, AnyElement)> = entity.update(cx, |items, cx| {
let order = projection(items, filter, sort);
match template {
Some(build) => order
.into_iter()
.map(|i| (i, build(&items[i], i, window, cx)))
.collect(),
None => Vec::new(),
}
});
let selectable = self.selectable;
let selected = self.selected;
if built.is_empty() {
let content = match &self.empty {
Some(build) => build(window, cx),
None => div()
.text_size(px(font_sm))
.text_color(dimmed)
.child(SharedString::new_static("Nothing to show"))
.into_any_element(),
};
return div()
.w_full()
.flex()
.justify_center()
.py(px(16.0))
.child(content);
}
let cells: Vec<AnyElement> = built
.into_iter()
.map(|(source_ix, element)| {
if !selectable {
return element;
}
let is_selected = selected == Some(source_ix);
let mut cell = div()
.id(("guise-dataview-item", source_ix))
.px(px(10.0))
.py(px(8.0))
.rounded(px(radius))
.cursor_pointer()
.child(element)
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.selected = Some(source_ix);
cx.emit(DataViewEvent::Selected(source_ix));
cx.notify();
}));
cell = if is_selected {
cell.bg(selected_bg).text_color(selected_fg)
} else {
cell.hover(move |s| s.bg(hover_bg))
};
cell.into_any_element()
})
.collect();
let root = div().w_full().flex().flex_col().gap(px(gap));
match self.layout {
DataViewLayout::List => root.children(cells),
DataViewLayout::Grid(cols) => {
let cols = cols.max(1);
let count = cells.len();
let mut rows = Vec::new();
let mut row = Vec::new();
for (i, cell) in cells.into_iter().enumerate() {
row.push(div().flex_1().min_w(px(0.0)).child(cell));
if row.len() == cols || i + 1 == count {
while row.len() < cols {
row.push(div().flex_1().min_w(px(0.0)));
}
rows.push(div().flex().gap(px(gap)).children(std::mem::take(&mut row)));
}
}
root.children(rows)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_without_projections() {
assert_eq!(projection(&[10, 20, 30], None, None), vec![0, 1, 2]);
assert_eq!(projection::<i32>(&[], None, None), Vec::<usize>::new());
}
#[test]
fn filter_keeps_source_indices() {
let even = |n: &i32| n % 2 == 0;
let order = projection(&[1, 2, 3, 4, 5, 6], Some(&even), None);
assert_eq!(order, vec![1, 3, 5]);
}
#[test]
fn sort_orders_indices_without_moving_items() {
let cmp = |a: &i32, b: &i32| a.cmp(b);
let items = [30, 10, 20];
let order = projection(&items, None, Some(&cmp));
assert_eq!(order, vec![1, 2, 0]);
assert_eq!(items, [30, 10, 20]);
}
#[test]
fn filter_then_sort_compose() {
let over_two = |n: &i32| *n > 2;
let desc = |a: &i32, b: &i32| b.cmp(a);
let order = projection(&[1, 4, 3, 2, 5], Some(&over_two), Some(&desc));
assert_eq!(order, vec![4, 1, 2]); }
#[test]
fn sort_is_stable_for_equal_keys() {
let by_len = |a: &&str, b: &&str| a.len().cmp(&b.len());
let items = ["bb", "aa", "c", "dd"];
let order = projection(&items, None, Some(&by_len));
assert_eq!(order, vec![2, 0, 1, 3]);
}
}