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