1use context_menu::{ContextMenu, ContextMenuItem};
2use gpui::{
3 elements::*,
4 impl_internal_actions,
5 platform::{CursorStyle, MouseButton},
6 AppContext, Element, ElementBox, Entity, RenderContext, View, ViewContext, ViewHandle,
7 WeakModelHandle, WeakViewHandle,
8};
9use settings::Settings;
10use std::any::TypeId;
11use terminal::Terminal;
12use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace};
13
14use crate::TerminalView;
15
16#[derive(Clone, PartialEq)]
17pub struct DeployTerminalMenu;
18
19#[derive(Clone, PartialEq)]
20pub struct FocusTerminal {
21 terminal_handle: WeakModelHandle<Terminal>,
22}
23
24impl_internal_actions!(terminal, [FocusTerminal, DeployTerminalMenu]);
25
26pub fn init(cx: &mut AppContext) {
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 terminal_count = project.local_terminal_handles().len() as i32;
61 let theme = cx.global::<Settings>().theme.clone();
62
63 Stack::new()
64 .with_child(
65 MouseEventHandler::<Self>::new(0, cx, {
66 let theme = theme.clone();
67 move |state, _cx| {
68 let style = theme
69 .workspace
70 .status_bar
71 .sidebar_buttons
72 .item
73 .style_for(state, active);
74
75 Flex::row()
76 .with_child(
77 Svg::new("icons/terminal_12.svg")
78 .with_color(style.icon_color)
79 .constrained()
80 .with_width(style.icon_size)
81 .aligned()
82 .named("terminals-icon"),
83 )
84 .with_children(has_terminals.then(|| {
85 Label::new(terminal_count.to_string(), style.label.text.clone())
86 .contained()
87 .with_style(style.label.container)
88 .aligned()
89 .boxed()
90 }))
91 .constrained()
92 .with_height(style.icon_size)
93 .contained()
94 .with_style(style.container)
95 .boxed()
96 }
97 })
98 .with_cursor_style(CursorStyle::PointingHand)
99 .on_click(MouseButton::Left, move |_, cx| {
100 if has_terminals {
101 cx.dispatch_action(DeployTerminalMenu);
102 } else {
103 if !active {
104 cx.dispatch_action(FocusDock);
105 }
106 };
107 })
108 .with_tooltip::<Self, _>(
109 0,
110 "Show Terminal".into(),
111 Some(Box::new(FocusDock)),
112 theme.tooltip.clone(),
113 cx,
114 )
115 .boxed(),
116 )
117 .with_child(
118 ChildView::new(&self.popup_menu, cx)
119 .aligned()
120 .top()
121 .right()
122 .boxed(),
123 )
124 .boxed()
125 }
126}
127
128impl TerminalButton {
129 pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
130 cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
131 Self {
132 workspace: workspace.downgrade(),
133 popup_menu: cx.add_view(|cx| {
134 let mut menu = ContextMenu::new(cx);
135 menu.set_position_mode(OverlayPositionMode::Local);
136 menu
137 }),
138 }
139 }
140
141 pub fn deploy_terminal_menu(
142 &mut self,
143 _action: &DeployTerminalMenu,
144 cx: &mut ViewContext<Self>,
145 ) {
146 let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
147
148 if let Some(workspace) = self.workspace.upgrade(cx) {
149 let project = workspace.read(cx).project().read(cx);
150 let local_terminal_handles = project.local_terminal_handles();
151
152 if !local_terminal_handles.is_empty() {
153 menu_options.push(ContextMenuItem::Separator)
154 }
155
156 for local_terminal_handle in local_terminal_handles {
157 if let Some(terminal) = local_terminal_handle.upgrade(cx) {
158 menu_options.push(ContextMenuItem::item(
159 terminal.read(cx).title(),
160 FocusTerminal {
161 terminal_handle: local_terminal_handle.clone(),
162 },
163 ))
164 }
165 }
166 }
167
168 self.popup_menu.update(cx, |menu, cx| {
169 menu.show(
170 Default::default(),
171 AnchorCorner::BottomRight,
172 menu_options,
173 cx,
174 );
175 });
176 }
177
178 pub fn focus_terminal(&mut self, action: &FocusTerminal, cx: &mut ViewContext<Self>) {
179 if let Some(workspace) = self.workspace.upgrade(cx) {
180 workspace.update(cx, |workspace, cx| {
181 let terminal = workspace
182 .items_of_type::<TerminalView>(cx)
183 .find(|terminal| {
184 terminal.read(cx).model().downgrade() == action.terminal_handle
185 });
186 if let Some(terminal) = terminal {
187 workspace.activate_item(&terminal, cx);
188 }
189 });
190 }
191 }
192}
193
194impl StatusItemView for TerminalButton {
195 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
196 cx.notify();
197 }
198}