1use crate::{
2 ItemHandle, MultiWorkspace, Pane, SidebarSide, ToggleWorkspaceSidebar,
3 sidebar_side_context_menu,
4};
5use gpui::{
6 AnyView, App, Context, Corner, Decorations, Entity, IntoElement, ParentElement, Render, Styled,
7 Subscription, WeakEntity, Window,
8};
9use std::any::TypeId;
10use theme::CLIENT_SIDE_DECORATION_ROUNDING;
11use ui::{Divider, Indicator, Tooltip, prelude::*};
12use util::ResultExt;
13
14pub trait StatusItemView: Render {
15 /// Event callback that is triggered when the active pane item changes.
16 fn set_active_pane_item(
17 &mut self,
18 active_pane_item: Option<&dyn crate::ItemHandle>,
19 window: &mut Window,
20 cx: &mut Context<Self>,
21 );
22}
23
24trait StatusItemViewHandle: Send {
25 fn to_any(&self) -> AnyView;
26 fn set_active_pane_item(
27 &self,
28 active_pane_item: Option<&dyn ItemHandle>,
29 window: &mut Window,
30 cx: &mut App,
31 );
32 fn item_type(&self) -> TypeId;
33}
34
35#[derive(Default)]
36struct SidebarStatus {
37 open: bool,
38 side: SidebarSide,
39 has_notifications: bool,
40 show_toggle: bool,
41}
42
43impl SidebarStatus {
44 fn query(multi_workspace: &Option<WeakEntity<MultiWorkspace>>, cx: &App) -> Self {
45 multi_workspace
46 .as_ref()
47 .and_then(|mw| mw.upgrade())
48 .map(|mw| {
49 let mw = mw.read(cx);
50 let enabled = mw.multi_workspace_enabled(cx);
51 Self {
52 open: mw.sidebar_open() && enabled,
53 side: mw.sidebar_side(cx),
54 has_notifications: mw.sidebar_has_notifications(cx),
55 show_toggle: enabled,
56 }
57 })
58 .unwrap_or_default()
59 }
60}
61
62pub struct StatusBar {
63 left_items: Vec<Box<dyn StatusItemViewHandle>>,
64 right_items: Vec<Box<dyn StatusItemViewHandle>>,
65 active_pane: Entity<Pane>,
66 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
67 _observe_active_pane: Subscription,
68}
69
70impl Render for StatusBar {
71 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
72 let sidebar = SidebarStatus::query(&self.multi_workspace, cx);
73
74 h_flex()
75 .w_full()
76 .justify_between()
77 .gap(DynamicSpacing::Base08.rems(cx))
78 .p(DynamicSpacing::Base04.rems(cx))
79 .bg(cx.theme().colors().status_bar_background)
80 .map(|el| match window.window_decorations() {
81 Decorations::Server => el,
82 Decorations::Client { tiling, .. } => el
83 .when(
84 !(tiling.bottom || tiling.right)
85 && !(sidebar.open && sidebar.side == SidebarSide::Right),
86 |el| el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING),
87 )
88 .when(
89 !(tiling.bottom || tiling.left)
90 && !(sidebar.open && sidebar.side == SidebarSide::Left),
91 |el| el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING),
92 )
93 // This border is to avoid a transparent gap in the rounded corners
94 .mb(px(-1.))
95 .border_b(px(1.0))
96 .border_color(cx.theme().colors().status_bar_background),
97 })
98 .child(self.render_left_tools(&sidebar, cx))
99 .child(self.render_right_tools(&sidebar, cx))
100 }
101}
102
103impl StatusBar {
104 fn render_left_tools(
105 &self,
106 sidebar: &SidebarStatus,
107 cx: &mut Context<Self>,
108 ) -> impl IntoElement {
109 h_flex()
110 .gap_1()
111 .min_w_0()
112 .overflow_x_hidden()
113 .when(
114 sidebar.show_toggle && !sidebar.open && sidebar.side == SidebarSide::Left,
115 |this| this.child(self.render_sidebar_toggle(sidebar, cx)),
116 )
117 .children(self.left_items.iter().map(|item| item.to_any()))
118 }
119
120 fn render_right_tools(
121 &self,
122 sidebar: &SidebarStatus,
123 cx: &mut Context<Self>,
124 ) -> impl IntoElement {
125 h_flex()
126 .flex_shrink_0()
127 .gap_1()
128 .overflow_x_hidden()
129 .children(self.right_items.iter().rev().map(|item| item.to_any()))
130 .when(
131 sidebar.show_toggle && !sidebar.open && sidebar.side == SidebarSide::Right,
132 |this| this.child(self.render_sidebar_toggle(sidebar, cx)),
133 )
134 }
135
136 fn render_sidebar_toggle(
137 &self,
138 sidebar: &SidebarStatus,
139 cx: &mut Context<Self>,
140 ) -> impl IntoElement {
141 let on_right = sidebar.side == SidebarSide::Right;
142 let has_notifications = sidebar.has_notifications;
143 let indicator_border = cx.theme().colors().status_bar_background;
144
145 let toggle = sidebar_side_context_menu("sidebar-status-toggle-menu", cx)
146 .anchor(if on_right {
147 Corner::BottomRight
148 } else {
149 Corner::BottomLeft
150 })
151 .attach(if on_right {
152 Corner::TopRight
153 } else {
154 Corner::TopLeft
155 })
156 .trigger(move |_is_active, _window, _cx| {
157 IconButton::new(
158 "toggle-workspace-sidebar",
159 if on_right {
160 IconName::ThreadsSidebarRightClosed
161 } else {
162 IconName::ThreadsSidebarLeftClosed
163 },
164 )
165 .icon_size(IconSize::Small)
166 .when(has_notifications, |this| {
167 this.indicator(Indicator::dot().color(Color::Accent))
168 .indicator_border_color(Some(indicator_border))
169 })
170 .tooltip(move |_, cx| {
171 Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
172 })
173 .on_click(move |_, window, cx| {
174 if let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten() {
175 multi_workspace.update(cx, |multi_workspace, cx| {
176 multi_workspace.toggle_sidebar(window, cx);
177 });
178 }
179 })
180 });
181
182 h_flex()
183 .gap_0p5()
184 .when(on_right, |this| {
185 this.child(Divider::vertical().color(ui::DividerColor::Border))
186 })
187 .child(toggle)
188 .when(!on_right, |this| {
189 this.child(Divider::vertical().color(ui::DividerColor::Border))
190 })
191 }
192}
193
194impl StatusBar {
195 pub fn new(
196 active_pane: &Entity<Pane>,
197 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
198 window: &mut Window,
199 cx: &mut Context<Self>,
200 ) -> Self {
201 let mut this = Self {
202 left_items: Default::default(),
203 right_items: Default::default(),
204 active_pane: active_pane.clone(),
205 multi_workspace,
206 _observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| {
207 this.update_active_pane_item(window, cx)
208 }),
209 };
210 this.update_active_pane_item(window, cx);
211 this
212 }
213
214 pub fn set_multi_workspace(
215 &mut self,
216 multi_workspace: WeakEntity<MultiWorkspace>,
217 cx: &mut Context<Self>,
218 ) {
219 self.multi_workspace = Some(multi_workspace);
220 cx.notify();
221 }
222
223 pub fn add_left_item<T>(&mut self, item: Entity<T>, window: &mut Window, cx: &mut Context<Self>)
224 where
225 T: 'static + StatusItemView,
226 {
227 let active_pane_item = self.active_pane.read(cx).active_item();
228 item.set_active_pane_item(active_pane_item.as_deref(), window, cx);
229
230 self.left_items.push(Box::new(item));
231 cx.notify();
232 }
233
234 pub fn item_of_type<T: StatusItemView>(&self) -> Option<Entity<T>> {
235 self.left_items
236 .iter()
237 .chain(self.right_items.iter())
238 .find_map(|item| item.to_any().downcast().log_err())
239 }
240
241 pub fn position_of_item<T>(&self) -> Option<usize>
242 where
243 T: StatusItemView,
244 {
245 for (index, item) in self.left_items.iter().enumerate() {
246 if item.item_type() == TypeId::of::<T>() {
247 return Some(index);
248 }
249 }
250 for (index, item) in self.right_items.iter().enumerate() {
251 if item.item_type() == TypeId::of::<T>() {
252 return Some(index + self.left_items.len());
253 }
254 }
255 None
256 }
257
258 pub fn insert_item_after<T>(
259 &mut self,
260 position: usize,
261 item: Entity<T>,
262 window: &mut Window,
263 cx: &mut Context<Self>,
264 ) where
265 T: 'static + StatusItemView,
266 {
267 let active_pane_item = self.active_pane.read(cx).active_item();
268 item.set_active_pane_item(active_pane_item.as_deref(), window, cx);
269
270 if position < self.left_items.len() {
271 self.left_items.insert(position + 1, Box::new(item))
272 } else {
273 self.right_items
274 .insert(position + 1 - self.left_items.len(), Box::new(item))
275 }
276 cx.notify()
277 }
278
279 pub fn remove_item_at(&mut self, position: usize, cx: &mut Context<Self>) {
280 if position < self.left_items.len() {
281 self.left_items.remove(position);
282 } else {
283 self.right_items.remove(position - self.left_items.len());
284 }
285 cx.notify();
286 }
287
288 pub fn add_right_item<T>(
289 &mut self,
290 item: Entity<T>,
291 window: &mut Window,
292 cx: &mut Context<Self>,
293 ) where
294 T: 'static + StatusItemView,
295 {
296 let active_pane_item = self.active_pane.read(cx).active_item();
297 item.set_active_pane_item(active_pane_item.as_deref(), window, cx);
298
299 self.right_items.push(Box::new(item));
300 cx.notify();
301 }
302
303 pub fn set_active_pane(
304 &mut self,
305 active_pane: &Entity<Pane>,
306 window: &mut Window,
307 cx: &mut Context<Self>,
308 ) {
309 self.active_pane = active_pane.clone();
310 self._observe_active_pane = cx.observe_in(active_pane, window, |this, _, window, cx| {
311 this.update_active_pane_item(window, cx)
312 });
313 self.update_active_pane_item(window, cx);
314 }
315
316 fn update_active_pane_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
317 let active_pane_item = self.active_pane.read(cx).active_item();
318 for item in self.left_items.iter().chain(&self.right_items) {
319 item.set_active_pane_item(active_pane_item.as_deref(), window, cx);
320 }
321 }
322}
323
324impl<T: StatusItemView> StatusItemViewHandle for Entity<T> {
325 fn to_any(&self) -> AnyView {
326 self.clone().into()
327 }
328
329 fn set_active_pane_item(
330 &self,
331 active_pane_item: Option<&dyn ItemHandle>,
332 window: &mut Window,
333 cx: &mut App,
334 ) {
335 self.update(cx, |this, cx| {
336 this.set_active_pane_item(active_pane_item, window, cx)
337 });
338 }
339
340 fn item_type(&self) -> TypeId {
341 TypeId::of::<T>()
342 }
343}
344
345impl From<&dyn StatusItemViewHandle> for AnyView {
346 fn from(val: &dyn StatusItemViewHandle) -> Self {
347 val.to_any()
348 }
349}