Move sidebar toggle buttons to the status bar

Max Brunsfeld created

Change summary

assets/themes/cave-dark.json         |  67 ++------
assets/themes/cave-light.json        |  67 ++------
assets/themes/dark.json              |  67 ++------
assets/themes/light.json             |  67 ++------
assets/themes/solarized-dark.json    |  67 ++------
assets/themes/solarized-light.json   |  67 ++------
assets/themes/sulphurpool-dark.json  |  67 ++------
assets/themes/sulphurpool-light.json |  67 ++------
crates/theme/src/theme.rs            |  15 -
crates/workspace/src/sidebar.rs      | 233 +++++++++++++++++------------
crates/workspace/src/status_bar.rs   |   4 
crates/workspace/src/workspace.rs    | 118 ++++++++------
crates/zed/src/zed.rs                |  32 +--
styles/src/styleTree/workspace.ts    |  40 ++---
14 files changed, 393 insertions(+), 585 deletions(-)

Detailed changes

assets/themes/cave-dark.json 🔗

@@ -161,54 +161,10 @@
       },
       "cursor": "Arrow"
     },
-    "left_sidebar": {
-      "width": 30,
-      "background": "#26232a",
-      "border": {
-        "color": "#19171c",
-        "width": 1,
-        "right": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#8b8792",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#efecf4",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#19171c",
-        "padding": {
-          "left": 1
-        }
-      }
-    },
-    "right_sidebar": {
-      "width": 30,
-      "background": "#26232a",
-      "border": {
-        "color": "#19171c",
-        "width": 1,
-        "left": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#8b8792",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#efecf4",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#19171c",
-        "padding": {
-          "left": 1
-        }
+    "sidebar_resize_handle": {
+      "background": "#19171c",
+      "padding": {
+        "left": 1
       }
     },
     "pane_divider": {
@@ -252,6 +208,21 @@
         "family": "Zed Sans",
         "color": "#8b8792",
         "size": 14
+      },
+      "sidebar_item": {
+        "height": 32,
+        "icon_color": "#8b8792",
+        "icon_size": 18
+      },
+      "sidebar_item_hover": {
+        "height": 32,
+        "icon_color": "#8b8792",
+        "icon_size": 18
+      },
+      "sidebar_item_active": {
+        "height": 32,
+        "icon_color": "#efecf4",
+        "icon_size": 18
       }
     },
     "titlebar": {

assets/themes/cave-light.json 🔗

@@ -161,54 +161,10 @@
       },
       "cursor": "Arrow"
     },
-    "left_sidebar": {
-      "width": 30,
-      "background": "#e2dfe7",
-      "border": {
-        "color": "#efecf4",
-        "width": 1,
-        "right": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#585260",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#19171c",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#efecf4",
-        "padding": {
-          "left": 1
-        }
-      }
-    },
-    "right_sidebar": {
-      "width": 30,
-      "background": "#e2dfe7",
-      "border": {
-        "color": "#efecf4",
-        "width": 1,
-        "left": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#585260",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#19171c",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#efecf4",
-        "padding": {
-          "left": 1
-        }
+    "sidebar_resize_handle": {
+      "background": "#efecf4",
+      "padding": {
+        "left": 1
       }
     },
     "pane_divider": {
@@ -252,6 +208,21 @@
         "family": "Zed Sans",
         "color": "#585260",
         "size": 14
+      },
+      "sidebar_item": {
+        "height": 32,
+        "icon_color": "#585260",
+        "icon_size": 18
+      },
+      "sidebar_item_hover": {
+        "height": 32,
+        "icon_color": "#585260",
+        "icon_size": 18
+      },
+      "sidebar_item_active": {
+        "height": 32,
+        "icon_color": "#19171c",
+        "icon_size": 18
       }
     },
     "titlebar": {

assets/themes/dark.json 🔗

@@ -161,54 +161,10 @@
       },
       "cursor": "Arrow"
     },
