use std::{cell::Cell, rc::Rc}; use crate::{ h_flex, scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState}, theme::{ActiveTheme, Colorize}, v_flex, }; use gpui::{ actions, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, Div, FocusHandle, FocusableView, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement as _, Render, ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled, UniformListScrollHandle, ViewContext, WindowContext, }; actions!( table, [ Cancel, SelectPrev, SelectNext, SelectPrevColumn, SelectNextColumn ] ); pub fn init(cx: &mut AppContext) { let context = Some("Table"); cx.bind_keys([ KeyBinding::new("escape", Cancel, context), KeyBinding::new("up", SelectPrev, context), KeyBinding::new("down", SelectNext, context), KeyBinding::new("left", SelectPrevColumn, context), KeyBinding::new("right", SelectNextColumn, context), ]); } struct ColGroup { width: Option, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum SelectionState { Column, Row, } pub struct Table { focus_handle: FocusHandle, delegate: D, horizontal_scroll_handle: ScrollHandle, vertical_scroll_handle: UniformListScrollHandle, col_groups: Vec, scrollbar_state: Rc>, selection_state: SelectionState, selected_row: Option, selected_col: Option, } #[allow(unused)] pub trait TableDelegate: Sized + 'static { /// Return the number of columns in the table. fn cols_count(&self) -> usize; /// Return the number of rows in the table. fn rows_count(&self) -> usize; /// Returns the name of the column at the given index. fn column_name(&self, col_ix: usize) -> SharedString; /// Returns whether the column at the given index can be resized. Default: true fn can_resize_col(&self, col_ix: usize) -> bool { true } /// Returns the width of the column at the given index. /// Return None, use auto width. fn col_width(&self, col_ix: usize) -> Option; /// Set the width of the column at the given index. fn on_col_width_changed(&mut self, col_ix: usize, width: Option) {} /// Render the header cell at the given column index, default to the column name. fn render_th(&self, col_ix: usize) -> impl IntoElement { div().size_full().child(self.column_name(col_ix)) } /// Render cell at the given row and column. fn render_td(&self, row_ix: usize, col_ix: usize) -> impl IntoElement; /// Return true to enable loop selection on the table. /// /// When the prev/next selection is out of the table bounds, the selection will loop to the other side. /// /// Default: true fn can_loop_select(&self) -> bool { true } } impl Table where D: TableDelegate, { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { let mut this = Self { focus_handle: cx.focus_handle(), delegate, col_groups: Vec::new(), horizontal_scroll_handle: ScrollHandle::new(), vertical_scroll_handle: UniformListScrollHandle::new(), scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())), selection_state: SelectionState::Row, selected_row: None, selected_col: None, }; this.update_col_groups(cx); this } pub fn delegate(&self) -> &D { &self.delegate } pub fn delegate_mut(&mut self) -> &mut D { &mut self.delegate } fn update_col_groups(&mut self, cx: &mut ViewContext) { self.col_groups = (0..self.delegate.cols_count()) .map(|col_ix| ColGroup { width: self.delegate.col_width(col_ix), }) .collect(); cx.notify(); } fn scroll_to_selected_row(&mut self, _cx: &mut ViewContext) { if let Some(row_ix) = self.selected_row { self.vertical_scroll_handle.scroll_to_item(row_ix); } } fn scroll_to_selected_column(&mut self, cx: &mut ViewContext) { if let Some(col_ix) = self.selected_col { self.horizontal_scroll_handle.scroll_to_item(col_ix); cx.notify(); } } fn on_row_click(&mut self, row_ix: usize, cx: &mut ViewContext) { self.selection_state = SelectionState::Row; self.selected_row = Some(row_ix); self.scroll_to_selected_row(cx); } fn on_col_head_click(&mut self, col_ix: usize, cx: &mut ViewContext) { self.selection_state = SelectionState::Column; self.selected_col = Some(col_ix); self.scroll_to_selected_column(cx); } fn action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { self.selection_state = SelectionState::Row; self.selected_row = None; self.selected_col = None; cx.notify(); } fn action_select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { let selected_row = self.selected_row.unwrap_or(0); let rows_count = self.delegate.rows_count(); if selected_row > 0 { self.selected_row = Some(selected_row - 1); } else { if self.delegate.can_loop_select() { self.selected_row = Some(rows_count - 1); } } self.selection_state = SelectionState::Row; self.scroll_to_selected_row(cx); cx.notify(); } fn action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { let selected_row = self.selected_row.unwrap_or(0); if selected_row < self.delegate.rows_count() - 1 { self.selected_row = Some(selected_row + 1); } else { if self.delegate.can_loop_select() { self.selected_row = Some(0); } } self.selection_state = SelectionState::Row; self.scroll_to_selected_row(cx); cx.notify(); } fn action_select_prev_column(&mut self, _: &SelectPrevColumn, cx: &mut ViewContext) { let selected_col = self.selected_col.unwrap_or(0); let cols_count = self.delegate.cols_count(); if selected_col > 0 { self.selected_col = Some(selected_col - 1); } else { if self.delegate.can_loop_select() { self.selected_col = Some(cols_count - 1); } } self.selection_state = SelectionState::Column; self.scroll_to_selected_column(cx); cx.notify(); } fn action_select_next_column(&mut self, _: &SelectNextColumn, cx: &mut ViewContext) { let selected_col = self.selected_col.unwrap_or(0); if selected_col < self.delegate.cols_count() - 1 { self.selected_col = Some(selected_col + 1); } else { if self.delegate.can_loop_select() { self.selected_col = Some(0); } } self.selection_state = SelectionState::Column; self.scroll_to_selected_column(cx); cx.notify(); } fn render_cell(&self, col_ix: usize, _cx: &mut ViewContext) -> Div { let col_width = self.col_groups[col_ix].width; div() .when_some(col_width, |this, width| this.w(px(width))) .overflow_hidden() .whitespace_nowrap() .py_1() .px_2() } /// Show Column selection style, when the column is selected and the selection state is Column. fn col_wrap(&self, col_ix: usize, cx: &mut ViewContext) -> Div { if self.selected_col == Some(col_ix) && self.selection_state == SelectionState::Column { div().bg(cx.theme().accent.opacity(0.5)) } else { div() } } fn render_scrollbar(&self, cx: &mut ViewContext) -> Option { let view = cx.view().clone(); let state = self.scrollbar_state.clone(); Some( div() .absolute() .top_0() .left_0() .right_0() .bottom_0() .child(Scrollbar::uniform_scroll( view, state, self.vertical_scroll_handle.clone(), self.delegate.rows_count(), )), ) } } impl FocusableView for Table where D: TableDelegate, { fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle { self.focus_handle.clone() } } impl Render for Table where D: TableDelegate, { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let view = cx.view().clone(); let vertical_scroll_handle = self.vertical_scroll_handle.clone(); let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); let cols_count: usize = self.delegate.cols_count(); let rows_count = self.delegate.rows_count(); let selected_bg = cx.theme().accent.opacity(0.8); let hover_bg = cx.theme().accent.opacity(0.5); fn tr(cx: &mut WindowContext) -> Div { h_flex().gap_1().border_color(cx.theme().border) } let inner_table = v_flex() .key_context("Table") .id("table") .track_focus(&self.focus_handle) .on_action(cx.listener(Self::action_cancel)) .on_action(cx.listener(Self::action_select_next)) .on_action(cx.listener(Self::action_select_prev)) .on_action(cx.listener(Self::action_select_next_column)) .on_action(cx.listener(Self::action_select_prev_column)) .size_full() .overflow_hidden() .child( v_flex() .flex_grow() .h_10() .w_full() .shadow_sm() .border_b_1() .border_color(cx.theme().border) .child( uniform_list(view.clone(), "table-uniform-list-head", 1, { let horizontal_scroll_handle = horizontal_scroll_handle.clone(); move |table, _, cx| { // Columns vec![tr(cx) .id("table-head") .w_full() .overflow_scroll() .track_scroll(&horizontal_scroll_handle) // The children must be one by one items. // Becuase the horizontal scroll handle will use the child_item_bounds to // calculate the item position for itself's `scroll_to_item` method. .children(table.col_groups.iter().enumerate().map( |(col_ix, _)| { table.col_wrap(col_ix, cx).child( table .render_cell(col_ix, cx) .on_mouse_down( MouseButton::Left, cx.listener(move |this, _, cx| { this.on_col_head_click(col_ix, cx); }), ) .child(table.delegate.render_th(col_ix)), ) }, )) .flex_1()] } }) .size_full(), ), ) .child( h_flex().id("table-body").flex_grow().size_full().child( uniform_list(view, "table-uniform-list", rows_count, { let horizontal_scroll_handle = horizontal_scroll_handle.clone(); move |table, visible_range, cx| { visible_range .map(|row_ix| { tr(cx) .id(("table-row", row_ix)) .w_full() .children((0..cols_count).map(|col_ix| { table .col_wrap(col_ix, cx) // Make the row scroll sync with the horizontal_scroll_handle to support horizontal scrolling. .left(horizontal_scroll_handle.offset().x) .child( table .render_cell(col_ix, cx) .flex_shrink_0() .child( table .delegate .render_td(row_ix, col_ix), ), ) })) .when(row_ix > 0, |this| this.border_t_1()) .hover(|this| { if table.selected_row.is_some() { this } else { this.bg(hover_bg) } }) // Row selected style .when_some(table.selected_row, |this, selected_row| { this.when( row_ix == selected_row && table.selection_state == SelectionState::Row, |this| this.bg(selected_bg), ) }) .on_mouse_down( MouseButton::Left, cx.listener(move |this, _, cx| { this.on_row_click(row_ix, cx); }), ) }) .collect::>() } }) .flex_grow() .size_full() .with_sizing_behavior(gpui::ListSizingBehavior::Auto) .track_scroll(vertical_scroll_handle) .into_any_element(), ), ); div() .size_full() .rounded_md() .border_1() .border_color(cx.theme().border) .bg(cx.theme().card) .child(inner_table) .children(self.render_scrollbar(cx)) .child(ScrollableMask::new( cx.view().clone(), ScrollableAxis::Horizontal, &horizontal_scroll_handle, )) } }