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::{
button::{Button, ButtonStyled},
checkbox::Checkbox,
context_menu::ContextMenuExt,
h_flex,
indicator::Indicator,
input::{InputEvent, TextInput},
label::Label,
popup_menu::PopupMenuExt,
popup_menu::{PopupMenu, PopupMenuExt},
prelude::FluentBuilder as _,
table::{ColFixed, ColSort, Table, TableDelegate, TableEvent},
v_flex, Selectable, Size, StyleSized as _,
@ -341,17 +340,11 @@ impl TableDelegate for StockTableDelegate {
}
}
fn render_tr(
&self,
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 Small", Box::new(ChangeSize(Size::Small)))
.menu("Size XSmall", Box::new(ChangeSize(Size::XSmall)))
})
fn context_menu(&self, _: usize, menu: PopupMenu, _: &WindowContext) -> PopupMenu {
menu.menu("Size Large", Box::new(ChangeSize(Size::Large)))
.menu("Size Medium", Box::new(ChangeSize(Size::Medium)))
.menu("Size Small", Box::new(ChangeSize(Size::Small)))
.menu("Size XSmall", Box::new(ChangeSize(Size::XSmall)))
}
fn render_td(

View file

@ -16,7 +16,7 @@ pub trait ContextMenuExt: ParentElement + Sized {
self,
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
) -> Self {
self.child(ContextMenu::new("context_menu").menu(f))
self.child(ContextMenu::new("context-menu").menu(f))
}
}
@ -118,23 +118,32 @@ impl Element for ContextMenu {
let menu_view = state.menu_view.borrow().clone();
let (menu_element, menu_layout_id) = if *open.borrow() {
let mut menu_element = deferred(
anchored()
.position(*position)
.snap_to_window_with_margin(px(8.))
.anchor(anchor)
.when_some(menu_view, |this, menu| {
// Focus the menu, so that can be handle the action.
menu.focus_handle(cx).focus(cx);
let has_menu_item = menu_view
.as_ref()
.map(|menu| !menu.read(cx).is_empty())
.unwrap_or(false);
this.child(div().occlude().child(menu.clone()))
}),
)
.with_priority(1)
.into_any();
if has_menu_item {
let mut menu_element = deferred(
anchored()
.position(*position)
.snap_to_window_with_margin(px(8.))
.anchor(anchor)
.when_some(menu_view, |this, menu| {
// Focus the menu, so that can be handle the action.
menu.focus_handle(cx).focus(cx);
let menu_layout_id = menu_element.request_layout(cx);
(Some(menu_element), Some(menu_layout_id))
this.child(div().occlude().child(menu.clone()))
}),
)
.with_priority(1)
.into_any();
let menu_layout_id = menu_element.request_layout(cx);
(Some(menu_element), Some(menu_layout_id))
} else {
(None, None)
}
} else {
(None, None)
};
@ -199,9 +208,6 @@ impl Element for ContextMenu {
&& event.button == MouseButton::Right
&& bounds.contains(&event.position)
{
cx.prevent_default();
cx.stop_propagation();
*position.borrow_mut() = event.position;
*open.borrow_mut() = true;

View file

@ -14,6 +14,7 @@ use gpui::{
ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task,
UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
};
use gpui::{deferred, px};
use smol::Timer;
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
@ -92,6 +93,7 @@ pub struct List<D: ListDelegate> {
pub(crate) size: Size,
selected_index: Option<usize>,
right_clicked_index: Option<usize>,
_search_task: Task<()>,
}
@ -117,6 +119,7 @@ where
query_input: Some(query_input),
last_query: None,
selected_index: None,
right_clicked_index: None,
vertical_scroll_handle: UniformListScrollHandle::new(),
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
max_height: None,
@ -303,6 +306,57 @@ where
self.scroll_to_selected_item(cx);
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>
@ -332,8 +386,6 @@ where
ListSizingBehavior::Auto
};
let selected_bg = cx.theme().list_active;
let inital_view = if let Some(input) = &self.query_input {
if input.read(cx).text().is_empty() {
self.delegate().render_initial(cx)
@ -385,31 +437,7 @@ where
uniform_list(view, "uniform-list", items_count, {
move |list, visible_range, cx| {
visible_range
.map(|ix| {
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,
);
}),
)
})
.map(|ix| list.render_list_item(ix, cx))
.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
}
pub fn is_empty(&self) -> bool {
self.menu_items.is_empty()
}
fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
self.menu_items
.iter()

View file

@ -1,14 +1,16 @@
use std::{cell::Cell, ops::Range, rc::Rc};
use crate::{
context_menu::ContextMenuExt,
h_flex,
popup_menu::PopupMenu,
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
theme::ActiveTheme,
v_flex, Icon, IconName, Sizable, Size, StyleSized as _,
};
use gpui::{
actions, canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds, Div,
DragMoveEvent, Edges, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
actions, canvas, deferred, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds,
Div, DragMoveEvent, Edges, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, Render,
ScrollHandle, SharedString, Stateful, StatefulInteractiveElement as _, Styled,
UniformListScrollHandle, ViewContext, VisualContext as _, WindowContext,
@ -119,6 +121,7 @@ pub struct Table<D: TableDelegate> {
selection_state: SelectionState,
selected_row: Option<usize>,
right_clicked_row: Option<usize>,
selected_col: Option<usize>,
/// The column index that is being resized.
@ -190,6 +193,11 @@ pub trait TableDelegate: Sized + 'static {
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.
fn render_td(
&self,
@ -265,6 +273,7 @@ where
horizontal_scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
selection_state: SelectionState::Row,
selected_row: None,
right_clicked_row: None,
selected_col: None,
resizing_col: None,
bounds: Bounds::default(),
@ -335,6 +344,7 @@ where
fn set_selected_row(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) {
self.selection_state = SelectionState::Row;
self.right_clicked_row = None;
self.selected_row = Some(row_ix);
if let Some(row_ix) = self.selected_row {
self.vertical_scroll_handle.scroll_to_item(row_ix);
@ -356,8 +366,17 @@ where
cx.notify();
}
fn on_row_click(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) {
self.set_selected_row(row_ix, cx)
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)
}
}
fn on_col_head_click(&mut self, col_ix: usize, cx: &mut ViewContext<Self>) {
@ -938,10 +957,14 @@ where
let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
let is_stripe_row = self.stripe && row_ix % 2 != 0;
let is_selected = self.selected_row == Some(row_ix);
let view = cx.view().clone();
if row_ix < rows_count {
self.delegate
.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()
.h(self.size.table_row_height())
.when(row_ix > 0, |this| {
@ -949,7 +972,7 @@ where
})
.when(is_stripe_row, |this| this.bg(cx.theme().table_even))
.hover(|this| {
if is_selected {
if is_selected || self.right_clicked_row == Some(row_ix) {
this
} else {
this.bg(cx.theme().table_hover)
@ -994,13 +1017,44 @@ where
.when_some(self.selected_row, |this, _| {
this.when(
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(
MouseButton::Left,
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 {
@ -1170,5 +1224,12 @@ where
.when(rows_count > 0, |this| {
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 list: Hsla,
pub list_even: Hsla,
pub list_active: Hsla,
pub list_head: Hsla,
pub link: Hsla,
pub drop_target: Hsla,
@ -220,7 +219,6 @@ impl Colors {
tab_bar: hsl(240.0, 4.8, 95.9),
list: hsl(0.0, 0.0, 100.),
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.),
link: hsl(221.0, 83.0, 53.0),
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),
list: hsl(0.0, 0.0, 8.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),
link: hsl(221.0, 83.0, 53.0),
drop_target: hsl(235.0, 30., 44.0).opacity(0.1),
@ -326,6 +323,7 @@ pub struct Theme {
pub list_even: Hsla,
pub list_head: Hsla,
pub list_active: Hsla,
pub list_active_border: Hsla,
pub list_hover: Hsla,
pub table: Hsla,
pub table_even: Hsla,
@ -333,6 +331,7 @@ pub struct Theme {
pub table_head_foreground: Hsla,
pub table_row_border: Hsla,
pub table_active: Hsla,
pub table_active_border: Hsla,
pub table_hover: Hsla,
pub link: Hsla,
pub link_hover: Hsla,
@ -378,7 +377,7 @@ impl Theme {
self.border = self.border.apply(mask_color);
self.input = self.input.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_thumb = self.scrollbar_thumb.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_even = self.list_even.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.table = self.table.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_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.link = self.link.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_even: colors.list_even,
list_head: colors.list_head,
list_active: colors.list_active,
list_hover: colors.list_active.opacity(0.6),
list_active: colors.selection.opacity(0.2),
list_active_border: colors.selection,
list_hover: colors.selection.opacity(0.2),
table_head: colors.list_head,
table: colors.list,
table_even: colors.list_even,
table_active: colors.list_active,
table_hover: colors.list_active.opacity(0.8),
table_active: colors.selection.opacity(0.2),
table_active_border: colors.selection,
table_hover: colors.selection.opacity(0.2),
table_row_border: colors.border.opacity(0.5),
table_head_foreground: colors.foreground.opacity(0.7),
link: colors.link,