-    "left_sidebar": {
-      "width": 30,
-      "background": "#1c1c1c",
-      "border": {
-        "color": "#070707",
-        "width": 1,
-        "right": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#9c9c9c",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#ffffff",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#070707",
-        "padding": {
-          "left": 1
-        }
-      }
-    },
-    "right_sidebar": {
-      "width": 30,
-      "background": "#1c1c1c",
-      "border": {
-        "color": "#070707",
-        "width": 1,
-        "left": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#9c9c9c",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#ffffff",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#070707",
-        "padding": {
-          "left": 1
-        }
+    "sidebar_resize_handle": {
+      "background": "#070707",
+      "padding": {
+        "left": 1
       }
     },
     "pane_divider": {
@@ -252,6 +208,21 @@
         "family": "Zed Sans",
         "color": "#808080",
         "size": 14
+      },
+      "sidebar_item": {
+        "height": 32,
+        "icon_color": "#9c9c9c",
+        "icon_size": 18
+      },
+      "sidebar_item_hover": {
+        "height": 32,
+        "icon_color": "#9c9c9c",
+        "icon_size": 18
+      },
+      "sidebar_item_active": {
+        "height": 32,
+        "icon_color": "#ffffff",
+        "icon_size": 18
       }
     },
     "titlebar": {

assets/themes/light.json 🔗

@@ -161,54 +161,10 @@
       },
       "cursor": "Arrow"
     },
-    "left_sidebar": {
-      "width": 30,
-      "background": "#f8f8f8",
-      "border": {
-        "color": "#d5d5d5",
-        "width": 1,
-        "right": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#717171",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#000000",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#d5d5d5",
-        "padding": {
-          "left": 1
-        }
-      }
-    },
-    "right_sidebar": {
-      "width": 30,
-      "background": "#f8f8f8",
-      "border": {
-        "color": "#d5d5d5",
-        "width": 1,
-        "left": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#717171",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#000000",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#d5d5d5",
-        "padding": {
-          "left": 1
-        }
+    "sidebar_resize_handle": {
+      "background": "#d5d5d5",
+      "padding": {
+        "left": 1
       }
     },
     "pane_divider": {
@@ -252,6 +208,21 @@
         "family": "Zed Sans",
         "color": "#636363",
         "size": 14
+      },
+      "sidebar_item": {
+        "height": 32,
+        "icon_color": "#717171",
+        "icon_size": 18
+      },
+      "sidebar_item_hover": {
+        "height": 32,
+        "icon_color": "#717171",
+        "icon_size": 18
+      },
+      "sidebar_item_active": {
+        "height": 32,
+        "icon_color": "#000000",
+        "icon_size": 18
       }
     },
     "titlebar": {

assets/themes/solarized-dark.json 🔗

@@ -161,54 +161,10 @@
       },
       "cursor": "Arrow"
     },
-    "left_sidebar": {
-      "width": 30,
-      "background": "#073642",
-      "border": {
-        "color": "#002b36",
-        "width": 1,
-        "right": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#93a1a1",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#fdf6e3",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#002b36",
-        "padding": {
-          "left": 1
-        }
-      }
-    },
-    "right_sidebar": {
-      "width": 30,
-      "background": "#073642",
-      "border": {
-        "color": "#002b36",
-        "width": 1,
-        "left": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#93a1a1",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#fdf6e3",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#002b36",
-        "padding": {
-          "left": 1
-        }
+    "sidebar_resize_handle": {
+      "background": "#002b36",
+      "padding": {
+        "left": 1
       }
     },
     "pane_divider": {
@@ -252,6 +208,21 @@
         "family": "Zed Sans",
         "color": "#93a1a1",
         "size": 14
+      },
+      "sidebar_item": {
+        "height": 32,
+        "icon_color": "#93a1a1",
+        "icon_size": 18
+      },
+      "sidebar_item_hover": {
+        "height": 32,
+        "icon_color": "#93a1a1",
+        "icon_size": 18
+      },
+      "sidebar_item_active": {
+        "height": 32,
+        "icon_color": "#fdf6e3",
+        "icon_size": 18
       }
     },
     "titlebar": {

assets/themes/solarized-light.json 🔗

@@ -161,54 +161,10 @@
       },
       "cursor": "Arrow"
     },
-    "left_sidebar": {
-      "width": 30,
-      "background": "#eee8d5",
-      "border": {
-        "color": "#fdf6e3",
-        "width": 1,
-        "right": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#586e75",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#002b36",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#fdf6e3",
-        "padding": {
-          "left": 1
-        }
-      }
-    },
-    "right_sidebar": {
-      "width": 30,
-      "background": "#eee8d5",
-      "border": {
-        "color": "#fdf6e3",
-        "width": 1,
-        "left": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#586e75",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#002b36",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#fdf6e3",
-        "padding": {
-          "left": 1
-        }
+    "sidebar_resize_handle": {
+      "background": "#fdf6e3",
+      "padding": {
+        "left": 1
       }
     },
     "pane_divider": {
@@ -252,6 +208,21 @@
         "family": "Zed Sans",
         "color": "#586e75",
         "size": 14
+      },
+      "sidebar_item": {
+        "height": 32,
+        "icon_color": "#586e75",
+        "icon_size": 18
+      },
+      "sidebar_item_hover": {
+        "height": 32,
+        "icon_color": "#586e75",
+        "icon_size": 18
+      },
+      "sidebar_item_active": {
+        "height": 32,
+        "icon_color": "#002b36",
+        "icon_size": 18
       }
     },
     "titlebar": {

assets/themes/sulphurpool-dark.json 🔗

@@ -161,54 +161,10 @@
       },
       "cursor": "Arrow"
     },
