Show a pop up menu for terminals

Joseph Lyons , Joseph T. Lyons , and Antonio Scandurra created

Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com>
Co-Authored-By: Antonio Scandurra <me@as-cii.com>

Change summary

crates/project/src/terminals.rs         |   4 
crates/workspace/src/pane.rs            |   2 
crates/workspace/src/terminal_button.rs | 157 +++++++++++++++++---------
crates/workspace/src/workspace.rs       |   8 +
4 files changed, 110 insertions(+), 61 deletions(-)

Detailed changes

crates/project/src/terminals.rs 🔗

@@ -58,6 +58,10 @@ impl Project {
             terminal
         }
     }
+
+    pub fn local_terminal_handles(&self) -> &Vec<WeakModelHandle<terminal::Terminal>> {
+        &self.terminals.local_handles
+    }
 }
 
 // TODO: Add a few tests for adding and removing terminal tabs

crates/workspace/src/pane.rs 🔗

@@ -166,8 +166,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
     cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
     cx.add_action(Pane::deploy_split_menu);
-    cx.add_action(Pane::deploy_new_menu);
     cx.add_action(Pane::deploy_dock_menu);
+    cx.add_action(Pane::deploy_new_menu);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         Pane::reopen_closed_item(workspace, cx).detach();
     });

crates/workspace/src/terminal_button.rs 🔗

@@ -1,26 +1,26 @@
+use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
-    elements::{Empty, MouseEventHandler, Svg},
-    CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    actions, elements::*, geometry::vector::vec2f, CursorStyle, Element, ElementBox, Entity,
+    MouseButton, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use settings::Settings;
 
-use crate::{dock::FocusDock, item::ItemHandle, StatusItemView, Workspace};
+use crate::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace};
 
-pub struct TerminalButton {
-    workspace: WeakViewHandle<Workspace>,
-}
+// #[derive(Clone, PartialEq)]
+// pub struct DeployTerminalMenu {
+//     position: Vector2F,
+// }
 
-// TODO: Rename this to `DeployTerminalButton`
-impl TerminalButton {
-    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
-        // When terminal moves, redraw so that the icon and toggle status matches.
-        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
+actions!(terminal, [DeployTerminalMenu]);
 
-        Self {
-            workspace: workspace.downgrade(),
-        }
-    }
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(TerminalButton::deploy_terminal_menu);
+}
+
+pub struct TerminalButton {
+    workspace: WeakViewHandle<Workspace>,
+    popup_menu: ViewHandle<ContextMenu>,
 }
 
 impl Entity for TerminalButton {
@@ -34,52 +34,93 @@ impl View for TerminalButton {
 
     fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
         let workspace = self.workspace.upgrade(cx);
+        let project = match workspace {
+            Some(workspace) => workspace.read(cx).project().read(cx),
+            None => return Empty::new().boxed(),
+        };
+        let has_terminals = !project.local_terminal_handles().is_empty();
+        let terminal_count = project.local_terminal_handles().iter().count();
+        let theme = cx.global::<Settings>().theme.clone();
 
-        if workspace.is_none() {
-            return Empty::new().boxed();
-        }
+        Stack::new()
+            .with_child(
+                MouseEventHandler::<Self>::new(0, cx, {
+                    let theme = theme.clone();
+                    move |state, _| {
+                        let style = theme
+                            .workspace
+                            .status_bar
+                            .sidebar_buttons
+                            .item
+                            .style_for(state, true);
 
-        // let workspace = workspace.unwrap();
-        let theme = cx.global::<Settings>().theme.clone();
+                        Svg::new("icons/terminal_12.svg")
+                            .with_color(style.icon_color)
+                            .constrained()
+                            .with_width(style.icon_size)
+                            .with_height(style.icon_size)
+                            .contained()
+                            .with_style(style.container)
+                            .boxed()
+                    }
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_up(MouseButton::Left, move |_, _| {
+                    // TODO: Do we need this stuff?
+                    // let dock_pane = workspace.read(cx.app).dock_pane();
+                    // let drop_index = dock_pane.read(cx.app).items_len() + 1;
+                    // handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
+                })
+                .on_click(MouseButton::Left, move |_, cx| {
+                    if has_terminals {
+                        cx.dispatch_action(DeployTerminalMenu);
+                        println!("Yes, has_terminals {}", terminal_count);
+                    } else {
+                        cx.dispatch_action(FocusDock);
+                    };
+                })
+                .with_tooltip::<Self, _>(
+                    0,
+                    "Show Terminal".into(),
+                    Some(Box::new(FocusDock)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
+                .boxed(),
+            )
+            .with_child(ChildView::new(&self.popup_menu, cx).boxed())
+            .boxed()
+    }
+}
 
-        MouseEventHandler::<Self>::new(0, cx, {
-            let theme = theme.clone();
-            move |state, _| {
-                let style = theme
-                    .workspace
-                    .status_bar
-                    .sidebar_buttons
-                    .item
-                    .style_for(state, true);
+// TODO: Rename this to `DeployTerminalButton`
+impl TerminalButton {
+    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+        // When terminal moves, redraw so that the icon and toggle status matches.
+        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
+        Self {
+            workspace: workspace.downgrade(),
+            popup_menu: cx.add_view(|cx| {
+                let mut menu = ContextMenu::new(cx);
+                menu.set_position_mode(OverlayPositionMode::Local);
+                menu
+            }),
+        }
+    }
 
-                Svg::new("icons/terminal_12.svg")
-                    .with_color(style.icon_color)
-                    .constrained()
-                    .with_width(style.icon_size)
-                    .with_height(style.icon_size)
-                    .contained()
-                    .with_style(style.container)
-                    .boxed()
-            }
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_up(MouseButton::Left, move |_, _| {
-            // TODO: Do we need this stuff?
-            // let dock_pane = workspace.read(cx.app).dock_pane();
-            // let drop_index = dock_pane.read(cx.app).items_len() + 1;
-            // handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
-        })
-        .on_click(MouseButton::Left, |_, cx| {
-            cx.dispatch_action(FocusDock);
-        })
-        .with_tooltip::<Self, _>(
-            0,
-            "Show Terminal".into(),
-            Some(Box::new(FocusDock)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .boxed()
+    pub fn deploy_terminal_menu(
+        &mut self,
+        _action: &DeployTerminalMenu,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.popup_menu.update(cx, |menu, cx| {
+            menu.show(
+                vec2f(0., 0.),
+                AnchorCorner::TopLeft,
+                vec![ContextMenuItem::item("New Terminal", NewTerminal)],
+                cx,
+            );
+        });
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -83,7 +83,7 @@ use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use theme::{Theme, ThemeRegistry};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use util::ResultExt;
+use util::{ResultExt, StaffMode};
 
 lazy_static! {
     static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
@@ -185,6 +185,7 @@ impl_actions!(workspace, [ActivatePane]);
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     pane::init(cx);
     dock::init(cx);
+    terminal_button::init(cx);
     notifications::init(cx);
 
     cx.add_global_action(open);
@@ -595,7 +596,10 @@ impl Workspace {
             status_bar.add_left_item(left_sidebar_buttons, cx);
             status_bar.add_right_item(right_sidebar_buttons, cx);
             status_bar.add_right_item(toggle_dock, cx);
-            status_bar.add_right_item(toggle_terminal, cx);
+            // TOOD: Remove this when things are done
+            if **cx.default_global::<StaffMode>() {
+                status_bar.add_right_item(toggle_terminal, cx);
+            }
             status_bar
         });