Add a simple context menu into terminal2 (#3343)

Kirill Bulatov created

Release Notes:

- N/A

Change summary

Cargo.lock                                    |   1 
crates/terminal_view2/Cargo.toml              |   1 
crates/terminal_view2/src/terminal_element.rs |  62 +++++-----
crates/terminal_view2/src/terminal_view.rs    | 115 ++++++++++++--------
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 
10 files changed, 163 insertions(+), 98 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10132,6 +10132,7 @@ dependencies = [
  "chrono",
  "gpui2",
  "itertools 0.11.0",
+ "menu2",
  "rand 0.8.5",
  "serde",
  "settings2",

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" }

crates/terminal_view2/src/terminal_element.rs 🔗

@@ -1,8 +1,8 @@
 // use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
 // use gpui::{
-//     AnyElement, AppContext, Bounds, Component, Element, HighlightStyle, Hsla, LayoutId, Line,
-//     ModelContext, MouseButton, Pixels, Point, TextStyle, Underline, ViewContext, WeakModel,
-//     WindowContext,
+//     point, transparent_black, AnyElement, AppContext, Bounds, Component, CursorStyle, Element,
+//     FontStyle, FontWeight, HighlightStyle, Hsla, LayoutId, Line, ModelContext, MouseButton,
+//     Overlay, Pixels, Point, Quad, TextStyle, Underline, ViewContext, WeakModel, WindowContext,
 // };
 // use itertools::Itertools;
 // use language::CursorShape;
@@ -23,6 +23,7 @@
 //     TerminalSize,
 // };
 // use theme::ThemeSettings;
+// use workspace::ElementId;
 
 // use std::mem;
 // use std::{fmt::Debug, ops::RangeInclusive};
@@ -130,23 +131,24 @@
 //         cx: &mut ViewContext<TerminalView>,
 //     ) {
 //         let position = {
-//             let point = self.point;
-//             vec2f(
-//                 (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
-//                 origin.y() + point.line as f32 * layout.size.line_height,
+//             let alac_point = self.point;
+//             point(
+//                 (origin.x + alac_point.column as f32 * layout.size.cell_width).floor(),
+//                 origin.y + alac_point.line as f32 * layout.size.line_height,
 //             )
 //         };
-//         let size = vec2f(
+//         let size = point(
 //             (layout.size.cell_width * self.num_of_cells as f32).ceil(),
 //             layout.size.line_height,
-//         );
+//         )
+//         .into();
 
 //         cx.paint_quad(
 //             Bounds::new(position, size),
 //             Default::default(),
 //             self.color,
 //             Default::default(),
-//             Default::default(),
+//             transparent_black(),
 //         );
 //     }
 // }
@@ -281,9 +283,9 @@
 //         cursor_point: DisplayCursor,
 //         size: TerminalSize,
 //         text_fragment: &Line,
-//     ) -> Option<(Vector2F, f32)> {
+//     ) -> Option<(Point<Pixels>, Pixels)> {
 //         if cursor_point.line() < size.total_lines() as i32 {
-//             let cursor_width = if text_fragment.width == 0. {
+//             let cursor_width = if text_fragment.width == Pixels::ZERO {
 //                 size.cell_width()
 //             } else {
 //                 text_fragment.width
@@ -292,7 +294,7 @@
 //             //Cursor should always surround as much of the text as possible,
 //             //hence when on pixel boundaries round the origin down and the width up
 //             Some((
-//                 vec2f(
+//                 point(
 //                     (cursor_point.col() as f32 * size.cell_width()).floor(),
 //                     (cursor_point.line() as f32 * size.line_height()).floor(),
 //                 ),
@@ -332,15 +334,15 @@
 
 //         let mut properties = Properties::new();
 //         if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
-//             properties = *properties.weight(Weight::BOLD);
+//             properties = *properties.weight(FontWeight::BOLD);
 //         }
 //         if indexed.flags.intersects(Flags::ITALIC) {
-//             properties = *properties.style(Italic);
+//             properties = *properties.style(FontStyle::Italic);
 //         }
 
 //         let font_id = font_cache
-//             .select_font(text_style.font_family_id, &properties)
-//             .unwrap_or(8text_style.font_id);
+//             .select_font(text_style.font_family, &properties)
+//             .unwrap_or(text_style.font_id);
 
 //         let mut result = RunStyle {
 //             color: fg,
@@ -366,7 +368,7 @@
 //     fn generic_button_handler<E>(
 //         connection: WeakModel<Terminal>,
 //         origin: Point<Pixels>,
-//         f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
+//         f: impl Fn(&mut Terminal, Point<Pixels>, E, &mut ModelContext<Terminal>),
 //     ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
 //         move |event, _: &mut TerminalView, cx| {
 //             cx.focus_parent();
@@ -522,9 +524,9 @@
 //     fn layout(
 //         &mut self,
 //         view_state: &mut TerminalView,
-//         element_state: &mut Self::ElementState,
+//         element_state: Option<Self::ElementState>,
 //         cx: &mut ViewContext<TerminalView>,
-//     ) -> LayoutId {
+//     ) -> (LayoutId, Self::ElementState) {
 //         let settings = ThemeSettings::get_global(cx);
 //         let terminal_settings = TerminalSettings::get_global(cx);
 
@@ -569,7 +571,7 @@
 //             let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
 //             gutter = cell_width;
 
-//             let size = constraint.max - vec2f(gutter, 0.);
+//             let size = constraint.max - point(gutter, 0.);
 //             TerminalSize::new(line_height, cell_width, size)
 //         };
 
@@ -607,11 +609,11 @@
 //                         cx,
 //                     ),
 //             )
-//             .with_position_mode(gpui::elements::OverlayPositionMode::Local)
+//             .with_position_mode(gpui::OverlayPositionMode::Local)
 //             .into_any();
 
 //             tooltip.layout(
-//                 SizeConstraint::new(Vector2F::zero(), cx.window_size()),
+//                 SizeConstraint::new(Point::zero(), cx.window_size()),
 //                 view_state,
 //                 cx,
 //             );
@@ -735,7 +737,7 @@
 //         let clip_bounds = Some(visible_bounds);
 
 //         cx.paint_layer(clip_bounds, |cx| {
-//             let origin = bounds.origin() + vec2f(element_state.gutter, 0.);
+//             let origin = bounds.origin + point(element_state.gutter, 0.);
 
 //             // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
 //             self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx);
@@ -808,7 +810,7 @@
 //         });
 //     }
 
-//     fn id(&self) -> Option<gpui::ElementId> {
+//     fn element_id(&self) -> Option<ElementId> {
 //         todo!()
 //     }
 
@@ -842,12 +844,12 @@
 //     // ) -> Option<Bounds<Pixels>> {
 //     //     // Use the same origin that's passed to `Cursor::paint` in the paint
 //     //     // method bove.
-//     //     let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
+//     //     let mut origin = bounds.origin() + point(layout.size.cell_width, 0.);
 
 //     //     // TODO - Why is it necessary to move downward one line to get correct
 //     //     // positioning? I would think that we'd want the same rect that is
 //     //     // painted for the cursor.
-//     //     origin += vec2f(0., layout.size.line_height);
+//     //     origin += point(0., layout.size.line_height);
 
 //     //     Some(layout.cursor.as_ref()?.bounding_rect(origin))
 //     // }
@@ -886,7 +888,7 @@
 //     range: &RangeInclusive<AlacPoint>,
 //     layout: &LayoutState,
 //     origin: Point<Pixels>,
-// ) -> Option<(f32, Vec<HighlightedRangeLine>)> {
+// ) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
 //     // Step 1. Normalize the points to be viewport relative.
 //     // When display_offset = 1, here's how the grid is arranged:
 //     //-2,0 -2,1...
@@ -937,8 +939,8 @@
 //         }
 
 //         highlighted_range_lines.push(HighlightedRangeLine {
-//             start_x: origin.x() + line_start as f32 * layout.size.cell_width,
-//             end_x: origin.x() + line_end as f32 * layout.size.cell_width,
+//             start_x: origin.x + line_start as f32 * layout.size.cell_width,
+//             end_x: origin.x + line_end as f32 * layout.size.cell_width,
 //         });
 //     }
 

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -7,27 +7,17 @@ pub mod terminal_panel;
 
 // todo!()
 // use crate::terminal_element::TerminalElement;
-use anyhow::Context;
-use dirs::home_dir;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
     actions, div, img, red, register_action, AnyElement, AppContext, Component, DispatchPhase, Div,
     EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
-    InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, ParentComponent, Pixels,
-    Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView,
+    InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
+    ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
 use language::Bias;
 use persistence::TERMINAL_DB;
 use project::{search::SearchQuery, LocalWorktree, Project};
-use serde::Deserialize;
-use settings::Settings;
-use smol::Timer;
-use std::{
-    ops::RangeInclusive,
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::Duration,
-};
 use terminal::{
     alacritty_terminal::{
         index::Point,
@@ -42,7 +32,21 @@ use workspace::{
     notifications::NotifyResultExt,
     register_deserializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem},
-    NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+    ui::{ContextMenu, ContextMenuItem, Label},
+    CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+};
+
+use anyhow::Context;
+use dirs::home_dir;
+use serde::Deserialize;
+use settings::Settings;
+use smol::Timer;
+
+use std::{
+    ops::RangeInclusive,
+    path::{Path, PathBuf},
+    sync::Arc,
+    time::Duration,
 };
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -62,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);
 
@@ -82,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<ContextMenu>,
+    context_menu: Option<ContextMenu>,
     blink_state: bool,
     blinking_on: bool,
     blinking_paused: bool,
@@ -265,8 +270,7 @@ impl TerminalView {
             has_new_content: true,
             has_bell: false,
             focus_handle: cx.focus_handle(),
-            // todo!()
-            // context_menu: cx.build_view(|cx| ContextMenu::new(view_id, cx)),
+            context_menu: None,
             blink_state: true,
             blinking_on: false,
             blinking_paused: false,
@@ -293,18 +297,20 @@ impl TerminalView {
         cx.emit(Event::Wakeup);
     }
 
-    pub fn deploy_context_menu(&mut self, _position: Point<Pixels>, _cx: &mut ViewContext<Self>) {
-        //todo!(context_menu)
-        // let menu_entries = vec![
-        //     ContextMenuItem::action("Clear", Clear),
-        //     ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }),
-        // ];
-
-        // self.context_menu.update(cx, |menu, cx| {
-        //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx)
-        // });
-
-        // cx.notify();
+    pub fn deploy_context_menu(
+        &mut self,
+        position: gpui::Point<Pixels>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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<Self>) {
@@ -541,28 +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,
-                                 // )
-            )
-        // todo!()
-        // .child(ChildView::new(&self.context_menu, cx))
     }
 }
 

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

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<dyn gpui::Action>),
     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<V: 'static>(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<ContextMenuItem>,
 }
@@ -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<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         v_stack()
             .flex()
@@ -55,9 +72,11 @@ impl ContextMenu {
                     .map(ContextMenuItem::to_list_item::<V>)
                     .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<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> 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());
+                    }
+                })
         }
     }
 }

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,

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<Box<dyn Action>>,
 }
 
 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<Box<dyn Action>>) -> 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)
     }
 }

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 = { path = "../context_menu" }
 client = { package = "client2", path = "../client2" }
 # clock = { path = "../clock" }
 copilot = { package = "copilot2", path = "../copilot2" }

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);