-    "left_sidebar": {
-      "width": 30,
-      "background": "#293256",
-      "border": {
-        "color": "#202746",
-        "width": 1,
-        "right": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#979db4",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#f5f7ff",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#202746",
-        "padding": {
-          "left": 1
-        }
-      }
-    },
-    "right_sidebar": {
-      "width": 30,
-      "background": "#293256",
-      "border": {
-        "color": "#202746",
-        "width": 1,
-        "left": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#979db4",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#f5f7ff",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#202746",
-        "padding": {
-          "left": 1
-        }
+    "sidebar_resize_handle": {
+      "background": "#202746",
+      "padding": {
+        "left": 1
       }
     },
     "pane_divider": {
@@ -252,6 +208,21 @@
         "family": "Zed Sans",
         "color": "#979db4",
         "size": 14
+      },
+      "sidebar_item": {
+        "height": 32,
+        "icon_color": "#979db4",
+        "icon_size": 18
+      },
+      "sidebar_item_hover": {
+        "height": 32,
+        "icon_color": "#979db4",
+        "icon_size": 18
+      },
+      "sidebar_item_active": {
+        "height": 32,
+        "icon_color": "#f5f7ff",
+        "icon_size": 18
       }
     },
     "titlebar": {

assets/themes/sulphurpool-light.json 🔗

@@ -161,54 +161,10 @@
       },
       "cursor": "Arrow"
     },
