mod body;
mod footer;
mod header;
mod page_size;
mod state;
pub use state::{Column, PageHandler, TableState};
use fluidattacks_designs_domain::search;
use fluidattacks_designs_domain::table::{
Page, BODY_BG, DEFAULT_HEIGHT, EMPTY_DESCRIPTION, EMPTY_TITLE, HEADER_HEIGHT, SEARCH_MIN_WIDTH,
SEARCH_ROW_GAP, TOP_RADIUS,
};
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, relative, rgb, AnyElement, App, Div, ElementId, Entity, Focusable as _,
InteractiveElement as _, IntoElement, KeyBinding, ListState, MouseButton, MouseUpEvent,
ParentElement as _, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _,
Styled as _, Window,
};
use crate::empty_state::EmptyState;
use crate::scrollbar::Scrollbar;
use crate::text_input::TextInput;
use crate::theme::ActiveTheme as _;
const CONTEXT: &str = "DesignsTable";
pub fn part(state: &Entity<TableState>, name: impl Into<SharedString>) -> ElementId {
ElementId::NamedChild(
std::sync::Arc::new(ElementId::View(state.entity_id())),
name.into(),
)
}
#[allow(clippy::derive_partial_eq_without_eq)]
mod action_defs {
gpui::actions!(designs_table, [Copy]);
}
use action_defs::Copy;
pub fn bind_keys(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
]);
}
#[derive(IntoElement)]
pub struct Table {
state: Entity<TableState>,
height: f32,
empty_title: SharedString,
empty_description: SharedString,
search: bool,
controls: Vec<AnyElement>,
on_page: Option<PageHandler>,
}
impl Table {
#[must_use]
pub fn new(state: &Entity<TableState>) -> Self {
Self {
state: state.clone(),
height: DEFAULT_HEIGHT,
empty_title: SharedString::new_static(EMPTY_TITLE),
empty_description: SharedString::new_static(EMPTY_DESCRIPTION),
search: false,
controls: Vec::new(),
on_page: None,
}
}
#[must_use]
pub fn pagination(mut self, on_page: impl Fn(Page, &mut App) + 'static) -> Self {
self.on_page = Some(std::rc::Rc::new(on_page));
self
}
#[must_use]
pub const fn search(mut self, search: bool) -> Self {
self.search = search;
self
}
#[must_use]
pub fn control(mut self, control: impl IntoElement) -> Self {
self.controls.push(control.into_any_element());
self
}
#[must_use]
pub const fn height(mut self, height: f32) -> Self {
self.height = height;
self
}
#[must_use]
pub fn empty(
mut self,
title: impl Into<SharedString>,
description: impl Into<SharedString>,
) -> Self {
self.empty_title = title.into();
self.empty_description = description.into();
self
}
}
impl RenderOnce for Table {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let focus = self.state.read(cx).focus_handle(cx);
let rows = self.state.read(cx).visible_rows();
let list = self.state.read(cx).list().clone();
let copying = self.state.clone();
let options = options_row(&self.state, self.search, self.controls, cx);
let frame = div()
.flex()
.flex_col()
.flex_none()
.h(px(self.height))
.bg(rgb(cx.theme().shade(BODY_BG)))
.text_color(rgb(cx.theme().colors.text))
.rounded_t(px(TOP_RADIUS))
.overflow_hidden()
.child(if rows == 0 {
empty_frame(&self.state, self.empty_title, self.empty_description, cx)
} else {
filled_frame(&self.state, &list, cx)
})
.children(
self.on_page
.as_ref()
.map(|on_page| footer::footer(&self.state, on_page, cx)),
);
let releasing = self.state;
div()
.track_focus(&focus)
.key_context(CONTEXT)
.on_action(move |_: &Copy, _win, app| {
copying.update(app, |table, inner| table.copy_selection(inner));
})
.on_mouse_up(MouseButton::Left, {
let releasing = releasing.clone();
move |_: &MouseUpEvent, _win, app| {
releasing.update(app, |table, _| table.end_row_drag());
}
})
.on_mouse_up_out(MouseButton::Left, move |_: &MouseUpEvent, _win, app| {
releasing.update(app, |table, _| table.end_row_drag());
})
.flex()
.flex_col()
.flex_none()
.gap(px(SEARCH_ROW_GAP))
.children(options)
.child(frame)
}
}
fn options_row(
state: &Entity<TableState>,
search: bool,
controls: Vec<AnyElement>,
cx: &App,
) -> Option<Div> {
if !search && controls.is_empty() {
return None;
}
let mut row = div().flex().items_center().gap(px(SEARCH_ROW_GAP));
if search {
row = row.child(
div()
.flex_none()
.w(px(SEARCH_MIN_WIDTH))
.child(TextInput::new(state.read(cx).search_box()).leading_glyph(search::GLYPH)),
);
}
Some(row.children(controls))
}
fn across_scroll(state: &Entity<TableState>, below: AnyElement, cx: &App) -> Stateful<Div> {
div()
.id(part(state, "across"))
.flex()
.flex_col()
.size_full()
.overflow_x_scroll()
.map(keep_the_wheel_off_x)
.track_scroll(state.read(cx).across_handle())
.child(header::header_row(state, cx))
.child(below)
}
fn filled_frame(state: &Entity<TableState>, list: &ListState, cx: &App) -> AnyElement {
let across = state.read(cx).across_handle().clone();
let width = state.read(cx).row_width();
div()
.relative()
.flex_grow(1.0)
.min_h_0()
.child(across_scroll(
state,
body_slot(state, list, width).into_any_element(),
cx,
))
.child(
Scrollbar::rows(part(state, "down-bar"), list)
.track_transparent()
.reserve_corner(),
)
.child(
Scrollbar::horizontal(part(state, "across-bar"), &across)
.track_transparent()
.reserve_corner(),
)
.into_any_element()
}
fn keep_the_wheel_off_x(mut across: Stateful<Div>) -> Stateful<Div> {
across.style().restrict_scroll_to_axis = Some(true);
across
}
fn body_slot(state: &Entity<TableState>, list: &ListState, width: f32) -> Div {
let listing = state.clone();
div().flex_grow(1.0).min_h_0().child(
gpui::list(list.clone(), move |position, _win, app| {
body::body_row(&listing, position, width, app).into_any_element()
})
.h_full()
.w(px(width))
.min_w(relative(1.0)),
)
}
fn empty_frame(
state: &Entity<TableState>,
title: SharedString,
description: SharedString,
cx: &App,
) -> AnyElement {
let across = state.read(cx).across_handle().clone();
div()
.relative()
.flex_grow(1.0)
.min_h_0()
.child(across_scroll(state, gpui::Empty.into_any_element(), cx))
.child(empty_slot(title, description))
.child(Scrollbar::horizontal(part(state, "across-bar"), &across).track_transparent())
.into_any_element()
}
fn empty_slot(title: SharedString, description: SharedString) -> gpui::AnyElement {
div()
.absolute()
.top(px(HEADER_HEIGHT))
.left_0()
.right_0()
.bottom_0()
.flex()
.flex_col()
.overflow_hidden()
.items_center()
.justify_center()
.child(
EmptyState::new(title, description)
.size(fluidattacks_designs_domain::empty_state::Size::Sm),
)
.into_any_element()
}