status_bar.rs

  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}