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