table, list: Add right clicked row style and improve selection style. (#362)

- table: Add `content_menu` to TableDelegate.

<img width="644" alt="image"
src="https://github.com/user-attachments/assets/4c21299f-00e8-4a61-964d-338ef7daa802">
This commit is contained in:
Jason Lee 2024-10-18 11:52:14 +08:00 committed by GitHub
parent c02cb802cb
commit 4bef681c9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 179 additions and 76 deletions

View file

@ -10,12 +10,11 @@ use serde::Deserialize;
use ui::{ use ui::{
button::{Button, ButtonStyled}, button::{Button, ButtonStyled},
checkbox::Checkbox, checkbox::Checkbox,
context_menu::ContextMenuExt,
h_flex, h_flex,
indicator::Indicator, indicator::Indicator,
input::{InputEvent, TextInput}, input::{InputEvent, TextInput},
label::Label, label::Label,
popup_menu::PopupMenuExt, popup_menu::{PopupMenu, PopupMenuExt},
prelude::FluentBuilder as _, prelude::FluentBuilder as _,
table::{ColFixed, ColSort, Table, TableDelegate, TableEvent}, table::{ColFixed, ColSort, Table, TableDelegate, TableEvent},
v_flex, Selectable, Size, StyleSized as _, v_flex, Selectable, Size, StyleSized as _,
@ -341,17 +340,11 @@ impl TableDelegate for StockTableDelegate {
} }
} }
fn render_tr( fn context_menu(&self, _: usize, menu: PopupMenu, _: &WindowContext) -> PopupMenu {
&self, menu.menu("Size Large", Box::new(ChangeSize(Size::Large)))
row_ix: usize,
_: &mut ViewContext<Table<Self>>,
) -> gpui::Stateful<gpui::Div> {
h_flex().id(("table-row", row_ix)).context_menu(|this, _| {
this.menu("Size Large", Box::new(ChangeSize(Size::Large)))
.menu("Size Medium", Box::new(ChangeSize(Size::Medium))) .menu("Size Medium", Box::new(ChangeSize(Size::Medium)))
.menu("Size Small", Box::new(ChangeSize(Size::Small))) .menu("Size Small", Box::new(ChangeSize(Size::Small)))
.menu("Size XSmall", Box::new(ChangeSize(Size::XSmall))) .menu("Size XSmall", Box::new(ChangeSize(Size::XSmall)))
})
} }
fn render_td( fn render_td(

View file

@ -16,7 +16,7 @@ pub trait ContextMenuExt: ParentElement + Sized {
self, self,
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static, f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
) -> Self { ) -> Self {
self.child(ContextMenu::new("context_menu").menu(f)) self.child(ContextMenu::new("context-menu").menu(f))
} }
} }
@ -118,6 +118,12 @@ impl Element for ContextMenu {
let menu_view = state.menu_view.borrow().clone(); let menu_view = state.menu_view.borrow().clone();
let (menu_element, menu_layout_id) = if *open.borrow() { let (menu_element, menu_layout_id) = if *open.borrow() {
let has_menu_item = menu_view
.as_ref()
.map(|menu| !menu.read(cx).is_empty())
.unwrap_or(false);
if has_menu_item {
let mut menu_element = deferred( let mut menu_element = deferred(
anchored() anchored()
.position(*position) .position(*position)
@ -137,6 +143,9 @@ impl Element for ContextMenu {
(Some(menu_element), Some(menu_layout_id)) (Some(menu_element), Some(menu_layout_id))
} else { } else {
(None, None) (None, None)
}
} else {
(None, None)
}; };
let mut layout_ids = vec![]; let mut layout_ids = vec![];
@ -199,9 +208,6 @@ impl Element for ContextMenu {
&& event.button == MouseButton::Right && event.button == MouseButton::Right
&& bounds.contains(&event.position) && bounds.contains(&event.position)
{ {
cx.prevent_default();
cx.stop_propagation();
*position.borrow_mut() = event.position; *position.borrow_mut() = event.position;
*open.borrow_mut() = true; *open.borrow_mut() = true;

View file

@ -14,6 +14,7 @@ use gpui::{
ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task, ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task,
UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
}; };
use gpui::{deferred, px};
use smol::Timer; use smol::Timer;
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]); actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
@ -92,6 +93,7 @@ pub struct List<D: ListDelegate> {
pub(crate) size: Size, pub(crate) size: Size,
selected_index: Option<usize>, selected_index: Option<usize>,
right_clicked_index: Option<usize>,
_search_task: Task<()>, _search_task: Task<()>,
} }
@ -117,6 +119,7 @@ where
query_input: Some(query_input), query_input: Some(query_input),
last_query: None, last_query: None,
selected_index: None, selected_index: None,
right_clicked_index: None,
vertical_scroll_handle: UniformListScrollHandle::new(), vertical_scroll_handle: UniformListScrollHandle::new(),
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())), scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
max_height: None, max_height: None,
@ -303,6 +306,57 @@ where
self.scroll_to_selected_item(cx); self.scroll_to_selected_item(cx);
cx.notify(); cx.notify();
} }
fn render_list_item(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.id("list-item")
.w_full()
.relative()
.children(self.delegate.render_item(ix, cx))
.when_some(self.selected_index, |this, selected_index| {
this.when(ix == selected_index, |this| {
this.child(deferred(
div()
.absolute()
.top(px(-1.))
.left(px(-1.))
.right(px(-1.))
.bottom(px(-1.))
.bg(cx.theme().list_active)
.border_1()
.border_color(cx.theme().list_active_border),
))
})
})
.when(self.right_clicked_index == Some(ix), |this| {
this.child(deferred(
div()
.absolute()
.top(px(-1.))
.left(px(-1.))
.right(px(-1.))
.bottom(px(-1.))
.border_1()
.border_color(cx.theme().list_active_border),
))
})
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, cx| {
cx.stop_propagation();
this.right_clicked_index = None;
this.selected_index = Some(ix);
this.on_action_confirm(&Confirm, cx);
}),
)
.on_mouse_down(
MouseButton::Right,
cx.listener(move |this, _, cx| {
this.right_clicked_index = Some(ix);
cx.notify();
}),
)
}
} }
impl<D> FocusableView for List<D> impl<D> FocusableView for List<D>
@ -332,8 +386,6 @@ where
ListSizingBehavior::Auto ListSizingBehavior::Auto
}; };
let selected_bg = cx.theme().list_active;
let inital_view = if let Some(input) = &self.query_input { let inital_view = if let Some(input) = &self.query_input {
if input.read(cx).text().is_empty() { if input.read(cx).text().is_empty() {
self.delegate().render_initial(cx) self.delegate().render_initial(cx)
@ -385,31 +437,7 @@ where
uniform_list(view, "uniform-list", items_count, { uniform_list(view, "uniform-list", items_count, {
move |list, visible_range, cx| { move |list, visible_range, cx| {
visible_range visible_range
.map(|ix| { .map(|ix| list.render_list_item(ix, cx))
div()
.id("list-item")
.w_full()
.children(list.delegate.render_item(ix, cx))
.when_some(
list.selected_index,
|this, selected_index| {
this.when(
ix == selected_index,
|this| this.bg(selected_bg),
)
},
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, cx| {
cx.stop_propagation();
this.selected_index = Some(ix);
this.on_action_confirm(
&Confirm, cx,
);
}),
)
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
}) })
@ -423,5 +451,12 @@ where
) )
} }
}) })
// Click out to cancel right clicked row
.when(self.right_clicked_index.is_some(), |this| {
this.on_mouse_down_out(cx.listener(|this, _, cx| {
this.right_clicked_index = None;
cx.notify();
}))
})
} }
} }

