diff --git a/Cargo.lock b/Cargo.lock index 384ded5b3ee428be6efdc446a51cd4a53ae7dc6c..8bd63c644cc90fb3293fa4645d3a3bb42b6c42ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8349,6 +8349,7 @@ dependencies = [ "serde_json", "settings", "smallvec", + "terminal", "theme", "util", "uuid 1.2.2", diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index 222c542eed4dba57e31a1b61d1099b4a72f02e24..52d1ab9d92d45817fd83c016efc3939489b71842 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -25,10 +25,10 @@ impl View for DeployFeedbackButton { fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { let active = self.active; + let theme = cx.global::().theme.clone(); Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, cx| { - let theme = &cx.global::().theme; + MouseEventHandler::::new(0, cx, |state, _| { let style = &theme .workspace .status_bar @@ -54,6 +54,13 @@ impl View for DeployFeedbackButton { cx.dispatch_action(GiveFeedback) } }) + .with_tooltip::( + 0, + "Give Feedback".into(), + Some(Box::new(GiveFeedback)), + theme.tooltip.clone(), + cx, + ) .boxed(), ) .boxed() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 7fc1de83fdc69257e8a0384b41db839dcfc21d13..51208f3930a1fd722f5186015fd72a2f94ee483c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2757,6 +2757,12 @@ impl AppContext { Some(self.views.get(&(window_id, view_id))?.ui_name()) } + pub fn view_type_id(&self, window_id: usize, view_id: usize) -> Option { + self.views + .get(&(window_id, view_id)) + .map(|view| view.as_any().type_id()) + } + pub fn background(&self) -> &Arc { &self.background } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b01546ff5c7f75ebfe880ff2095d50bdb9b5ca68..f7b4105dd2ac8f52a52e2a524ca7afced24ace0d 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -58,6 +58,10 @@ impl Project { terminal } } + + pub fn local_terminal_handles(&self) -> &Vec> { + &self.terminals.local_handles + } } // TODO: Add a few tests for adding and removing terminal tabs diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index feed3d510f78c30a1b32fc57c3a6162b5f9c4816..0eefc8939a35682acae1c21487a95f42c9a0d13d 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -32,6 +32,7 @@ use mappings::mouse::{ use procinfo::LocalProcessInfo; use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; +use util::truncate_and_trailoff; use std::{ cmp::min, @@ -1169,6 +1170,38 @@ impl Terminal { all_search_matches(&term, &searcher).collect() }) } + + pub fn title(&self) -> String { + self.foreground_process_info + .as_ref() + .map(|fpi| { + format!( + "{} — {}", + truncate_and_trailoff( + &fpi.cwd + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_default(), + 25 + ), + truncate_and_trailoff( + &{ + format!( + "{}{}", + fpi.name, + if fpi.argv.len() >= 1 { + format!(" {}", (&fpi.argv[1..]).join(" ")) + } else { + "".to_string() + } + ) + }, + 25 + ) + ) + }) + .unwrap_or_else(|| "Terminal".to_string()) + } } impl Drop for Terminal { diff --git a/crates/terminal_view/src/terminal_button.rs b/crates/terminal_view/src/terminal_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..c4f4c0571f10e3d6a772c7bd7518d8e787b718ab --- /dev/null +++ b/crates/terminal_view/src/terminal_button.rs @@ -0,0 +1,176 @@ +use context_menu::{ContextMenu, ContextMenuItem}; +use gpui::{ + elements::*, geometry::vector::Vector2F, impl_internal_actions, CursorStyle, Element, + ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, + ViewHandle, WeakModelHandle, WeakViewHandle, +}; +use settings::Settings; +use std::any::TypeId; +use terminal::Terminal; +use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace}; + +use crate::TerminalView; + +#[derive(Clone, PartialEq)] +pub struct FocusTerminal { + terminal_handle: WeakModelHandle, +} + +#[derive(Clone, PartialEq)] +pub struct DeployTerminalMenu { + position: Vector2F, +} + +impl_internal_actions!(terminal, [FocusTerminal, DeployTerminalMenu]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(TerminalButton::deploy_terminal_menu); + cx.add_action(TerminalButton::focus_terminal); +} + +pub struct TerminalButton { + workspace: WeakViewHandle, + popup_menu: ViewHandle, +} + +impl Entity for TerminalButton { + type Event = (); +} + +impl View for TerminalButton { + fn ui_name() -> &'static str { + "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 focused_view = cx.focused_view_id(cx.window_id()); + let active = focused_view + .map(|view_id| { + cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::()) + }) + .unwrap_or(false); + + let has_terminals = !project.local_terminal_handles().is_empty(); + let theme = cx.global::().theme.clone(); + + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, { + let theme = theme.clone(); + move |state, _| { + let style = theme + .workspace + .status_bar + .sidebar_buttons + .item + .style_for(state, active); + + 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_click(MouseButton::Left, move |e, cx| { + if has_terminals { + cx.dispatch_action(DeployTerminalMenu { + position: e.region.upper_right(), + }); + } else { + if !active { + cx.dispatch_action(FocusDock); + } + }; + }) + .with_tooltip::( + 0, + "Show Terminal".into(), + Some(Box::new(FocusDock)), + theme.tooltip.clone(), + cx, + ) + .boxed(), + ) + .with_child(ChildView::new(&self.popup_menu, cx).boxed()) + .boxed() + } +} + +impl TerminalButton { + pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> 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::Window); + menu + }), + } + } + + pub fn deploy_terminal_menu( + &mut self, + action: &DeployTerminalMenu, + cx: &mut ViewContext, + ) { + let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)]; + + if let Some(workspace) = self.workspace.upgrade(cx) { + let project = workspace.read(cx).project().read(cx); + let local_terminal_handles = project.local_terminal_handles(); + + if !local_terminal_handles.is_empty() { + menu_options.push(ContextMenuItem::Separator) + } + + for local_terminal_handle in local_terminal_handles { + if let Some(terminal) = local_terminal_handle.upgrade(cx) { + menu_options.push(ContextMenuItem::item( + terminal.read(cx).title(), + FocusTerminal { + terminal_handle: local_terminal_handle.clone(), + }, + )) + } + } + } + + self.popup_menu.update(cx, |menu, cx| { + menu.show(action.position, AnchorCorner::BottomRight, menu_options, cx); + }); + } + + pub fn focus_terminal(&mut self, action: &FocusTerminal, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + let terminal = workspace + .items_of_type::(cx) + .find(|terminal| { + terminal.read(cx).model().downgrade() == action.terminal_handle + }); + if let Some(terminal) = terminal { + workspace.activate_item(&terminal, cx); + } + }); + } + } +} + +impl StatusItemView for TerminalButton { + fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + cx.notify(); + } +} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 330be6dcf484d763cfb3453e4b6d9318fd29bd57..f1a627e29d2dbade2e4d8398727c166028709352 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,4 +1,5 @@ mod persistence; +pub mod terminal_button; pub mod terminal_element; use std::{ @@ -30,7 +31,7 @@ use terminal::{ }, Event, Terminal, }; -use util::{truncate_and_trailoff, ResultExt}; +use util::ResultExt; use workspace::{ item::{Item, ItemEvent}, notifications::NotifyResultExt, @@ -177,8 +178,8 @@ impl TerminalView { } } - pub fn handle(&self) -> ModelHandle { - self.terminal.clone() + pub fn model(&self) -> &ModelHandle { + &self.terminal } pub fn has_new_content(&self) -> bool { @@ -547,38 +548,7 @@ impl Item for TerminalView { tab_theme: &theme::Tab, cx: &gpui::AppContext, ) -> ElementBox { - let title = self - .terminal() - .read(cx) - .foreground_process_info - .as_ref() - .map(|fpi| { - format!( - "{} — {}", - truncate_and_trailoff( - &fpi.cwd - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_default(), - 25 - ), - truncate_and_trailoff( - &{ - format!( - "{}{}", - fpi.name, - if fpi.argv.len() >= 1 { - format!(" {}", (&fpi.argv[1..]).join(" ")) - } else { - "".to_string() - } - ) - }, - 25 - ) - ) - }) - .unwrap_or_else(|| "Terminal".to_string()); + let title = self.terminal().read(cx).title(); Flex::row() .with_child( diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 2ba7a6cc40b31e6fbe72862f2fbc85ef8d60a2ee..a143e1a2400af479a8f08fae08c8d82d86135484 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -32,6 +32,7 @@ language = { path = "../language" } menu = { path = "../menu" } project = { path = "../project" } settings = { path = "../settings" } +terminal = { path = "../terminal" } theme = { path = "../theme" } util = { path = "../util" } async-recursion = "1.0.0" diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 587f180a4aaa73722869f235faa713e98f593275..b0d7b7870895ef9718c81d7cda8985c54bc7554a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -168,8 +168,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(); }); diff --git a/crates/workspace/src/terminal_button.rs b/crates/workspace/src/terminal_button.rs deleted file mode 100644 index b4ad662c94cba2d69c3c39230b45beb1726fc825..0000000000000000000000000000000000000000 --- a/crates/workspace/src/terminal_button.rs +++ /dev/null @@ -1,92 +0,0 @@ -use gpui::{ - elements::{Empty, MouseEventHandler, Svg}, - CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext, - ViewHandle, WeakViewHandle, -}; -use settings::Settings; - -use crate::{dock::FocusDock, item::ItemHandle, StatusItemView, Workspace}; - -pub struct TerminalButton { - workspace: WeakViewHandle, -} - -impl TerminalButton { - pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { - // When terminal moves, redraw so that the icon and toggle status matches. - cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach(); - - Self { - workspace: workspace.downgrade(), - } - } -} - -impl Entity for TerminalButton { - type Event = (); -} - -impl View for TerminalButton { - fn ui_name() -> &'static str { - "TerminalButton" - } - - fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { - let workspace = self.workspace.upgrade(cx); - - if workspace.is_none() { - return Empty::new().boxed(); - } - - let focused_view = cx.focused_view_id(cx.window_id()); - - // FIXME: Don't hardcode "Terminal" in here - let active = focused_view - .map(|view| cx.view_ui_name(cx.window_id(), view) == Some("Terminal")) - .unwrap_or(false); - - // let workspace = workspace.unwrap(); - let theme = cx.global::().theme.clone(); - - MouseEventHandler::::new(0, cx, { - let theme = theme.clone(); - move |state, _| { - let style = theme - .workspace - .status_bar - .sidebar_buttons - .item - .style_for(state, active); - - 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_click(MouseButton::Left, move |_, cx| { - if !active { - cx.dispatch_action(FocusDock); - } - }) - .with_tooltip::( - 0, - "Show Terminal".into(), - Some(Box::new(FocusDock)), - theme.tooltip.clone(), - cx, - ) - .boxed() - } -} - -impl StatusItemView for TerminalButton { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext) { - cx.notify(); - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 609afded3325b97b8356c85b9b6955622a27e289..43ae12b732938e0c02fb055a82632bfa7b163a5a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,7 +12,6 @@ pub mod searchable; pub mod shared_screen; pub mod sidebar; mod status_bar; -pub mod terminal_button; mod toolbar; pub use smallvec; @@ -58,7 +57,6 @@ use std::{ sync::Arc, time::Duration, }; -use terminal_button::TerminalButton; use crate::{ notifications::simple_message_notification::{MessageNotification, OsOpen}, @@ -666,7 +664,6 @@ impl Workspace { let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left)); let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right)); let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx)); - let toggle_terminal = cx.add_view(|cx| TerminalButton::new(handle.clone(), cx)); let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx)); let right_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx)); @@ -675,7 +672,6 @@ 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); status_bar }); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 042d486852a63a32609aec7fa5e8bac7a15a8b40..00e67f63b3635e032510aee6ab941a3ccdbedc50 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -31,6 +31,7 @@ use serde::Deserialize; use serde_json::to_string_pretty; use settings::{keymap_file_json_schema, settings_file_json_schema, Settings}; use std::{borrow::Cow, env, path::Path, str, sync::Arc}; +use terminal_view::terminal_button::{self, TerminalButton}; use util::{channel::ReleaseChannel, paths, ResultExt, StaffMode}; use uuid::Uuid; pub use workspace; @@ -72,6 +73,7 @@ actions!( const MIN_FONT_SIZE: f32 = 6.0; pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { + terminal_button::init(cx); cx.add_action(about); cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| { cx.platform().hide(); @@ -336,6 +338,7 @@ pub fn initialize_workspace( ) }); + let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx)); let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); let activity_indicator = @@ -347,6 +350,9 @@ pub fn initialize_workspace( workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); + if **cx.default_global::() { + status_bar.add_right_item(toggle_terminal, cx); + } status_bar.add_right_item(feedback_button, cx); status_bar.add_right_item(active_buffer_language, cx); status_bar.add_right_item(cursor_position, cx);