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}