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 { let mut next: Vec = 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, sorting: &Entity>>, column: &Column, width: Option, 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, 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, _, 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 { 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"]) ); } }