1use context_menu::{ContextMenu, ContextMenuItem};
2use gpui::{
3 elements::*,
4 impl_internal_actions,
5 platform::{CursorStyle, MouseButton},
6 AnyElement, AppContext, Element, Entity, View, ViewContext, ViewHandle, WeakModelHandle,
7 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 ViewContext<Self>) -> AnyElement<Self> {
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().into_any(),
50 };
51
52 let focused_view = cx.focused_view_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 .into_any_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 }))
90 .constrained()
91 .with_height(style.icon_size)
92 .contained()
93 .with_style(style.container)
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 )
114 .with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
115 .into_any_named("terminal button")
116 }
117}
118
119impl TerminalButton {
120 pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
121 cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
122 Self {
123 workspace: workspace.downgrade(),
124 popup_menu: cx.add_view(|cx| {
125 let mut menu = ContextMenu::new(cx);
126 menu.set_position_mode(OverlayPositionMode::Local);
127 menu
128 }),
129 }
130 }
131
132 pub fn deploy_terminal_menu(
133 &mut self,
134 _action: &DeployTerminalMenu,
135 cx: &mut ViewContext<Self>,
136 ) {
137 let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
138
139 if let Some(workspace) = self.workspace.upgrade(cx) {
140 let project = workspace.read(cx).project().read(cx);
141 let local_terminal_handles = project.local_terminal_handles();
142
143 if !local_terminal_handles.is_empty() {
144 menu_options.push(ContextMenuItem::Separator)
145 }
146
147 for local_terminal_handle in local_terminal_handles {
148 if let Some(terminal) = local_terminal_handle.upgrade(cx) {
149 menu_options.push(ContextMenuItem::item(
150 terminal.read(cx).title(),
151 FocusTerminal {
152 terminal_handle: local_terminal_handle.clone(),
153 },
154 ))
155 }
156 }
157 }
158
159 self.popup_menu.update(cx, |menu, cx| {
160 menu.show(
161 Default::default(),
162 AnchorCorner::BottomRight,
163 menu_options,
164 cx,
165 );
166 });
167 }
168
169 pub fn focus_terminal(&mut self, action: &FocusTerminal, cx: &mut ViewContext<Self>) {
170 if let Some(workspace) = self.workspace.upgrade(cx) {
171 workspace.update(cx, |workspace, cx| {
172 let terminal = workspace
173 .items_of_type::<TerminalView>(cx)
174 .find(|terminal| {
175 terminal.read(cx).model().downgrade() == action.terminal_handle
176 });
177 if let Some(terminal) = terminal {
178 workspace.activate_item(&terminal, cx);
179 }
180 });
181 }
182 }
183}
184
185impl StatusItemView for TerminalButton {
186 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
187 cx.notify();
188 }
189}