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