Files
Ely-GPUI-Components/src/tables/header.rs
T

286 lines
8.3 KiB
Rust

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"])
);
}
}