From ab0a3f19ab363acbd2a9990e7fd0bb3d4e33bff8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 16 Nov 2023 15:13:04 +0200 Subject: [PATCH] Add an actual context menu into terminal-2 (click the text, not the pane!) Co-Authored-By: Piotr --- Cargo.lock | 16 +- Cargo.toml | 1 - crates/context_menu2/Cargo.toml | 19 - crates/context_menu2/src/context_menu.rs | 557 --------------------- crates/terminal_view2/Cargo.toml | 1 - crates/terminal_view2/src/terminal_view.rs | 82 +-- crates/ui2/Cargo.toml | 1 + crates/ui2/src/components/context_menu.rs | 47 +- crates/ui2/src/components/label.rs | 2 +- crates/ui2/src/components/list.rs | 30 +- crates/zed2/Cargo.toml | 1 - crates/zed2/src/main.rs | 1 - 12 files changed, 112 insertions(+), 646 deletions(-) delete mode 100644 crates/context_menu2/Cargo.toml delete mode 100644 crates/context_menu2/src/context_menu.rs diff --git a/Cargo.lock b/Cargo.lock index 87c370f59e82cfa36f8452ca4477453739ea1391..bf2e964ea8f533a8a0c0ada0e60077fdd140caa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1989,19 +1989,6 @@ dependencies = [ "theme", ] -[[package]] -name = "context_menu2" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui2", - "menu2", - "settings2", - "smallvec", - "theme2", - "workspace2", -] - [[package]] name = "convert_case" version = "0.4.0" @@ -9194,7 +9181,6 @@ version = "0.1.0" dependencies = [ "anyhow", "client2", - "context_menu2", "db2", "dirs 4.0.0", "editor2", @@ -10146,6 +10132,7 @@ dependencies = [ "chrono", "gpui2", "itertools 0.11.0", + "menu2", "rand 0.8.5", "serde", "settings2", @@ -11532,7 +11519,6 @@ dependencies = [ "collab_ui2", "collections", "command_palette2", - "context_menu2", "copilot2", "ctor", "db2", diff --git a/Cargo.toml b/Cargo.toml index 65c7630077e93cf8c23158ae175b359fdba3b7f4..f8d0af77fa85220f348c866edfe5242d0cccbeec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ members = [ "crates/command_palette2", "crates/component_test", "crates/context_menu", - "crates/context_menu2", "crates/copilot", "crates/copilot2", "crates/copilot_button", diff --git a/crates/context_menu2/Cargo.toml b/crates/context_menu2/Cargo.toml deleted file mode 100644 index 5c8aae03a1007d85c32df61ad64f5fcb0b5aca49..0000000000000000000000000000000000000000 --- a/crates/context_menu2/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "context_menu2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/context_menu.rs" -doctest = false - -[dependencies] -gpui = { package = "gpui2", path = "../gpui2" } -menu = { package = "menu2", path = "../menu2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -workspace = { package = "workspace2", path = "../workspace2" } - -anyhow.workspace = true -smallvec.workspace = true diff --git a/crates/context_menu2/src/context_menu.rs b/crates/context_menu2/src/context_menu.rs deleted file mode 100644 index 885c5c8521a91e1a4a44ed362b8d498970fa1481..0000000000000000000000000000000000000000 --- a/crates/context_menu2/src/context_menu.rs +++ /dev/null @@ -1,557 +0,0 @@ -#![allow(unused_variables, unused)] -//todo!(remove) - -use gpui::{ - div, Action, AnchorCorner, AnyElement, AppContext, BorrowWindow, Div, EntityId, FocusHandle, - FocusableView, Pixels, Point, Render, ViewContext, -}; -use menu::*; - -use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration}; - -pub fn init(cx: &mut AppContext) { - // todo!() - // cx.observe_new_views( - // |workspace: &mut Workspace, _: &mut ViewContext| { - // workspace.register_action(ContextMenu::select_first); - // workspace.register_action(ContextMenu::select_last); - // workspace.register_action(ContextMenu::select_next); - // workspace.register_action(ContextMenu::select_prev); - // workspace.register_action(ContextMenu::confirm); - // workspace.register_action(ContextMenu::cancel); - // }, - // ) - // .detach(); -} - -pub type StaticItem = Box AnyElement>; - -type ContextMenuItemBuilder = (); -// todo!() -// Box AnyElement>; - -pub enum ContextMenuItemLabel { - String(Cow<'static, str>), - Element(ContextMenuItemBuilder), -} - -impl From> for ContextMenuItemLabel { - fn from(s: Cow<'static, str>) -> Self { - Self::String(s) - } -} - -impl From<&'static str> for ContextMenuItemLabel { - fn from(s: &'static str) -> Self { - Self::String(s.into()) - } -} - -impl From for ContextMenuItemLabel { - fn from(s: String) -> Self { - Self::String(s.into()) - } -} - -// todo!() -// impl From for ContextMenuItemLabel -// where -// T: 'static + Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement, -// { -// fn from(f: T) -> Self { -// Self::Element(Box::new(f)) -// } -// } - -pub enum ContextMenuItemAction { - Action(Box), - Handler(Arc)>), -} - -impl Clone for ContextMenuItemAction { - fn clone(&self) -> Self { - match self { - Self::Action(action) => Self::Action(action.boxed_clone()), - Self::Handler(handler) => Self::Handler(handler.clone()), - } - } -} - -pub enum ContextMenuItem { - Item { - label: ContextMenuItemLabel, - action: ContextMenuItemAction, - }, - Static(StaticItem), - Separator, -} - -impl ContextMenuItem { - pub fn action(label: impl Into, action: impl 'static + Action) -> Self { - Self::Item { - label: label.into(), - action: ContextMenuItemAction::Action(Box::new(action)), - } - } - - pub fn handler( - label: impl Into, - handler: impl 'static + Fn(&mut ViewContext), - ) -> Self { - Self::Item { - label: label.into(), - action: ContextMenuItemAction::Handler(Arc::new(handler)), - } - } - - pub fn separator() -> Self { - Self::Separator - } - - fn is_action(&self) -> bool { - matches!(self, Self::Item { .. }) - } - - fn action_id(&self) -> Option { - match self { - ContextMenuItem::Item { action, .. } => match action { - ContextMenuItemAction::Action(action) => Some(action.type_id()), - ContextMenuItemAction::Handler(_) => None, - }, - ContextMenuItem::Static(..) | ContextMenuItem::Separator => None, - } - } -} - -pub struct ContextMenu { - show_count: usize, - anchor_position: Point, - anchor_corner: AnchorCorner, - // todo!() - // position_mode: OverlayPositionMode, - items: Vec, - selected_index: Option, - visible: bool, - delay_cancel: bool, - previously_focused_view_handle: Option, - parent_view_id: EntityId, - focus_handle: FocusHandle, - // todo!() - // _actions_observation: Subscription, -} - -impl FocusableView for ContextMenu { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -// todo!() -// fn ui_name() -> &'static str { -// "ContextMenu" -// } -// -// fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { -// Self::reset_to_default_keymap_context(keymap); -// keymap.add_identifier("menu"); -// } -// -// fn focus_out(&mut self, _: AnyView, cx: &mut ViewContext) { -// self.reset(cx); -// } - -impl Render for ContextMenu { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - if !self.visible { - return div(); - } - - // todo!() - // // Render the menu once at minimum width. - // let mut collapsed_menu = self.render_menu_for_measurement(cx); - // let expanded_menu = self - // .render_menu(cx) - // .dynamically(move |constraint, view, cx| { - // SizeConstraint::strict_along( - // Axis::Horizontal, - // collapsed_menu.layout(constraint, view, cx).0.x(), - // ) - // }); - - // Overlay::new(expanded_menu) - // .with_hoverable(true) - // .with_fit_mode(OverlayFitMode::SnapToWindow) - // .with_anchor_position(self.anchor_position) - // .with_anchor_corner(self.anchor_corner) - // .with_position_mode(self.position_mode) - // .into_any() - div() - } -} - -impl ContextMenu { - pub fn new(parent_view_id: EntityId, cx: &mut ViewContext) -> Self { - Self { - show_count: 0, - delay_cancel: false, - anchor_position: Default::default(), - anchor_corner: AnchorCorner::TopLeft, - // todo!() - // position_mode: OverlayPositionMode::Window, - items: Default::default(), - selected_index: Default::default(), - visible: Default::default(), - previously_focused_view_handle: None, - parent_view_id, - // todo!() - // _actions_observation: cx.observe_actions(Self::action_dispatched), - focus_handle: cx.focus_handle(), - } - } - - pub fn visible(&self) -> bool { - self.visible - } - - fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext) { - if let Some(ix) = self - .items - .iter() - .position(|item| item.action_id() == Some(action_id)) - { - self.selected_index = Some(ix); - cx.notify(); - cx.spawn(|this, mut cx| async move { - cx.background_executor() - .timer(Duration::from_millis(50)) - .await; - this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(ix) = self.selected_index { - if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) { - match action { - ContextMenuItemAction::Action(action) => { - let window = cx.window(); - let view_id = self.parent_view_id; - let action = action.boxed_clone(); - // todo!() - // cx.app_context() - // .spawn(|mut cx| async move { - // window - // .dispatch_action(view_id, action.as_ref(), &mut cx) - // .ok_or_else(|| anyhow!("window was closed")) - // }) - // .detach_and_log_err(cx); - } - ContextMenuItemAction::Handler(handler) => handler(cx), - } - self.reset(cx); - } - } - } - - pub fn delay_cancel(&mut self) { - self.delay_cancel = true; - } - - fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if !self.delay_cancel { - self.reset(cx); - let show_count = self.show_count; - cx.defer(move |this, cx| { - if this.focus_handle.is_focused(cx) && this.show_count == show_count { - if let Some(previously_focused_view_handle) = - this.previously_focused_view_handle.take() - { - previously_focused_view_handle.focus(cx); - } - } - }); - } else { - self.delay_cancel = false; - } - } - - fn reset(&mut self, cx: &mut ViewContext) { - self.items.clear(); - self.visible = false; - self.selected_index.take(); - cx.notify(); - } - - fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { - self.selected_index = self.items.iter().position(|item| item.is_action()); - cx.notify(); - } - - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { - for (ix, item) in self.items.iter().enumerate().rev() { - if item.is_action() { - self.selected_index = Some(ix); - cx.notify(); - break; - } - } - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(ix) = self.selected_index { - for (ix, item) in self.items.iter().enumerate().skip(ix + 1) { - if item.is_action() { - self.selected_index = Some(ix); - cx.notify(); - break; - } - } - } else { - self.select_first(&Default::default(), cx); - } - } - - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(ix) = self.selected_index { - for (ix, item) in self.items.iter().enumerate().take(ix).rev() { - if item.is_action() { - self.selected_index = Some(ix); - cx.notify(); - break; - } - } - } else { - self.select_last(&Default::default(), cx); - } - } - - pub fn toggle( - &mut self, - anchor_position: Point, - anchor_corner: AnchorCorner, - items: Vec, - cx: &mut ViewContext, - ) { - if self.visible() { - self.cancel(&Cancel, cx); - } else { - let mut items = items.into_iter().peekable(); - if items.peek().is_some() { - self.items = items.collect(); - self.anchor_position = anchor_position; - self.anchor_corner = anchor_corner; - self.visible = true; - self.show_count += 1; - if !self.focus_handle.is_focused(cx) { - self.previously_focused_view_handle = cx.focused(); - } - cx.focus_self(); - } else { - self.visible = false; - } - } - cx.notify(); - } - - pub fn show( - &mut self, - anchor_position: Point, - anchor_corner: AnchorCorner, - items: Vec, - cx: &mut ViewContext, - ) { - let mut items = items.into_iter().peekable(); - if items.peek().is_some() { - self.items = items.collect(); - self.anchor_position = anchor_position; - self.anchor_corner = anchor_corner; - self.visible = true; - self.show_count += 1; - if !self.focus_handle.is_focused(cx) { - self.previously_focused_view_handle = cx.focused(); - } - cx.focus_self(); - } else { - self.visible = false; - } - cx.notify(); - } - - // todo!() - // pub fn set_position_mode(&mut self, mode: OverlayPositionMode) { - // self.position_mode = mode; - // } - - fn render_menu_for_measurement(&self, cx: &mut ViewContext) -> Div { - // let style = theme::current(cx).context_menu.clone(); - // Flex::row() - // .with_child( - // Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| { - // match item { - // ContextMenuItem::Item { label, .. } => { - // let style = style.item.in_state(self.selected_index == Some(ix)); - // let style = style.style_for(&mut Default::default()); - - // match label { - // ContextMenuItemLabel::String(label) => { - // Label::new(label.to_string(), style.label.clone()) - // .contained() - // .with_style(style.container) - // .into_any() - // } - // ContextMenuItemLabel::Element(element) => { - // element(&mut Default::default(), style) - // } - // } - // } - - // ContextMenuItem::Static(f) => f(cx), - - // ContextMenuItem::Separator => Empty::new() - // .collapsed() - // .contained() - // .with_style(style.separator) - // .constrained() - // .with_height(1.) - // .into_any(), - // } - // })), - // ) - // .with_child( - // Flex::column() - // .with_children(self.items.iter().enumerate().map(|(ix, item)| { - // match item { - // ContextMenuItem::Item { action, .. } => { - // let style = style.item.in_state(self.selected_index == Some(ix)); - // let style = style.style_for(&mut Default::default()); - - // match action { - // ContextMenuItemAction::Action(action) => KeystrokeLabel::new( - // self.parent_view_id, - // action.boxed_clone(), - // style.keystroke.container, - // style.keystroke.text.clone(), - // ) - // .into_any(), - // ContextMenuItemAction::Handler(_) => Empty::new().into_any(), - // } - // } - - // ContextMenuItem::Static(_) => Empty::new().into_any(), - - // ContextMenuItem::Separator => Empty::new() - // .collapsed() - // .constrained() - // .with_height(1.) - // .contained() - // .with_style(style.separator) - // .into_any(), - // } - // })) - // .contained() - // .with_margin_left(style.keystroke_margin), - // ) - // .contained() - // .with_style(style.container) - todo!() - } - - fn render_menu(&self, cx: &mut ViewContext) -> Div { - enum Menu {} - enum MenuItem {} - - // let style = theme::current(cx).context_menu.clone(); - - // MouseEventHandler::new::(0, cx, |_, cx| { - // Flex::column() - // .with_children(self.items.iter().enumerate().map(|(ix, item)| { - // match item { - // ContextMenuItem::Item { label, action } => { - // let action = action.clone(); - // let view_id = self.parent_view_id; - // MouseEventHandler::new::(ix, cx, |state, _| { - // let style = style.item.in_state(self.selected_index == Some(ix)); - // let style = style.style_for(state); - // let keystroke = match &action { - // ContextMenuItemAction::Action(action) => Some( - // KeystrokeLabel::new( - // view_id, - // action.boxed_clone(), - // style.keystroke.container, - // style.keystroke.text.clone(), - // ) - // .flex_float(), - // ), - // ContextMenuItemAction::Handler(_) => None, - // }; - - // Flex::row() - // .with_child(match label { - // ContextMenuItemLabel::String(label) => { - // Label::new(label.clone(), style.label.clone()) - // .contained() - // .into_any() - // } - // ContextMenuItemLabel::Element(element) => { - // element(state, style) - // } - // }) - // .with_children(keystroke) - // .contained() - // .with_style(style.container) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_up(MouseButton::Left, |_, _, _| {}) // Capture these events - // .on_down(MouseButton::Left, |_, _, _| {}) // Capture these events - // .on_click(MouseButton::Left, move |_, menu, cx| { - // menu.cancel(&Default::default(), cx); - // let window = cx.window(); - // match &action { - // ContextMenuItemAction::Action(action) => { - // let action = action.boxed_clone(); - // cx.app_context() - // .spawn(|mut cx| async move { - // window - // .dispatch_action( - // view_id, - // action.as_ref(), - // &mut cx, - // ) - // .ok_or_else(|| anyhow!("window was closed")) - // }) - // .detach_and_log_err(cx); - // } - // ContextMenuItemAction::Handler(handler) => handler(cx), - // } - // }) - // .on_drag(MouseButton::Left, |_, _, _| {}) - // .into_any() - // } - - // ContextMenuItem::Static(f) => f(cx), - - // ContextMenuItem::Separator => Empty::new() - // .constrained() - // .with_height(1.) - // .contained() - // .with_style(style.separator) - // .into_any(), - // } - // })) - // .contained() - // .with_style(style.container) - // }) - // .on_down_out(MouseButton::Left, |_, this, cx| { - // this.cancel(&Default::default(), cx); - // }) - // .on_down_out(MouseButton::Right, |_, this, cx| { - // this.cancel(&Default::default(), cx); - // }) - todo!() - } -} diff --git a/crates/terminal_view2/Cargo.toml b/crates/terminal_view2/Cargo.toml index 714ccb60a2ca3cfb2bdc651de2991dbd8d5a6486..12e2c06504fad5adc7e502c4225a28c9bf985f30 100644 --- a/crates/terminal_view2/Cargo.toml +++ b/crates/terminal_view2/Cargo.toml @@ -9,7 +9,6 @@ path = "src/terminal_view.rs" doctest = false [dependencies] -context_menu = { package = "context_menu2", path = "../context_menu2" } editor = { package = "editor2", path = "../editor2" } language = { package = "language2", path = "../language2" } gpui = { package = "gpui2", path = "../gpui2" } diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index ad47e720e25f6d0fbd3b7fba5f58c4bffc0faea7..14391ca2b2f357f56f084a5688601182bad780cf 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -7,12 +7,11 @@ pub mod terminal_panel; // todo!() // use crate::terminal_element::TerminalElement; -use context_menu::{ContextMenu, ContextMenuItem}; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, img, red, register_action, AnchorCorner, AnyElement, AppContext, Component, - DispatchPhase, Div, EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, - FocusableView, InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, + actions, div, img, red, register_action, AnyElement, AppContext, Component, DispatchPhase, Div, + EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, + InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton, ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, }; @@ -33,6 +32,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, + ui::{ContextMenu, ContextMenuItem, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -66,6 +66,7 @@ pub struct SendKeystroke(String); actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest); pub fn init(cx: &mut AppContext) { + workspace::ui::init(cx); terminal_panel::init(cx); terminal::init(cx); @@ -86,7 +87,7 @@ pub struct TerminalView { has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, - context_menu: View, + context_menu: Option, blink_state: bool, blinking_on: bool, blinking_paused: bool, @@ -269,7 +270,7 @@ impl TerminalView { has_new_content: true, has_bell: false, focus_handle: cx.focus_handle(), - context_menu: cx.build_view(|cx| ContextMenu::new(view_id, cx)), + context_menu: None, blink_state: true, blinking_on: false, blinking_paused: false, @@ -301,16 +302,15 @@ impl TerminalView { position: gpui::Point, cx: &mut ViewContext, ) { - let menu_entries = vec![ - ContextMenuItem::action("Clear", Clear), - ContextMenuItem::action("Close", CloseActiveItem { save_intent: None }), - ]; - - self.context_menu.update(cx, |menu, cx| { - menu.show(position, AnchorCorner::TopLeft, menu_entries, cx) - }); - - cx.notify(); + self.context_menu = Some(ContextMenu::new(vec![ + ContextMenuItem::entry(Label::new("Clear"), Clear), + ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }), + ])); + dbg!(&position); + // todo!() + // self.context_menu + // .show(position, AnchorCorner::TopLeft, menu_entries, cx); + // cx.notify(); } fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { @@ -547,27 +547,41 @@ impl Render for TerminalView { let focused = self.focus_handle.is_focused(cx); div() + .relative() + .child( + div() + .z_index(0) + .absolute() + .on_key_down(Self::key_down) + .on_action(TerminalView::send_text) + .on_action(TerminalView::send_keystroke) + .on_action(TerminalView::copy) + .on_action(TerminalView::paste) + .on_action(TerminalView::clear) + .on_action(TerminalView::show_character_palette) + .on_action(TerminalView::select_all) + // todo!() + .child( + "TERMINAL HERE", // TerminalElement::new( + // terminal_handle, + // focused, + // self.should_show_cursor(focused, cx), + // self.can_navigate_to_selected_word, + // ) + ) + .on_mouse_down(MouseButton::Right, |this, event, cx| { + this.deploy_context_menu(event.position, cx); + cx.notify(); + }), + ) + .children( + self.context_menu + .clone() + .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())), + ) .track_focus(&self.focus_handle) .on_focus_in(Self::focus_in) .on_focus_out(Self::focus_out) - .on_key_down(Self::key_down) - .on_action(TerminalView::send_text) - .on_action(TerminalView::send_keystroke) - .on_action(TerminalView::copy) - .on_action(TerminalView::paste) - .on_action(TerminalView::clear) - .on_action(TerminalView::show_character_palette) - .on_action(TerminalView::select_all) - // todo!() - .child( - "TERMINAL HERE", // TerminalElement::new( - // terminal_handle, - // focused, - // self.should_show_cursor(focused, cx), - // self.can_navigate_to_selected_word, - // ) - ) - .child(self.context_menu.clone()) } } diff --git a/crates/ui2/Cargo.toml b/crates/ui2/Cargo.toml index 754bca371f41e741356f1ad9df670de2154e9978..0a7de6299d1f0999df813ced30902c039615f662 100644 --- a/crates/ui2/Cargo.toml +++ b/crates/ui2/Cargo.toml @@ -9,6 +9,7 @@ anyhow.workspace = true chrono = "0.4" gpui = { package = "gpui2", path = "../gpui2" } itertools = { version = "0.11.0", optional = true } +menu = { package = "menu2", path = "../menu2"} serde.workspace = true settings2 = { path = "../settings2" } smallvec.workspace = true diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 117be127798bd56f9d0c3963f8119832aa170550..8f32c3ed5664ede770af66580e0abe2f7379f767 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -3,17 +3,29 @@ use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHea pub enum ContextMenuItem { Header(SharedString), - Entry(Label), + Entry(Label, Box), Separator, } +impl Clone for ContextMenuItem { + fn clone(&self) -> Self { + match self { + ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()), + ContextMenuItem::Entry(label, action) => { + ContextMenuItem::Entry(label.clone(), action.boxed_clone()) + } + ContextMenuItem::Separator => ContextMenuItem::Separator, + } + } +} impl ContextMenuItem { fn to_list_item(self) -> ListItem { match self { ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), - ContextMenuItem::Entry(label) => { - ListEntry::new(label).variant(ListItemVariant::Inset).into() - } + ContextMenuItem::Entry(label, action) => ListEntry::new(label) + .variant(ListItemVariant::Inset) + .on_click(action) + .into(), ContextMenuItem::Separator => ListSeparator::new().into(), } } @@ -26,12 +38,12 @@ impl ContextMenuItem { Self::Separator } - pub fn entry(label: Label) -> Self { - Self::Entry(label) + pub fn entry(label: Label, action: impl Action) -> Self { + Self::Entry(label, Box::new(action)) } } -#[derive(Component)] +#[derive(Component, Clone)] pub struct ContextMenu { items: Vec, } @@ -42,7 +54,12 @@ impl ContextMenu { items: items.into_iter().collect(), } } - + // todo!() + // cx.add_action(ContextMenu::select_first); + // cx.add_action(ContextMenu::select_last); + // cx.add_action(ContextMenu::select_next); + // cx.add_action(ContextMenu::select_prev); + // cx.add_action(ContextMenu::confirm); fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { v_stack() .flex() @@ -55,9 +72,11 @@ impl ContextMenu { .map(ContextMenuItem::to_list_item::) .collect(), )) + .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel))) } } +use gpui::Action; #[cfg(feature = "stories")] pub use stories::*; @@ -65,7 +84,7 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{Div, Render}; + use gpui::{action, Div, Render}; pub struct ContextMenuStory; @@ -73,14 +92,22 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + #[action] + struct PrintCurrentDate {} + Story::container(cx) .child(Story::title_for::<_, ContextMenu>(cx)) .child(Story::label(cx, "Default")) .child(ContextMenu::new([ ContextMenuItem::header("Section header"), ContextMenuItem::Separator, - ContextMenuItem::entry(Label::new("Some entry")), + ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}), ])) + .on_action(|_, _: &PrintCurrentDate, _| { + if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { + println!("Current Unix time is {:?}", unix_time.as_secs()); + } + }) } } } diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index cbb75278c29487e0b5466421ce33f52342e60fed..1beee5c8b731bdcf984e53818bec4e960f6f41da 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -60,7 +60,7 @@ pub enum LineHeightStyle { UILabel, } -#[derive(Component)] +#[derive(Clone, Component)] pub struct Label { label: SharedString, size: LabelSize, diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 1ddad269dd752fbfce95028c11fcc555f7bbfeb0..4b355dd5b6c159c906117e64b3008f8d100129c7 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,11 +1,10 @@ -use gpui::div; +use gpui::{div, Action}; -use crate::prelude::*; use crate::settings::user_settings; use crate::{ - disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label, - TextColor, Toggle, + disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle, }; +use crate::{prelude::*, GraphicSlot}; #[derive(Clone, Copy, Default, Debug, PartialEq)] pub enum ListItemVariant { @@ -232,6 +231,7 @@ pub struct ListEntry { size: ListEntrySize, toggle: Toggle, variant: ListItemVariant, + on_click: Option>, } impl ListEntry { @@ -245,9 +245,15 @@ impl ListEntry { size: ListEntrySize::default(), toggle: Toggle::NotToggleable, variant: ListItemVariant::default(), + on_click: Default::default(), } } + pub fn on_click(mut self, action: impl Into>) -> Self { + self.on_click = Some(action.into()); + self + } + pub fn variant(mut self, variant: ListItemVariant) -> Self { self.variant = variant; self @@ -303,9 +309,21 @@ impl ListEntry { ListEntrySize::Small => div().h_6(), ListEntrySize::Medium => div().h_7(), }; - div() .relative() + .hover(|mut style| { + style.background = Some(cx.theme().colors().editor_background.into()); + style + }) + .on_mouse_down(gpui::MouseButton::Left, { + let action = self.on_click.map(|action| action.boxed_clone()); + + move |entry: &mut V, event, cx| { + if let Some(action) = action.as_ref() { + cx.dispatch_action(action.boxed_clone()); + } + } + }) .group("") .bg(cx.theme().colors().surface_background) // TODO: Add focus state @@ -401,7 +419,7 @@ impl List { v_stack() .w_full() .py_1() - .children(self.header) + .children(self.header.map(|header| header)) .child(list_content) } } diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 398a7a8920562c27e1093df31dab4c90211661f6..df3332803e790a7d7e32575487b40e041ee9e5cb 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -27,7 +27,6 @@ collab_ui = { package = "collab_ui2", path = "../collab_ui2" } collections = { path = "../collections" } command_palette = { package="command_palette2", path = "../command_palette2" } # component_test = { path = "../component_test" } -context_menu = { package = "context_menu2", path = "../context_menu2" } client = { package = "client2", path = "../client2" } # clock = { path = "../clock" } copilot = { package = "copilot2", path = "../copilot2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 581913b752fb8e07f8b6f594ea2a14f003a016c2..ef65d00fc386503d68a6aaa457ecadabbf21e1a0 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -141,7 +141,6 @@ fn main() { cx.set_global(client.clone()); theme::init(cx); - context_menu::init(cx); project::Project::init(&client, cx); client::init(&client, cx); command_palette::init(cx);