View file

@ -334,6 +334,10 @@ impl PopupMenu {
None None
} }
pub fn is_empty(&self) -> bool {
self.menu_items.is_empty()
}
fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> { fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
self.menu_items self.menu_items
.iter() .iter()

View file

@ -1,14 +1,16 @@
use std::{cell::Cell, ops::Range, rc::Rc}; use std::{cell::Cell, ops::Range, rc::Rc};
use crate::{ use crate::{
context_menu::ContextMenuExt,
h_flex, h_flex,
popup_menu::PopupMenu,
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState}, scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
theme::ActiveTheme, theme::ActiveTheme,
v_flex, Icon, IconName, Sizable, Size, StyleSized as _, v_flex, Icon, IconName, Sizable, Size, StyleSized as _,
}; };
use gpui::{ use gpui::{
actions, canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds, Div, actions, canvas, deferred, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds,
DragMoveEvent, Edges, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, Div, DragMoveEvent, Edges, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, Render, InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, Render,
ScrollHandle, SharedString, Stateful, StatefulInteractiveElement as _, Styled, ScrollHandle, SharedString, Stateful, StatefulInteractiveElement as _, Styled,
UniformListScrollHandle, ViewContext, VisualContext as _, WindowContext, UniformListScrollHandle, ViewContext, VisualContext as _, WindowContext,
@ -119,6 +121,7 @@ pub struct Table<D: TableDelegate> {
selection_state: SelectionState, selection_state: SelectionState,
selected_row: Option<usize>, selected_row: Option<usize>,
right_clicked_row: Option<usize>,
selected_col: Option<usize>, selected_col: Option<usize>,
/// The column index that is being resized. /// The column index that is being resized.
@ -190,6 +193,11 @@ pub trait TableDelegate: Sized + 'static {
h_flex().id(("table-row", row_ix)) h_flex().id(("table-row", row_ix))
} }
/// Render the context menu for the row at the given row index.
fn context_menu(&self, row_ix: usize, menu: PopupMenu, cx: &WindowContext) -> PopupMenu {
menu
}
/// Render cell at the given row and column. /// Render cell at the given row and column.
fn render_td( fn render_td(
&self, &self,
@ -265,6 +273,7 @@ where
horizontal_scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())), horizontal_scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
selection_state: SelectionState::Row, selection_state: SelectionState::Row,
selected_row: None, selected_row: None,
right_clicked_row: None,
selected_col: None, selected_col: None,
resizing_col: None, resizing_col: None,
bounds: Bounds::default(), bounds: Bounds::default(),
@ -335,6 +344,7 @@ where
fn set_selected_row(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) { fn set_selected_row(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) {
self.selection_state = SelectionState::Row; self.selection_state = SelectionState::Row;
self.right_clicked_row = None;
self.selected_row = Some(row_ix); self.selected_row = Some(row_ix);
if let Some(row_ix) = self.selected_row { if let Some(row_ix) = self.selected_row {
self.vertical_scroll_handle.scroll_to_item(row_ix); self.vertical_scroll_handle.scroll_to_item(row_ix);
@ -356,9 +366,18 @@ where
cx.notify(); cx.notify();
} }
fn on_row_click(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) { fn on_row_click(
&mut self,
mouse_button: MouseButton,
row_ix: usize,
cx: &mut ViewContext<Self>,
) {
if mouse_button == MouseButton::Right {
self.right_clicked_row = Some(row_ix);
} else {
self.set_selected_row(row_ix, cx) self.set_selected_row(row_ix, cx)
} }
}
fn on_col_head_click(&mut self, col_ix: usize, cx: &mut ViewContext<Self>) { fn on_col_head_click(&mut self, col_ix: usize, cx: &mut ViewContext<Self>) {
if !self.delegate.can_select_col(col_ix, cx) { if !self.delegate.can_select_col(col_ix, cx) {
@ -938,10 +957,14 @@ where
let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
let is_stripe_row = self.stripe && row_ix % 2 != 0; let is_stripe_row = self.stripe && row_ix % 2 != 0;
let is_selected = self.selected_row == Some(row_ix); let is_selected = self.selected_row == Some(row_ix);
let view = cx.view().clone();
if row_ix < rows_count { if row_ix < rows_count {
self.delegate self.delegate
.render_tr(row_ix, cx) .render_tr(row_ix, cx)
.context_menu(move |this, cx: &mut ViewContext<PopupMenu>| {
view.read(cx).delegate.context_menu(row_ix, this, cx)
})
.w_full() .w_full()
.h(self.size.table_row_height()) .h(self.size.table_row_height())
.when(row_ix > 0, |this| { .when(row_ix > 0, |this| {
@ -949,7 +972,7 @@ where
}) })
.when(is_stripe_row, |this| this.bg(cx.theme().table_even)) .when(is_stripe_row, |this| this.bg(cx.theme().table_even))
.hover(|this| { .hover(|this| {
if is_selected { if is_selected || self.right_clicked_row == Some(row_ix) {
this this
} else { } else {
this.bg(cx.theme().table_hover) this.bg(cx.theme().table_hover)
@ -994,13 +1017,44 @@ where
.when_some(self.selected_row, |this, _| { .when_some(self.selected_row, |this, _| {
this.when( this.when(
is_selected && self.selection_state == SelectionState::Row, is_selected && self.selection_state == SelectionState::Row,
|this| this.bg(cx.theme().table_active), |this| {
this.child(deferred(
div()
.top(px(-1.))
.left(px(-1.))
.right(px(-1.))
.bottom_0()
.absolute()
.bg(cx.theme().table_active)
.border_1()
.border_color(cx.theme().table_active_border),
))
},
) )
}) })
// Row right click row style
.when(self.right_clicked_row == Some(row_ix), |this| {
this.child(deferred(
div()
.top(px(-1.))
.left(px(-1.))
.right(px(-1.))
.bottom_0()
.absolute()
.border_1()
.border_color(cx.theme().selection),
))
})
.on_mouse_down( .on_mouse_down(
MouseButton::Left, MouseButton::Left,
cx.listener(move |this, _, cx| { cx.listener(move |this, _, cx| {
this.on_row_click(row_ix, cx); this.on_row_click(MouseButton::Left, row_ix, cx);
}),
)
.on_mouse_down(
MouseButton::Right,
cx.listener(move |this, _, cx| {
this.on_row_click(MouseButton::Right, row_ix, cx);
}), }),
) )
} else { } else {
@ -1170,5 +1224,12 @@ where
.when(rows_count > 0, |this| { .when(rows_count > 0, |this| {
this.children(self.render_scrollbar(cx)) this.children(self.render_scrollbar(cx))
}) })
// Click out to cancel right clicked row
.when(self.right_clicked_row.is_some(), |this| {
this.on_mouse_down_out(cx.listener(|this, _, cx| {
this.right_clicked_row = None;
cx.notify();
}))
})
} }
} }

View file

@ -177,7 +177,6 @@ struct Colors {
pub tab_bar: Hsla, pub tab_bar: Hsla,
pub list: Hsla, pub list: Hsla,
pub list_even: Hsla, pub list_even: Hsla,
pub list_active: Hsla,
pub list_head: Hsla, pub list_head: Hsla,
pub link: Hsla, pub link: Hsla,
pub drop_target: Hsla, pub drop_target: Hsla,
@ -220,7 +219,6 @@ impl Colors {
tab_bar: hsl(240.0, 4.8, 95.9), tab_bar: hsl(240.0, 4.8, 95.9),
list: hsl(0.0, 0.0, 100.), list: hsl(0.0, 0.0, 100.),
list_even: hsl(240.0, 5.0, 96.0), list_even: hsl(240.0, 5.0, 96.0),
list_active: hsl(240.0, 7., 88.0).opacity(0.75),
list_head: hsl(0.0, 0.0, 100.), list_head: hsl(0.0, 0.0, 100.),
link: hsl(221.0, 83.0, 53.0), link: hsl(221.0, 83.0, 53.0),
drop_target: hsl(235.0, 30., 44.0).opacity(0.25), drop_target: hsl(235.0, 30., 44.0).opacity(0.25),
@ -263,7 +261,6 @@ impl Colors {
tab_bar: hsl(299.0, 0., 5.5), tab_bar: hsl(299.0, 0., 5.5),
list: hsl(0.0, 0.0, 8.0), list: hsl(0.0, 0.0, 8.0),
list_even: hsl(240.0, 3.7, 10.0), list_even: hsl(240.0, 3.7, 10.0),
list_active: hsl(240.0, 3.7, 17.0),
list_head: hsl(0.0, 0.0, 8.0), list_head: hsl(0.0, 0.0, 8.0),
link: hsl(221.0, 83.0, 53.0), link: hsl(221.0, 83.0, 53.0),
drop_target: hsl(235.0, 30., 44.0).opacity(0.1), drop_target: hsl(235.0, 30., 44.0).opacity(0.1),
@ -326,6 +323,7 @@ pub struct Theme {
pub list_even: Hsla, pub list_even: Hsla,
pub list_head: Hsla, pub list_head: Hsla,
pub list_active: Hsla, pub list_active: Hsla,
pub list_active_border: Hsla,
pub list_hover: Hsla, pub list_hover: Hsla,
pub table: Hsla, pub table: Hsla,
pub table_even: Hsla, pub table_even: Hsla,
@ -333,6 +331,7 @@ pub struct Theme {
pub table_head_foreground: Hsla, pub table_head_foreground: Hsla,
pub table_row_border: Hsla, pub table_row_border: Hsla,
pub table_active: Hsla, pub table_active: Hsla,
pub table_active_border: Hsla,
pub table_hover: Hsla, pub table_hover: Hsla,
pub link: Hsla, pub link: Hsla,
pub link_hover: Hsla, pub link_hover: Hsla,
@ -378,7 +377,7 @@ impl Theme {
self.border = self.border.apply(mask_color); self.border = self.border.apply(mask_color);
self.input = self.input.apply(mask_color); self.input = self.input.apply(mask_color);
self.ring = self.ring.apply(mask_color); self.ring = self.ring.apply(mask_color);
self.selection = self.selection.apply(mask_color); // self.selection = self.selection.apply(mask_color);
self.scrollbar = self.scrollbar.apply(mask_color); self.scrollbar = self.scrollbar.apply(mask_color);
self.scrollbar_thumb = self.scrollbar_thumb.apply(mask_color); self.scrollbar_thumb = self.scrollbar_thumb.apply(mask_color);
self.panel = self.panel.apply(mask_color); self.panel = self.panel.apply(mask_color);
@ -395,13 +394,16 @@ impl Theme {
self.list = self.list.apply(mask_color); self.list = self.list.apply(mask_color);
self.list_even = self.list_even.apply(mask_color); self.list_even = self.list_even.apply(mask_color);
self.list_head = self.list_head.apply(mask_color); self.list_head = self.list_head.apply(mask_color);
self.list_active = self.list_active.apply(mask_color); // self.list_active = self.list_active.apply(mask_color);
// self.list_active_border = self.list_active_border.apply(mask_color);
self.list_hover = self.list_hover.apply(mask_color); self.list_hover = self.list_hover.apply(mask_color);
self.table = self.table.apply(mask_color); self.table = self.table.apply(mask_color);
self.table_even = self.table_even.apply(mask_color); self.table_even = self.table_even.apply(mask_color);
self.table_active = self.table_active.apply(mask_color); // self.table_active = self.table_active.apply(mask_color);
// self.table_active_border = self.table_active_border.apply(mask_color);
self.table_hover = self.table_hover.apply(mask_color); self.table_hover = self.table_hover.apply(mask_color);
self.table_row_border = self.table_row_border.apply(mask_color); self.table_row_border = self.table_row_border.apply(mask_color);
self.table_head = self.table_head.apply(mask_color);
self.table_head_foreground = self.table_head_foreground.apply(mask_color); self.table_head_foreground = self.table_head_foreground.apply(mask_color);
self.link = self.link.apply(mask_color); self.link = self.link.apply(mask_color);
self.link_hover = self.link_hover.apply(mask_color); self.link_hover = self.link_hover.apply(mask_color);
@ -472,13 +474,15 @@ impl From<Colors> for Theme {
list: colors.list, list: colors.list,
list_even: colors.list_even, list_even: colors.list_even,
list_head: colors.list_head, list_head: colors.list_head,
list_active: colors.list_active, list_active: colors.selection.opacity(0.2),
list_hover: colors.list_active.opacity(0.6), list_active_border: colors.selection,
list_hover: colors.selection.opacity(0.2),
table_head: colors.list_head, table_head: colors.list_head,
table: colors.list, table: colors.list,
table_even: colors.list_even, table_even: colors.list_even,
table_active: colors.list_active, table_active: colors.selection.opacity(0.2),
table_hover: colors.list_active.opacity(0.8), table_active_border: colors.selection,
table_hover: colors.selection.opacity(0.2),
table_row_border: colors.border.opacity(0.5), table_row_border: colors.border.opacity(0.5),
table_head_foreground: colors.foreground.opacity(0.7), table_head_foreground: colors.foreground.opacity(0.7),
link: colors.link, link: colors.link,