From 4bef681c9a7bb029a13cb328382c317f6cbf9730 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 18 Oct 2024 11:52:14 +0800 Subject: [PATCH] table, list: Add right clicked row style and improve selection style. (#362) - table: Add `content_menu` to TableDelegate. image --- crates/story/src/table_story.rs | 19 +++---- crates/ui/src/context_menu.rs | 44 +++++++++------- crates/ui/src/list/list.rs | 89 +++++++++++++++++++++++---------- crates/ui/src/popup_menu.rs | 4 ++ crates/ui/src/table.rs | 75 ++++++++++++++++++++++++--- crates/ui/src/theme.rs | 24 +++++---- 6 files changed, 179 insertions(+), 76 deletions(-) diff --git a/crates/story/src/table_story.rs b/crates/story/src/table_story.rs index 7244da03..cb7afa1f 100644 --- a/crates/story/src/table_story.rs +++ b/crates/story/src/table_story.rs @@ -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>, - ) -> gpui::Stateful { - 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( diff --git a/crates/ui/src/context_menu.rs b/crates/ui/src/context_menu.rs index 2d968760..65c12f07 100644 --- a/crates/ui/src/context_menu.rs +++ b/crates/ui/src/context_menu.rs @@ -16,7 +16,7 @@ pub trait ContextMenuExt: ParentElement + Sized { self, f: impl Fn(PopupMenu, &mut ViewContext) -> 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; diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 910a176e..31b4aae4 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -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 { pub(crate) size: Size, selected_index: Option, + right_clicked_index: Option, _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) -> 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 FocusableView for List @@ -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::>() } }) @@ -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(); + })) + }) } } diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index 8dd1eea8..cf378440 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -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 { self.menu_items .iter() diff --git a/crates/ui/src/table.rs b/crates/ui/src/table.rs index 4859c08f..2eb6b1b4 100644 --- a/crates/ui/src/table.rs +++ b/crates/ui/src/table.rs @@ -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 { selection_state: SelectionState, selected_row: Option, + right_clicked_row: Option, selected_col: Option, /// 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.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.set_selected_row(row_ix, cx) + fn on_row_click( + &mut self, + mouse_button: MouseButton, + row_ix: usize, + cx: &mut ViewContext, + ) { + 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) { @@ -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| { + 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(); + })) + }) } } diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index d86cf062..71d8a19d 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -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 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,