use fluidattacks_designs_domain::icon::Style as IconStyle;
use fluidattacks_designs_domain::table::{
Sort, BORDER_PX, CELL_PADDING_X, HEADER_BG, HEADER_DIVIDER, HEADER_DIVIDER_BESIDE_SELECTION,
HEADER_HEIGHT, HEADER_HOVER_BG, HEADER_TEXT, HEADER_TEXT_SIZE, HEADER_WEIGHT, RESIZER_BORDER,
RESIZER_WIDTH, SELECTION_PADDING, SELECTION_WIDTH, SORT_ASC_ICON, SORT_DESC_ICON, SORT_GLYPH,
SORT_ICON_COLOR, SORT_ICON_GAP, SORT_ICON_HEIGHT, SORT_ICON_WIDTH,
};
use fluidattacks_designs_domain::typography::FONT_PRIMARY;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, relative, rgb, App, AppContext as _, CursorStyle, Div, DragMoveEvent, Empty, Entity,
FontWeight, InteractiveElement as _, MouseButton, MouseDownEvent, ParentElement as _,
SharedString, Stateful, StatefulInteractiveElement as _, Styled as _,
};
use crate::checkbox::Checkbox;
use crate::icon::Icon;
use crate::table::state::TableState;
use crate::theme::{ActiveTheme as _, Theme};
#[derive(Clone)]
pub struct Resize;
#[derive(Clone)]
pub struct Reorder {
position: usize,
}
struct Cell {
position: usize,
column: usize,
label: SharedString,
width: f32,
sort: Sort,
divider: u32,
}
pub fn header_row(state: &Entity<TableState>, cx: &App) -> Div {
let read = state.read(cx);
let theme = cx.theme();
let (sorted, sort) = read.sort();
let divider = if read.checkboxes() {
HEADER_DIVIDER_BESIDE_SELECTION
} else {
HEADER_DIVIDER
};
let shown: Vec<Cell> = read
.order()
.iter()
.enumerate()
.filter_map(|(position, &column)| {
let entry = read.columns().get(column)?;
entry.visible.then(|| Cell {
position,
column,
label: entry.header.clone(),
width: entry.width,
sort: if column == sorted { sort } else { Sort::None },
divider,
})
})
.collect();
let mut row = div()
.flex()
.flex_none()
.w(px(read.row_width()))
.min_w(relative(1.0))
.bg(rgb(theme.shade(HEADER_BG)))
.children(
read.checkboxes()
.then(|| selection_head(state, read, theme)),
);
for cell in shown {
row = row.child(header_cell(state, cell, theme));
}
row
}
fn header_cell(state: &Entity<TableState>, cell: Cell, theme: &Theme) -> Stateful<Div> {
let hover_bg = theme.shade(HEADER_HOVER_BG);
let sorting = state.clone();
let resizing = state.clone();
let dropping = state.clone();
let Cell {
position,
column,
label,
width,
sort,
divider,
} = cell;
div()
.id(crate::table::part(state, format!("th-{column}")))
.relative()
.flex()
.flex_none()
.items_center()
.justify_between()
.gap(px(SORT_ICON_GAP))
.w(px(width))
.h(px(HEADER_HEIGHT))
.px(px(CELL_PADDING_X))
.when(position == 0, |head| {
head.border_r_1().border_color(rgb(theme.shade(divider)))
})
.hover(move |head| head.bg(rgb(hover_bg)))
.cursor_pointer()
.child(label_text(&label, theme))
.child(sort_indicator(sort, theme))
.child(resizer(state, column, theme))
.on_drag(Reorder { position }, |_, _, _, app| app.new(|_| Empty))
.on_drop(move |dragged: &Reorder, _window, app| {
let from = dragged.position;
dropping.update(app, |table, app| table.move_column(from, position, app));
})
.on_drag_move(move |event: &DragMoveEvent<Resize>, _window, app| {
let reached = f32::from(event.event.position.x);
resizing.update(app, |table, inner| table.drag_resize(reached, inner));
})
.on_click(move |_, _window, app| {
sorting.update(app, |table, app| table.cycle_sort(column, app));
})
}
fn selection_head(state: &Entity<TableState>, read: &TableState, theme: &Theme) -> Div {
let toggling = state.clone();
div()
.flex()
.flex_none()
.items_center()
.w(px(SELECTION_WIDTH))
.h(px(HEADER_HEIGHT))
.pl(px(SELECTION_PADDING))
.border_r_1()
.border_color(rgb(theme.shade(HEADER_DIVIDER)))
.when(read.multi(), |head| {
head.child(
Checkbox::new(
crate::table::part(state, "pick-shown"),
read.all_shown_selected(),
)
.on_change(move |_, _win, app| {
toggling.update(app, TableState::toggle_shown);
}),
)
})
}
fn label_text(label: &SharedString, theme: &Theme) -> Div {
div()
.overflow_hidden()
.font_family(FONT_PRIMARY)
.text_size(px(HEADER_TEXT_SIZE.px()))
.line_height(px(HEADER_TEXT_SIZE.line_height_px()))
.font_weight(FontWeight(f32::from(HEADER_WEIGHT.value())))
.text_color(rgb(theme.shade(HEADER_TEXT)))
.child(label.clone())
}
fn sort_indicator(sort: Sort, theme: &Theme) -> Div {
let (up, down) = sort.caret_opacity();
div()
.flex()
.flex_col()
.flex_none()
.items_center()
.justify_center()
.gap(px(SORT_ICON_GAP))
.w(px(SORT_ICON_WIDTH))
.h(px(SORT_ICON_HEIGHT))
.child(caret(SORT_ASC_ICON, up, theme))
.child(caret(SORT_DESC_ICON, down, theme))
}
fn caret(glyph: &'static str, opacity: f32, theme: &Theme) -> Icon {
let mut color = rgb(theme.shade(SORT_ICON_COLOR));
color.alpha = opacity;
Icon::new(glyph)
.style(IconStyle::Solid)
.size(SORT_GLYPH)
.color_rgba(color)
}
fn resizer(state: &Entity<TableState>, column: usize, theme: &Theme) -> Stateful<Div> {
let line = theme.shade(RESIZER_BORDER);
let starting = state.clone();
div()
.id(crate::table::part(state, format!("grip-{column}")))
.absolute()
.top_0()
.right(px(-BORDER_PX))
.h_full()
.w(px(RESIZER_WIDTH))
.cursor(CursorStyle::ResizeLeftRight)
.border_r_1()
.border_color(gpui::transparent_black())
.hover(move |grip| grip.border_color(rgb(line)))
.on_mouse_down(
MouseButton::Left,
move |event: &MouseDownEvent, _win, app| {
app.stop_propagation();
let pressed = f32::from(event.position.x);
starting.update(app, |table, _| table.begin_resize(column, pressed));
},
)
.on_drag(Resize, |_, _, _, app| app.new(|_| Empty))
}