terminal_button.rs

  1use context_menu::{ContextMenu, ContextMenuItem};
  2use gpui::{
  3    elements::*, geometry::vector::Vector2F, impl_internal_actions, CursorStyle, Element,
  4    ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
  5    ViewHandle, WeakModelHandle, WeakViewHandle,
  6};
  7use settings::Settings;
  8use std::any::TypeId;
  9use terminal::Terminal;
 10use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace};
 11
 12use crate::TerminalView;
 13
 14#[derive(Clone, PartialEq)]
 15pub struct FocusTerminal {
 16    terminal_handle: WeakModelHandle<Terminal>,
 17}
 18
 19#[derive(Clone, PartialEq)]
 20pub struct DeployTerminalMenu {
 21    position: Vector2F,
 22}
 23
 24impl_internal_actions!(terminal, [FocusTerminal, DeployTerminalMenu]);
 25
 26pub fn init(cx: &mut MutableAppContext) {
 27    cx.add_action(TerminalButton::deploy_terminal_menu);
 28    cx.add_action(TerminalButton::focus_terminal);
 29}
 30
 31pub struct TerminalButton {
 32    workspace: WeakViewHandle<Workspace>,
 33    popup_menu: ViewHandle<ContextMenu>,
 34}
 35
 36impl Entity for TerminalButton {
 37    type Event = ();
 38}
 39
 40impl View for TerminalButton {
 41    fn ui_name() -> &'static str {
 42        "TerminalButton"
 43    }
 44
 45    fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
 46        let workspace = self.workspace.upgrade(cx);
 47        let project = match workspace {
 48            Some(workspace) => workspace.read(cx).project().read(cx),
 49            None => return Empty::new().boxed(),
 50        };
 51
 52        let focused_view = cx.focused_view_id(cx.window_id());
 53        let active = focused_view
 54            .map(|view_id| {
 55                cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::<TerminalView>())
 56            })
 57            .unwrap_or(false);
 58
 59        let has_terminals = !project.local_terminal_handles().is_empty();
 60        let theme = cx.global::<Settings>().theme.clone();
 61
 62        Stack::new()
 63            .with_child(
 64                MouseEventHandler::<Self>::new(0, cx, {
 65                    let theme = theme.clone();
 66                    move |state, _| {
 67                        let style = theme
 68                            .workspace
 69                            .status_bar
 70                            .sidebar_buttons
 71                            .item
 72                            .style_for(state, active);
 73
 74                        Svg::new("icons/terminal_12.svg")
 75                            .with_color(style.icon_color)
 76                            .constrained()
 77                            .with_width(style.icon_size)
 78                            .with_height(style.icon_size)
 79                            .contained()
 80                            .with_style(style.container)
 81                            .boxed()
 82                    }
 83                })
 84                .with_cursor_style(CursorStyle::PointingHand)
 85                .on_click(MouseButton::Left, move |e, cx| {
 86                    if has_terminals {
 87                        cx.dispatch_action(DeployTerminalMenu {
 88                            position: e.region.upper_right(),
 89                        });
 90                    } else {
 91                        if !active {
 92                            cx.dispatch_action(FocusDock);
 93                        }
 94                    };
 95                })
 96                .with_tooltip::<Self, _>(
 97                    0,
 98                    "Show Terminal".into(),
 99                    Some(Box::new(FocusDock)),
100                    theme.tooltip.clone(),
101                    cx,
102                )
103                .boxed(),
104            )
105            .with_child(ChildView::new(&self.popup_menu, cx).boxed())
106            .boxed()
107    }
108}
109
110impl TerminalButton {
111    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
112        // When terminal moves, redraw so that the icon and toggle status matches.
113        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
114        Self {
115            workspace: workspace.downgrade(),
116            popup_menu: cx.add_view(|cx| {
117                let mut menu = ContextMenu::new(cx);
118                menu.set_position_mode(OverlayPositionMode::Window);
119                menu
120            }),
121        }
122    }
123
124    pub fn deploy_terminal_menu(
125        &mut self,
126        action: &DeployTerminalMenu,
127        cx: &mut ViewContext<Self>,
128    ) {
129        let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
130
131        if let Some(workspace) = self.workspace.upgrade(cx) {
132            let project = workspace.read(cx).project().read(cx);
133            let local_terminal_handles = project.local_terminal_handles();
134
135            if !local_terminal_handles.is_empty() {
136                menu_options.push(ContextMenuItem::Separator)
137            }
138
139            for local_terminal_handle in local_terminal_handles {
140                if let Some(terminal) = local_terminal_handle.upgrade(cx) {
141                    menu_options.push(ContextMenuItem::item(
142                        terminal.read(cx).title(),
143                        FocusTerminal {
144                            terminal_handle: local_terminal_handle.clone(),
145                        },
146                    ))
147                }
148            }
149        }
150
151        self.popup_menu.update(cx, |menu, cx| {
152            menu.show(action.position, AnchorCorner::BottomRight, menu_options, cx);
153        });
154    }
155
156    pub fn focus_terminal(&mut self, action: &FocusTerminal, cx: &mut ViewContext<Self>) {
157        if let Some(workspace) = self.workspace.upgrade(cx) {
158            workspace.update(cx, |workspace, cx| {
159                let terminal = workspace
160                    .items_of_type::<TerminalView>(cx)
161                    .find(|terminal| {
162                        terminal.read(cx).model().downgrade() == action.terminal_handle
163                    });
164                if let Some(terminal) = terminal {
165                    workspace.activate_item(&terminal, cx);
166                }
167            });
168        }
169    }
170}
171
172impl StatusItemView for TerminalButton {
173    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
174        cx.notify();
175    }
176}