T14b Tables: resize, reorder, visibility, pinning, expansion, grouping, inline edit, toolbar, filter and sort builders (review pending: codex 401)
This commit is contained in:
17 files changed
+2292
-420
No files matched your search
@@ -0,0 +1,285 @@
|
||||
use gpui::{
|
||||
App, DragMoveEvent, ElementId, Entity, EntityId, FontWeight, InteractiveElement, IntoElement,
|
||||
ParentElement, Pixels, Rems, SharedString, StatefulInteractiveElement, Styled, canvas, div,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use super::{Column, body::sized, table::View};
|
||||
use crate::layout::seeded::Seeded;
|
||||
use crate::{
|
||||
primitives::{DragGhost, Icon, IconName},
|
||||
theme::{ActiveTheme, IconSize, TextSize},
|
||||
};
|
||||
|
||||
/// A header's right edge being dragged, to resize its column.
|
||||
pub(crate) struct Resize {
|
||||
owner: EntityId,
|
||||
key: SharedString,
|
||||
}
|
||||
|
||||
/// A header being dragged, to move its column.
|
||||
pub(crate) struct Move {
|
||||
owner: EntityId,
|
||||
key: SharedString,
|
||||
}
|
||||
|
||||
/// The sort after a press on column `key`: alone it replaces the sort, with Shift it joins it; each press turns it rising, falling, then off.
|
||||
pub(crate) fn next_sort(
|
||||
sort: &[(SharedString, bool)],
|
||||
key: &SharedString,
|
||||
join: bool,
|
||||
) -> Vec<(SharedString, bool)> {
|
||||
let now = sort
|
||||
.iter()
|
||||
.find(|(known, _)| known == key)
|
||||
.map(|(_, rising)| *rising);
|
||||
let turned = match now {
|
||||
None => Some(true),
|
||||
Some(true) => Some(false),
|
||||
Some(false) => None,
|
||||
};
|
||||
let mut next: Vec<(SharedString, bool)> = if join {
|
||||
sort.iter()
|
||||
.filter(|(known, _)| known != key)
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let at = if join {
|
||||
sort.iter()
|
||||
.position(|(known, _)| known == key)
|
||||
.unwrap_or(next.len())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Some(rising) = turned {
|
||||
next.insert(at.min(next.len()), (key.clone(), rising));
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
/// The column order after moving `key` in front of `target`.
|
||||
pub(crate) fn moved(
|
||||
order: &[SharedString],
|
||||
key: &SharedString,
|
||||
target: &SharedString,
|
||||
) -> Vec<SharedString> {
|
||||
let mut next: Vec<SharedString> = order
|
||||
.iter()
|
||||
.filter(|known| *known != key)
|
||||
.cloned()
|
||||
.collect();
|
||||
let at = next
|
||||
.iter()
|
||||
.position(|known| known == target)
|
||||
.unwrap_or(next.len());
|
||||
next.insert(at, key.clone());
|
||||
next
|
||||
}
|
||||
|
||||
/// One header cell: its title and sort mark; a press sorts, a drag moves it, and its right edge resizes.
|
||||
pub(crate) fn header(
|
||||
id: &ElementId,
|
||||
view: &Entity<View>,
|
||||
sorting: &Entity<Seeded<Vec<(SharedString, bool)>>>,
|
||||
column: &Column,
|
||||
width: Option<Pixels>,
|
||||
narrowest: Rems,
|
||||
cx: &App,
|
||||
) -> impl IntoElement + use<> {
|
||||
let sort = sorting.read(cx).value.clone();
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let owner = view.entity_id();
|
||||
let place = sort.iter().position(|(known, _)| *known == column.key);
|
||||
let mark = place.map(|place| (sort[place].1, (sort.len() > 1).then_some(place + 1)));
|
||||
let key = column.key.clone();
|
||||
let (sorter, pager, mover, sizer, edges) = (
|
||||
sorting.clone(),
|
||||
view.clone(),
|
||||
view.clone(),
|
||||
view.clone(),
|
||||
view.clone(),
|
||||
);
|
||||
let title = column.title.clone();
|
||||
let cell = div()
|
||||
.id((id.clone(), format!("head-{key}")))
|
||||
.relative()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.px_3()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(colors.fg_muted);
|
||||
let cell = match width {
|
||||
Some(width) => cell.w(width).flex_none(),
|
||||
None => sized(cell, column, narrowest),
|
||||
};
|
||||
cell.when(column.sortable, |cell| {
|
||||
let key = key.clone();
|
||||
cell.cursor_pointer()
|
||||
.hover(|style| style.text_color(colors.fg))
|
||||
.on_click(move |event, _, cx| {
|
||||
let join = event.modifiers().shift;
|
||||
sorter.update(cx, |sorting, cx| {
|
||||
sorting.value = next_sort(&sorting.value, &key, join);
|
||||
log::info!("data table: sort {:?}", sorting.value);
|
||||
cx.notify();
|
||||
});
|
||||
pager.update(cx, |view, _| view.page = 0);
|
||||
})
|
||||
})
|
||||
.on_drag(
|
||||
Move {
|
||||
owner,
|
||||
key: key.clone(),
|
||||
},
|
||||
move |_, _, _, cx| DragGhost::new(title.clone(), None, cx),
|
||||
)
|
||||
.on_drop({
|
||||
let key = key.clone();
|
||||
move |drag: &Move, _, cx| {
|
||||
if drag.owner != owner || drag.key == key {
|
||||
return;
|
||||
}
|
||||
mover.update(cx, |view, cx| {
|
||||
view.order = moved(&view.order, &drag.key, &key);
|
||||
log::info!("data table: moved {} before {key}", drag.key);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
})
|
||||
.child(column.title.clone())
|
||||
.children(mark.map(|(rising, rank)| {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(
|
||||
Icon::new(if rising {
|
||||
IconName::ArrowUp
|
||||
} else {
|
||||
IconName::ArrowDown
|
||||
})
|
||||
.size(IconSize::Xs)
|
||||
.color(colors.fg_muted),
|
||||
)
|
||||
.children(rank.map(|rank| {
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.child(rank.to_string())
|
||||
}))
|
||||
}))
|
||||
.child(grip(id, &key, owner, sizer, theme.handle_hit(), cx))
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, _, cx| {
|
||||
if edges.read(cx).edges.get(&key) != Some(&bounds.left()) {
|
||||
edges.update(cx, |view, _| view.edges.insert(key.clone(), bounds.left()));
|
||||
}
|
||||
},
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The resize grip on a header's right edge.
|
||||
fn grip(
|
||||
id: &ElementId,
|
||||
key: &SharedString,
|
||||
owner: EntityId,
|
||||
view: Entity<View>,
|
||||
hit: Rems,
|
||||
cx: &App,
|
||||
) -> impl IntoElement + use<> {
|
||||
let border = cx.theme().colors.border_strong;
|
||||
let key = key.clone();
|
||||
div()
|
||||
.id((id.clone(), format!("grip-{key}")))
|
||||
.absolute()
|
||||
.top_1()
|
||||
.bottom_1()
|
||||
.right_0()
|
||||
.w(hit)
|
||||
.flex()
|
||||
.justify_center()
|
||||
.cursor_col_resize()
|
||||
.hover(|style| style.bg(border.opacity(0.2)))
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.border_r_1()
|
||||
.border_color(border.opacity(0.5)),
|
||||
)
|
||||
.on_drag(
|
||||
Resize {
|
||||
owner,
|
||||
key: key.clone(),
|
||||
},
|
||||
|_, _, _, cx| cx.new(|_| gpui::EmptyView),
|
||||
)
|
||||
.on_drag_move(move |event: &DragMoveEvent<Resize>, _, cx| {
|
||||
let drag = event.drag(cx);
|
||||
if drag.owner != owner || drag.key != key {
|
||||
return;
|
||||
}
|
||||
let Some(left) = view.read(cx).edges.get(&key).copied() else {
|
||||
return;
|
||||
};
|
||||
let wide = (event.event.position.x - left).max(view.read(cx).narrowest);
|
||||
view.update(cx, |view, cx| {
|
||||
view.widths.insert(key.clone(), wide);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::SharedString;
|
||||
|
||||
use super::{moved, next_sort};
|
||||
|
||||
fn keys(list: &[&'static str]) -> Vec<SharedString> {
|
||||
list.iter().map(|key| SharedString::from(*key)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_replaces_the_sort_and_shift_joins_it() {
|
||||
let a = SharedString::from("a");
|
||||
let b = SharedString::from("b");
|
||||
assert_eq!(next_sort(&[], &a, false), [(a.clone(), true)]);
|
||||
assert_eq!(
|
||||
next_sort(&[(a.clone(), true)], &b, true),
|
||||
[(a.clone(), true), (b.clone(), true)]
|
||||
);
|
||||
assert_eq!(
|
||||
next_sort(&[(a.clone(), true), (b.clone(), true)], &a, true),
|
||||
[(a.clone(), false), (b.clone(), true)]
|
||||
);
|
||||
assert_eq!(
|
||||
next_sort(&[(a.clone(), false), (b.clone(), true)], &a, true),
|
||||
[(b.clone(), true)]
|
||||
);
|
||||
assert_eq!(
|
||||
next_sort(&[(a.clone(), true), (b.clone(), true)], &b, false),
|
||||
[(b, false)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_column_moves_in_front_of_its_target() {
|
||||
assert_eq!(
|
||||
moved(&keys(&["a", "b", "c"]), &"c".into(), &"a".into()),
|
||||
keys(&["c", "a", "b"])
|
||||
);
|
||||
assert_eq!(
|
||||
moved(&keys(&["a", "b", "c"]), &"a".into(), &"c".into()),
|
||||
keys(&["b", "a", "c"])
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user