status_bar.rs

  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}