-    "left_sidebar": {
-      "width": 30,
-      "background": "#dfe2f1",
-      "border": {
-        "color": "#f5f7ff",
-        "width": 1,
-        "right": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#5e6687",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#202746",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#f5f7ff",
-        "padding": {
-          "left": 1
-        }
-      }
-    },
-    "right_sidebar": {
-      "width": 30,
-      "background": "#dfe2f1",
-      "border": {
-        "color": "#f5f7ff",
-        "width": 1,
-        "left": true
-      },
-      "item": {
-        "height": 32,
-        "icon_color": "#5e6687",
-        "icon_size": 18
-      },
-      "active_item": {
-        "height": 32,
-        "icon_color": "#202746",
-        "icon_size": 18
-      },
-      "resize_handle": {
-        "background": "#f5f7ff",
-        "padding": {
-          "left": 1
-        }
+    "sidebar_resize_handle": {
+      "background": "#f5f7ff",
+      "padding": {
+        "left": 1
       }
     },
     "pane_divider": {
@@ -252,6 +208,21 @@
         "family": "Zed Sans",
         "color": "#5e6687",
         "size": 14
+      },
+      "sidebar_item": {
+        "height": 32,
+        "icon_color": "#5e6687",
+        "icon_size": 18
+      },
+      "sidebar_item_hover": {
+        "height": 32,
+        "icon_color": "#5e6687",
+        "icon_size": 18
+      },
+      "sidebar_item_active": {
+        "height": 32,
+        "icon_color": "#202746",
+        "icon_size": 18
       }
     },
     "titlebar": {

crates/theme/src/theme.rs 🔗

@@ -38,8 +38,7 @@ pub struct Workspace {
     pub pane_divider: Border,
     pub leader_border_opacity: f32,
     pub leader_border_width: f32,
-    pub left_sidebar: Sidebar,
-    pub right_sidebar: Sidebar,
+    pub sidebar_resize_handle: ContainerStyle,
     pub status_bar: StatusBar,
     pub toolbar: Toolbar,
     pub disconnected_overlay: ContainedText,
@@ -139,16 +138,13 @@ pub struct FindEditor {
 
 #[derive(Deserialize, Default)]
 pub struct Sidebar {
-    #[serde(flatten)]
-    pub container: ContainerStyle,
-    pub width: f32,
-    pub item: SidebarItem,
-    pub active_item: SidebarItem,
     pub resize_handle: ContainerStyle,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Clone, Copy, Deserialize, Default)]
 pub struct SidebarItem {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
     pub icon_color: Color,
     pub icon_size: f32,
     pub height: f32,
@@ -165,6 +161,9 @@ pub struct StatusBar {
     pub lsp_message: TextStyle,
     pub auto_update_progress_message: TextStyle,
     pub auto_update_done_message: TextStyle,
+    pub sidebar_item: SidebarItem,
+    pub sidebar_item_active: SidebarItem,
+    pub sidebar_item_hover: SidebarItem,
 }
 
 #[derive(Deserialize, Default)]

crates/workspace/src/sidebar.rs 🔗

@@ -1,9 +1,14 @@
-use super::Workspace;
-use gpui::{elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, RenderContext};
+use gpui::{
+    elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, Entity, RenderContext, View,
+    ViewContext, ViewHandle,
+};
 use serde::Deserialize;
+use settings::Settings;
 use std::{cell::RefCell, rc::Rc};
 use theme::Theme;
 
+use crate::StatusItemView;
+
 pub struct Sidebar {
     side: Side,
     items: Vec<Item>,
@@ -12,31 +17,36 @@ pub struct Sidebar {
     custom_width: Rc<RefCell<f32>>,
 }
 
-#[derive(Clone, Copy, Deserialize)]
+#[derive(Clone, Copy, Debug, Deserialize)]
 pub enum Side {
     Left,
     Right,
 }
 
+#[derive(Clone)]
 struct Item {
     icon_path: &'static str,
     view: AnyViewHandle,
 }
 
-#[derive(Clone, Deserialize)]
-pub struct ToggleSidebarItem(pub SidebarItemId);
-
-#[derive(Clone, Deserialize)]
-pub struct ToggleSidebarItemFocus(pub SidebarItemId);
+pub struct SidebarButtons {
+    sidebar: ViewHandle<Sidebar>,
+}
 
-impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
+#[derive(Clone, Debug, Deserialize)]
+pub struct ToggleSidebarItem {
+    pub side: Side,
+    pub item_index: usize,
+}
 
-#[derive(Clone, Deserialize)]
-pub struct SidebarItemId {
+#[derive(Clone, Debug, Deserialize)]
+pub struct ToggleSidebarItemFocus {
     pub side: Side,
     pub item_index: usize,
 }
 
+impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
+
 impl Sidebar {
     pub fn new(side: Side) -> Self {
         Self {
@@ -48,20 +58,28 @@ impl Sidebar {
         }
     }
 
-    pub fn add_item(&mut self, icon_path: &'static str, view: AnyViewHandle) {
+    pub fn add_item(
+        &mut self,
+        icon_path: &'static str,
+        view: AnyViewHandle,
+        cx: &mut ViewContext<Self>,
+    ) {
         self.items.push(Item { icon_path, view });
+        cx.notify()
     }
 
-    pub fn activate_item(&mut self, item_ix: usize) {
+    pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
         self.active_item_ix = Some(item_ix);
+        cx.notify();
     }
 
-    pub fn toggle_item(&mut self, item_ix: usize) {
+    pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
         if self.active_item_ix == Some(item_ix) {
             self.active_item_ix = None;
         } else {
             self.active_item_ix = Some(item_ix);
         }
+        cx.notify();
     }
 
     pub fn active_item(&self) -> Option<&AnyViewHandle> {
@@ -70,75 +88,56 @@ impl Sidebar {
             .map(|item| &item.view)
     }
 
-    fn theme<'a>(&self, theme: &'a Theme) -> &'a theme::Sidebar {
-        match self.side {
-            Side::Left => &theme.workspace.left_sidebar,
-            Side::Right => &theme.workspace.right_sidebar,
-        }
-    }
-
-    pub fn render(&self, theme: &Theme, cx: &mut RenderContext<Workspace>) -> ElementBox {
+    fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
+        let actual_width = self.actual_width.clone();
+        let custom_width = self.custom_width.clone();
         let side = self.side;
-        let theme = self.theme(theme);
-
-        ConstrainedBox::new(
-            Container::new(
-                Flex::column()
-                    .with_children(self.items.iter().enumerate().map(|(item_index, item)| {
-                        let theme = if Some(item_index) == self.active_item_ix {
-                            &theme.active_item
-                        } else {
-                            &theme.item
-                        };
-                        enum SidebarButton {}
-                        MouseEventHandler::new::<SidebarButton, _, _>(item.view.id(), cx, |_, _| {
-                            ConstrainedBox::new(
-                                Align::new(
-                                    ConstrainedBox::new(
-                                        Svg::new(item.icon_path)
-                                            .with_color(theme.icon_color)
-                                            .boxed(),
-                                    )
-                                    .with_height(theme.icon_size)
-                                    .boxed(),
-                                )
-                                .boxed(),
-                            )
-                            .with_height(theme.height)
-                            .boxed()
-                        })
-                        .with_cursor_style(CursorStyle::PointingHand)
-                        .on_mouse_down(move |cx| {
-                            cx.dispatch_action(ToggleSidebarItem(SidebarItemId {
-                                side,
-                                item_index,
-                            }))
-                        })
-                        .boxed()
-                    }))
-                    .boxed(),
-            )
-            .with_style(theme.container)
-            .boxed(),
-        )
-        .with_width(theme.width)
+        MouseEventHandler::new::<Self, _, _>(side as usize, cx, |_, _| {
+            Empty::new()
+                .contained()
+                .with_style(theme.workspace.sidebar_resize_handle)
+                .boxed()
+        })
+        .with_padding(Padding {
+            left: 4.,
+            right: 4.,
+            ..Default::default()
+        })
+        .with_cursor_style(CursorStyle::ResizeLeftRight)
+        .on_drag(move |delta, cx| {
+            let prev_width = *actual_width.borrow();
+            match side {
+                Side::Left => *custom_width.borrow_mut() = 0f32.max(prev_width + delta.x()),
+                Side::Right => *custom_width.borrow_mut() = 0f32.max(prev_width - delta.x()),
+            }
+
+            cx.notify();
+        })
         .boxed()
     }
+}
+
+impl Entity for Sidebar {
+    type Event = ();
+}
 
-    pub fn render_active_item(
-        &self,
-        theme: &Theme,
-        cx: &mut RenderContext<Workspace>,
-    ) -> Option<ElementBox> {
+impl View for Sidebar {
+    fn ui_name() -> &'static str {
+        "Sidebar"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = cx.global::<Settings>().theme.clone();
         if let Some(active_item) = self.active_item() {
             let mut container = Flex::row();
             if matches!(self.side, Side::Right) {
-                container.add_child(self.render_resize_handle(theme, cx));
+                container.add_child(self.render_resize_handle(&theme, cx));
             }
 
             container.add_child(
                 Hook::new(
-                    ConstrainedBox::new(ChildView::new(active_item).boxed())
+                    ChildView::new(active_item)
+                        .constrained()
                         .with_max_width(*self.custom_width.borrow())
                         .boxed(),
                 )
@@ -150,38 +149,76 @@ impl Sidebar {
                 .boxed(),
             );
             if matches!(self.side, Side::Left) {
-                container.add_child(self.render_resize_handle(theme, cx));
+                container.add_child(self.render_resize_handle(&theme, cx));
             }
-            Some(container.boxed())
+            container.boxed()
         } else {
-            None
+            Empty::new().boxed()
         }
     }
+}
 
-    fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Workspace>) -> ElementBox {
-        let actual_width = self.actual_width.clone();
-        let custom_width = self.custom_width.clone();
-        let side = self.side;
-        MouseEventHandler::new::<Self, _, _>(side as usize, cx, |_, _| {
-            Container::new(Empty::new().boxed())
-                .with_style(self.theme(theme).resize_handle)
+impl SidebarButtons {
+    pub fn new(sidebar: ViewHandle<Sidebar>, cx: &mut ViewContext<Self>) -> Self {
+        cx.observe(&sidebar, |_, _, cx| cx.notify()).detach();
+        Self { sidebar }
+    }
+}
+
+impl Entity for SidebarButtons {
+    type Event = ();
+}
+
+impl View for SidebarButtons {
+    fn ui_name() -> &'static str {
+        "SidebarToggleButton"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = &cx.global::<Settings>().theme.workspace.status_bar;
+        let style = theme.sidebar_item;
+        let hover_style = theme.sidebar_item_hover;
+        let active_style = theme.sidebar_item_active;
+        let sidebar = self.sidebar.read(cx);
+        let active_ix = sidebar.active_item_ix;
+        let side = sidebar.side;
+        let items = sidebar.items.clone();
+        Flex::row()
+            .with_children(items.iter().enumerate().map(|(ix, item)| {
+                MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, _| {
+                    let style = if Some(ix) == active_ix {
+                        active_style
+                    } else if state.hovered {
+                        hover_style
+                    } else {
+                        style
+                    };
+                    Svg::new(item.icon_path)
+                        .with_color(style.icon_color)
+                        .constrained()
+                        .with_height(style.icon_size)
+                        .contained()
+                        .with_style(style.container)
+                        .boxed()
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(move |cx| {
+                    cx.dispatch_action(ToggleSidebarItem {
+                        side,
+                        item_index: ix,
+                    })
+                })
                 .boxed()
-        })
-        .with_padding(Padding {
-            left: 4.,
-            right: 4.,
-            ..Default::default()
-        })
-        .with_cursor_style(CursorStyle::ResizeLeftRight)
-        .on_drag(move |delta, cx| {
-            let prev_width = *actual_width.borrow();
-            match side {
-                Side::Left => *custom_width.borrow_mut() = 0f32.max(prev_width + delta.x()),
-                Side::Right => *custom_width.borrow_mut() = 0f32.max(prev_width - delta.x()),
-            }
+            }))
+            .boxed()
+    }
+}
 
-            cx.notify();
-        })
-        .boxed()
+impl StatusItemView for SidebarButtons {
+    fn set_active_pane_item(
+        &mut self,
+        _: Option<&dyn crate::ItemHandle>,
+        _: &mut ViewContext<Self>,
+    ) {
     }
 }

crates/workspace/src/status_bar.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{ItemHandle, Pane};
-use settings::Settings;
 use gpui::{
     elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
     View, ViewContext, ViewHandle,
 };
+use settings::Settings;
 
 pub trait StatusItemView: View {
     fn set_active_pane_item(
@@ -48,7 +48,7 @@ impl View for StatusBar {
                     .with_margin_right(theme.item_spacing)
                     .boxed()
             }))
-            .with_children(self.right_items.iter().map(|i| {
+            .with_children(self.right_items.iter().rev().map(|i| {
                 ChildView::new(i.as_ref())
                     .aligned()
                     .contained()

crates/workspace/src/workspace.rs 🔗

@@ -31,7 +31,7 @@ pub use pane_group::*;
 use postage::prelude::Stream;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
 use settings::Settings;
-use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
+use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
@@ -678,8 +678,8 @@ pub struct Workspace {
     themes: Arc<ThemeRegistry>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
-    left_sidebar: Sidebar,
-    right_sidebar: Sidebar,
+    left_sidebar: ViewHandle<Sidebar>,
+    right_sidebar: ViewHandle<Sidebar>,
     panes: Vec<ViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
     status_bar: ViewHandle<StatusBar>,
@@ -751,7 +751,6 @@ impl Workspace {
         cx.focus(&pane);
         cx.emit(Event::PaneAdded(pane.clone()));
 
-        let status_bar = cx.add_view(|cx| StatusBar::new(&pane, cx));
         let mut current_user = params.user_store.read(cx).watch_current_user().clone();
         let mut connection_status = params.client.status().clone();
         let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
@@ -773,6 +772,18 @@ impl Workspace {
 
         cx.emit_global(WorkspaceCreated(weak_self.clone()));
 
+        let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
+        let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
+        let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
+        let right_sidebar_buttons =
+            cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
+        let status_bar = cx.add_view(|cx| {
+            let mut status_bar = StatusBar::new(&pane.clone(), cx);
+            status_bar.add_left_item(left_sidebar_buttons, cx);
+            status_bar.add_right_item(right_sidebar_buttons, cx);
+            status_bar
+        });
+
         let mut this = Workspace {
             modal: None,
             weak_self,
@@ -785,8 +796,8 @@ impl Workspace {
             user_store: params.user_store.clone(),
             fs: params.fs.clone(),
             themes: params.themes.clone(),
-            left_sidebar: Sidebar::new(Side::Left),
-            right_sidebar: Sidebar::new(Side::Right),
+            left_sidebar,
+            right_sidebar,
             project: params.project.clone(),
             leader_state: Default::default(),
             follower_states_by_leader: Default::default(),
@@ -801,12 +812,12 @@ impl Workspace {
         self.weak_self.clone()
     }
 
-    pub fn left_sidebar_mut(&mut self) -> &mut Sidebar {
-        &mut self.left_sidebar
+    pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
+        &self.left_sidebar
     }
 
-    pub fn right_sidebar_mut(&mut self) -> &mut Sidebar {
-        &mut self.right_sidebar
+    pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
+        &self.right_sidebar
     }
 
     pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
@@ -1028,12 +1039,15 @@ impl Workspace {
     }
 
     pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
-        let sidebar = match action.0.side {
+        let sidebar = match action.side {
             Side::Left => &mut self.left_sidebar,
             Side::Right => &mut self.right_sidebar,
         };
-        sidebar.toggle_item(action.0.item_index);
-        if let Some(active_item) = sidebar.active_item() {
+        let active_item = sidebar.update(cx, |sidebar, cx| {
+            sidebar.toggle_item(action.item_index, cx);
+            sidebar.active_item().cloned()
+        });
+        if let Some(active_item) = active_item {
             cx.focus(active_item);
         } else {
             cx.focus_self();
@@ -1046,12 +1060,15 @@ impl Workspace {
         action: &ToggleSidebarItemFocus,
         cx: &mut ViewContext<Self>,
     ) {
-        let sidebar = match action.0.side {
+        let sidebar = match action.side {
             Side::Left => &mut self.left_sidebar,
             Side::Right => &mut self.right_sidebar,
         };
-        sidebar.activate_item(action.0.item_index);
-        if let Some(active_item) = sidebar.active_item() {
+        let active_item = sidebar.update(cx, |sidebar, cx| {
+            sidebar.toggle_item(action.item_index, cx);
+            sidebar.active_item().cloned()
+        });
+        if let Some(active_item) = active_item {
             if active_item.is_focused(cx) {
                 cx.focus_self();
             } else {
@@ -1610,7 +1627,7 @@ impl Workspace {
                     .boxed(),
             )
             .constrained()
-            .with_width(theme.workspace.right_sidebar.width)
+            .with_width(theme.workspace.titlebar.avatar_width)
             .contained()
             .with_margin_left(2.)
             .boxed();
@@ -1656,7 +1673,7 @@ impl Workspace {
                         .with_width(24.)
                         .aligned()
                         .constrained()
-                        .with_width(theme.workspace.right_sidebar.width)
+                        .with_width(24.)
                         .aligned()
                         .boxed()
                 })
@@ -1983,37 +2000,39 @@ impl View for Workspace {
                     .with_child(
                         Stack::new()
                             .with_child({
-                                let mut content = Flex::row();
-                                content.add_child(self.left_sidebar.render(&theme, cx));
-                                if let Some(element) =
-                                    self.left_sidebar.render_active_item(&theme, cx)
-                                {
-                                    content
-                                        .add_child(FlexItem::new(element).flex(0.8, false).boxed());
-                                }
-                                content.add_child(
-                                    Flex::column()
-                                        .with_child(
-                                            FlexItem::new(self.center.render(
-                                                &theme,
-                                                &self.follower_states_by_leader,
-                                                self.project.read(cx).collaborators(),
-                                            ))
-                                            .flex(1., true)
-                                            .boxed(),
-                                        )
-                                        .with_child(ChildView::new(&self.status_bar).boxed())
+                                Flex::row()
+                                    .with_children(
+                                        if self.left_sidebar.read(cx).active_item().is_some() {
+                                            Some(
+                                                ChildView::new(&self.left_sidebar)
+                                                    .flex(0.8, false)
+                                                    .boxed(),
+                                            )
+                                        } else {
+                                            None
+                                        },
+                                    )
+                                    .with_child(
+                                        FlexItem::new(self.center.render(
+                                            &theme,
+                                            &self.follower_states_by_leader,
+                                            self.project.read(cx).collaborators(),
+                                        ))
                                         .flex(1., true)
                                         .boxed(),
-                                );
-                                if let Some(element) =
-                                    self.right_sidebar.render_active_item(&theme, cx)
-                                {
-                                    content
-                                        .add_child(FlexItem::new(element).flex(0.8, false).boxed());
-                                }
-                                content.add_child(self.right_sidebar.render(&theme, cx));
-                                content.boxed()
+                                    )
+                                    .with_children(
+                                        if self.right_sidebar.read(cx).active_item().is_some() {
+                                            Some(
+                                                ChildView::new(&self.right_sidebar)
+                                                    .flex(0.8, false)
+                                                    .boxed(),
+                                            )
+                                        } else {
+                                            None
+                                        },
+                                    )
+                                    .boxed()
                             })
                             .with_children(self.modal.as_ref().map(|m| {
                                 ChildView::new(m)
@@ -2026,6 +2045,7 @@ impl View for Workspace {
                             .flex(1.0, true)
                             .boxed(),
                     )
+                    .with_child(ChildView::new(&self.status_bar).boxed())
                     .contained()
                     .with_background_color(theme.workspace.background)
                     .boxed(),
@@ -2202,10 +2222,10 @@ pub fn open_paths(
                 let mut workspace = (app_state.build_workspace)(project, &app_state, cx);
                 if contains_directory {
                     workspace.toggle_sidebar_item(
-                        &ToggleSidebarItem(SidebarItemId {
+                        &ToggleSidebarItem {
                             side: Side::Left,
                             item_index: 0,
-                        }),
+                        },
                         cx,
                     );
                 }

crates/zed/src/zed.rs 🔗

@@ -6,7 +6,6 @@ pub mod test;
 
 use anyhow::{anyhow, Context, Result};
 use breadcrumbs::Breadcrumbs;
-use chat_panel::ChatPanel;
 pub use client;
 pub use contacts_panel;
 use contacts_panel::ContactsPanel;
@@ -147,7 +146,7 @@ pub fn build_workspace(
         user_store: app_state.user_store.clone(),
         channel_list: app_state.channel_list.clone(),
     };
-    let mut workspace = Workspace::new(&workspace_params, cx);
+    let workspace = Workspace::new(&workspace_params, cx);
     let project = workspace.project().clone();
 
     let theme_names = app_state.themes.list().collect();
@@ -171,22 +170,15 @@ pub fn build_workspace(
         }));
     });
 
-    workspace.left_sidebar_mut().add_item(
-        "icons/folder-tree-16.svg",
-        ProjectPanel::new(project, cx).into(),
-    );
-    workspace.right_sidebar_mut().add_item(
-        "icons/user-16.svg",
-        cx.add_view(|cx| ContactsPanel::new(app_state.clone(), cx))
-            .into(),
-    );
-    workspace.right_sidebar_mut().add_item(
-        "icons/comment-16.svg",
-        cx.add_view(|cx| {
-            ChatPanel::new(app_state.client.clone(), app_state.channel_list.clone(), cx)
-        })
-        .into(),
-    );
+    let project_panel = ProjectPanel::new(project, cx);
+    let contact_panel = cx.add_view(|cx| ContactsPanel::new(app_state.clone(), cx));
+
+    workspace.left_sidebar().update(cx, |sidebar, cx| {
+        sidebar.add_item("icons/folder-tree-16.svg", project_panel.into(), cx)
+    });
+    workspace.right_sidebar().update(cx, |sidebar, cx| {
+        sidebar.add_item("icons/user-16.svg", contact_panel.into(), cx)
+    });
 
     let diagnostic_message = cx.add_view(|_| editor::items::DiagnosticMessage::new());
     let diagnostic_summary =
@@ -200,8 +192,8 @@ pub fn build_workspace(
         status_bar.add_left_item(diagnostic_summary, cx);
         status_bar.add_left_item(diagnostic_message, cx);
         status_bar.add_left_item(lsp_status, cx);
-        status_bar.add_right_item(auto_update, cx);
         status_bar.add_right_item(cursor_position, cx);
+        status_bar.add_right_item(auto_update, cx);
     });
 
     workspace
@@ -362,7 +354,7 @@ mod tests {
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
         workspace_1.update(cx, |workspace, cx| {
             assert_eq!(workspace.worktrees(cx).count(), 2);
-            assert!(workspace.left_sidebar_mut().active_item().is_some());
+            assert!(workspace.left_sidebar().read(cx).active_item().is_some());
             assert!(workspace.active_pane().is_focused(cx));
         });
 

styles/src/styleTree/workspace.ts 🔗

@@ -52,22 +52,6 @@ export default function workspace(theme: Theme) {
     iconColor: iconColor(theme, "secondary"),
     iconSize: 18,
   };
-  const sidebar = {
-    width: 30,
-    background: backgroundColor(theme, 300),
-    border: border(theme, "primary", { right: true }),
-    item: sidebarItem,
-    activeItem: {
-      ...sidebarItem,
-      iconColor: iconColor(theme, "active"),
-    },
-    resizeHandle: {
-      background: border(theme, "primary").color,
-      padding: {
-        left: 1,
-      },
-    },
-  };
   const shareIcon = {
     margin: { top: 3, bottom: 2 },
     cornerRadius: 6,
@@ -86,19 +70,17 @@ export default function workspace(theme: Theme) {
       },
       cursor: "Arrow"
     },
-    leftSidebar: {
-      ...sidebar,
-      border: border(theme, "primary", { right: true }),
-    },
-    rightSidebar: {
-      ...sidebar,
-      border: border(theme, "primary", { left: true }),
+    sidebarResizeHandle: {
+      background: border(theme, "primary").color,
+      padding: {
+        left: 1,
+      },
     },
     paneDivider: {
       color: border(theme, "secondary").color,
       width: 1,
     },
-    status_bar: {
+    statusBar: {
       height: 24,
       itemSpacing: 8,
       padding: {
@@ -111,6 +93,16 @@ export default function workspace(theme: Theme) {
       lspMessage: text(theme, "sans", "muted"),
       autoUpdateProgressMessage: text(theme, "sans", "muted"),
       autoUpdateDoneMessage: text(theme, "sans", "muted"),
+      sidebarItem: {
+        ...sidebarItem
+      },
+      sidebarItemHover: {
+        ...sidebarItem
+      },
+      sidebarItemActive: {
+        ...sidebarItem,
+        iconColor: iconColor(theme, "active"),
+      },
     },
     titlebar: {
       avatarWidth: 18,