Add sidebars

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

zed/assets/icons/comment-16.svg     |  3 +
zed/assets/icons/folder-tree-16.svg |  1 
zed/assets/icons/user-16.svg        |  3 +
zed/assets/themes/_base.toml        | 41 ++++++++-----
zed/assets/themes/dark.toml         | 21 ++++--
zed/src/lib.rs                      |  1 
zed/src/project_browser.rs          | 19 ++++++
zed/src/theme.rs                    |  8 ++
zed/src/workspace.rs                | 86 +++++++++++++++++++++++++---
zed/src/workspace/sidebar.rs        | 93 +++++++++++++++++++++++++++++++
10 files changed, 241 insertions(+), 35 deletions(-)

Detailed changes

zed/assets/icons/comment-16.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.01234 1.86426C4.13913 1.86426 1.00077 4.41444 1.00077 7.56176C1.00077 8.86644 1.54614 10.0613 2.45007 11.0186C2.04248 12.1006 1.19361 13.0149 1.17991 13.0251C0.998442 13.2168 0.950506 13.4976 1.05323 13.7373C1.15939 13.9769 1.39392 14.1358 1.65743 14.1358C3.34203 14.1358 4.64588 13.4305 5.46764 12.8689C6.23461 13.1168 7.11663 13.2593 8.01234 13.2593C11.8855 13.2593 15 10.7083 15 7.56176C15 4.41526 11.8855 1.86426 8.01234 1.86426ZM8.01508 11.9445C7.28235 11.9445 6.56002 11.8315 5.86811 11.6125L5.24494 11.4173L4.7108 11.7939C4.32047 12.0711 3.78276 12.3796 3.13577 12.5883C3.33778 12.2563 3.52939 11.883 3.68032 11.4858L3.97122 10.7188L3.4064 10.1198C2.91252 9.5915 2.31675 8.7177 2.31675 7.56176C2.31675 5.14443 4.87104 3.17907 8.01426 3.17907C11.1575 3.17907 13.7118 5.14443 13.7118 7.56176C13.7118 9.97909 11.1569 11.9445 8.01508 11.9445Z" fill="#7E7E83"/>
+</svg>

zed/assets/icons/user-16.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.3125 9.3125H6.6875C4.02969 9.3125 1.875 11.4672 1.875 14.125C1.875 14.6082 2.26684 15 2.75 15H13.25C13.7332 15 14.125 14.6082 14.125 14.125C14.125 11.4672 11.9703 9.3125 9.3125 9.3125ZM3.21457 13.6875C3.43059 11.9621 4.90469 10.625 6.6875 10.625H9.3125C11.0942 10.625 12.5691 11.9635 12.7852 13.6875H3.21457ZM8 8C9.93293 8 11.5 6.43293 11.5 4.5C11.5 2.56707 9.93293 1 8 1C6.06707 1 4.5 2.56707 4.5 4.5C4.5 6.4332 6.0668 8 8 8ZM8 2.3125C9.20613 2.3125 10.1875 3.29387 10.1875 4.5C10.1875 5.70613 9.20613 6.6875 8 6.6875C6.79387 6.6875 5.8125 5.70586 5.8125 4.5C5.8125 3.29387 6.79414 2.3125 8 2.3125Z" fill="#9BA8BE"/>
+</svg>

zed/assets/themes/_base.toml 🔗

@@ -2,22 +2,29 @@
 background = "$surface.0"
 
 [tab]
-background = "$surface.1"
-text = "$text_color.dull"
-border = { color = "#000000", width = 1.0 }
+text = "$text.2"
 padding = { left = 10, right = 10 }
-icon_close = "#383839"
-icon_dirty = "#556de8"
-icon_conflict = "#e45349"
+icon_close = "$text.0"
+icon_dirty = "$status.info"
+icon_conflict = "$status.warn"
 
 [active_tab]
 extends = "$tab"
-background = "$surface.2"
-text = "$text_color.bright"
+background = "$surface.1"
+text = "$text.0"
+
+[sidebar]
+padding = { left = 10, right = 10 }
+
+[sidebar_icon]
+color = "$text.2"
+
+[active_sidebar_icon]
+color = "$text.0"
 
 [selector]
-background = "$surface.3"
-text = "$text_color.bright"
+background = "$surface.2"
+text = "$text.0"
 padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 }
 margin.top = 12.0
 corner_radius = 6.0
@@ -35,13 +42,13 @@ extends = "$selector.item"
 background = "#094771"
 
 [editor]
-background = "$surface.2"
-gutter_background = "$surface.2"
-active_line_background = "$surface.3"
-line_number = "$text_color.dull"
-line_number_active = "$text_color.bright"
-text = "$text_color.normal"
+background = "$surface.1"
+gutter_background = "$surface.1"
+active_line_background = "$surface.2"
+line_number = "$text.2"
+line_number_active = "$text.0"
+text = "$text.1"
 replicas = [
-    { selection = "#264f78", cursor = "$text_color.bright" },
+    { selection = "#264f78", cursor = "$text.0" },
     { selection = "#504f31", cursor = "#fcf154" },
 ]

zed/assets/themes/dark.toml 🔗

@@ -1,15 +1,20 @@
 extends = "_base"
 
 [surface]
-0 = "#050101"
-1 = "#131415"
-2 = "#1c1d1e"
-3 = "#3a3b3c"
+0 = "#222324"
+1 = "#141516"
+2 = "#131415"
 
-[text_color]
-dull = "#5a5a5b"
-bright = "#ffffff"
-normal = "#d4d4d4"
+[text]
+0 = "#ffffff"
+1 = "#b3b3b3"
+2 = "#7b7d80"
+
+[status]
+good = "#4fac63"
+info = "#3c5dd4"
+warn = "#faca50"
+bad = "#b7372e"
 
 [syntax]
 keyword = { color = "#0086c0", weight = "bold" }

zed/src/lib.rs 🔗

@@ -7,6 +7,7 @@ mod fuzzy;
 pub mod language;
 pub mod menus;
 mod operation_queue;
+pub mod project_browser;
 pub mod rpc;
 pub mod settings;
 mod sum_tree;

zed/src/project_browser.rs 🔗

@@ -0,0 +1,19 @@
+use gpui::{elements::Empty, Element, Entity, View};
+
+pub struct ProjectBrowser;
+
+pub enum Event {}
+
+impl Entity for ProjectBrowser {
+    type Event = Event;
+}
+
+impl View for ProjectBrowser {
+    fn ui_name() -> &'static str {
+        "ProjectBrowser"
+    }
+
+    fn render(&self, _: &gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+        Empty::new().boxed()
+    }
+}

zed/src/theme.rs 🔗

@@ -33,6 +33,9 @@ pub struct Theme {
     pub workspace: Workspace,
     pub tab: Tab,
     pub active_tab: Tab,
+    pub sidebar: ContainerStyle,
+    pub sidebar_icon: SidebarIcon,
+    pub active_sidebar_icon: SidebarIcon,
     pub selector: Selector,
     pub editor: Editor,
     #[serde(deserialize_with = "deserialize_syntax_theme")]
@@ -72,6 +75,11 @@ pub struct Tab {
     pub icon_conflict: Color,
 }
 
+#[derive(Debug, Default, Deserialize)]
+pub struct SidebarIcon {
+    pub color: Color,
+}
+
 #[derive(Debug, Default, Deserialize)]
 pub struct Selector {
     #[serde(flatten)]

zed/src/workspace.rs 🔗

@@ -1,10 +1,12 @@
 pub mod pane;
 pub mod pane_group;
+pub mod sidebar;
 
 use crate::{
     editor::{Buffer, Editor},
     fs::Fs,
     language::LanguageRegistry,
+    project_browser::ProjectBrowser,
     rpc,
     settings::Settings,
     worktree::{File, Worktree},
@@ -25,6 +27,7 @@ use log::error;
 pub use pane::*;
 pub use pane_group::*;
 use postage::watch;
+use sidebar::{Side, Sidebar};
 use smol::prelude::*;
 use std::{
     collections::{hash_map::Entry, HashMap, HashSet},
@@ -46,6 +49,10 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action("workspace:new_file", Workspace::open_new_file);
     cx.add_action("workspace:share_worktree", Workspace::share_worktree);
     cx.add_action("workspace:join_worktree", Workspace::join_worktree);
+    cx.add_action(
+        "workspace:toggle_sidebar_item",
+        Workspace::toggle_sidebar_item,
+    );
     cx.add_bindings(vec![
         Binding::new("cmd-s", "workspace:save", None),
         Binding::new("cmd-alt-i", "workspace:debug_elements", None),
@@ -318,12 +325,6 @@ impl Clone for Box<dyn ItemHandle> {
     }
 }
 
-#[derive(Debug)]
-pub struct State {
-    pub modal: Option<usize>,
-    pub center: PaneGroup,
-}
-
 pub struct Workspace {
     pub settings: watch::Receiver<Settings>,
     languages: Arc<LanguageRegistry>,
@@ -331,6 +332,8 @@ pub struct Workspace {
     fs: Arc<dyn Fs>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
+    left_sidebar: Sidebar,
+    right_sidebar: Sidebar,
     panes: Vec<ViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
     worktrees: HashSet<ModelHandle<Worktree>>,
@@ -350,6 +353,19 @@ impl Workspace {
         });
         cx.focus(&pane);
 
+        let mut left_sidebar = Sidebar::new(Side::Left);
+        left_sidebar.add_item(
+            "icons/folder-tree-16.svg",
+            cx.add_view(|_| ProjectBrowser).into(),
+        );
+
+        let mut right_sidebar = Sidebar::new(Side::Right);
+        right_sidebar.add_item(
+            "icons/comment-16.svg",
+            cx.add_view(|_| ProjectBrowser).into(),
+        );
+        right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
+
         Workspace {
             modal: None,
             center: PaneGroup::new(pane.id()),
@@ -359,6 +375,8 @@ impl Workspace {
             languages: app_state.languages.clone(),
             rpc: app_state.rpc.clone(),
             fs: app_state.fs.clone(),
+            left_sidebar,
+            right_sidebar,
             worktrees: Default::default(),
             items: Default::default(),
             loading_items: Default::default(),
@@ -724,6 +742,19 @@ impl Workspace {
         }
     }
 
+    pub fn toggle_sidebar_item(
+        &mut self,
+        (side, item_ix): &(Side, usize),
+        cx: &mut ViewContext<Self>,
+    ) {
+        let sidebar = match side {
+            Side::Left => &mut self.left_sidebar,
+            Side::Right => &mut self.right_sidebar,
+        };
+        sidebar.toggle_item(*item_ix);
+        cx.notify();
+    }
+
     pub fn debug_elements(&mut self, _: &(), cx: &mut ViewContext<Self>) {
         match to_string_pretty(&cx.debug_elements()) {
             Ok(json) => {
@@ -892,12 +923,47 @@ impl View for Workspace {
         "Workspace"
     }
 
-    fn render(&self, _: &RenderContext<Self>) -> ElementBox {
+    fn render(&self, cx: &RenderContext<Self>) -> ElementBox {
         let settings = self.settings.borrow();
         Container::new(
-            Stack::new()
-                .with_child(self.center.render())
-                .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
+            Flex::column()
+                .with_child(
+                    ConstrainedBox::new(Empty::new().boxed())
+                        .with_height(cx.titlebar_height)
+                        .named("titlebar"),
+                )
+                .with_child(
+                    Expanded::new(
+                        1.0,
+                        Stack::new()
+                            .with_child({
+                                let mut content = Flex::row();
+                                content.add_child(self.left_sidebar.render(&settings, cx));
+                                if let Some(panel) = self.left_sidebar.active_item() {
+                                    content.add_child(
+                                        ConstrainedBox::new(ChildView::new(panel.id()).boxed())
+                                            .with_width(200.0)
+                                            .named("left panel"),
+                                    );
+                                }
+                                content.add_child(Expanded::new(1.0, self.center.render()).boxed());
+                                if let Some(panel) = self.right_sidebar.active_item() {
+                                    content.add_child(
+                                        ConstrainedBox::new(ChildView::new(panel.id()).boxed())
+                                            .with_width(200.0)
+                                            .named("right panel"),
+                                    );
+                                }
+                                content.add_child(self.right_sidebar.render(&settings, cx));
+                                content.boxed()
+                            })
+                            .with_children(
+                                self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()),
+                            )
+                            .boxed(),
+                    )
+                    .boxed(),
+                )
                 .boxed(),
         )
         .with_background_color(settings.theme.workspace.background)

zed/src/workspace/sidebar.rs 🔗

@@ -0,0 +1,93 @@
+use crate::Settings;
+use gpui::{
+    elements::{
+        Align, ConstrainedBox, Container, Flex, MouseEventHandler, ParentElement as _, Svg,
+    },
+    AnyViewHandle, AppContext, Element as _, ElementBox,
+};
+
+pub struct Sidebar {
+    side: Side,
+    items: Vec<Item>,
+    active_item_ix: Option<usize>,
+}
+
+#[derive(Clone, Copy)]
+pub enum Side {
+    Left,
+    Right,
+}
+
+struct Item {
+    icon_path: &'static str,
+    view: AnyViewHandle,
+}
+
+impl Sidebar {
+    pub fn new(side: Side) -> Self {
+        Self {
+            side,
+            items: Default::default(),
+            active_item_ix: None,
+        }
+    }
+
+    pub fn add_item(&mut self, icon_path: &'static str, view: AnyViewHandle) {
+        self.items.push(Item { icon_path, view });
+    }
+
+    pub fn toggle_item(&mut self, item_ix: usize) {
+        if self.active_item_ix == Some(item_ix) {
+            self.active_item_ix = None;
+        } else {
+            self.active_item_ix = Some(item_ix)
+        }
+    }
+
+    pub fn active_item(&self) -> Option<&AnyViewHandle> {
+        self.active_item_ix
+            .and_then(|ix| self.items.get(ix))
+            .map(|item| &item.view)
+    }
+
+    pub fn render(&self, settings: &Settings, cx: &AppContext) -> ElementBox {
+        let side = self.side;
+        let line_height = cx.font_cache().line_height(
+            cx.font_cache().default_font(settings.ui_font_family),
+            settings.ui_font_size,
+        );
+
+        Container::new(
+            Flex::column()
+                .with_children(self.items.iter().enumerate().map(|(item_ix, item)| {
+                    let theme = if Some(item_ix) == self.active_item_ix {
+                        &settings.theme.active_sidebar_icon
+                    } else {
+                        &settings.theme.sidebar_icon
+                    };
+                    enum SidebarButton {}
+                    MouseEventHandler::new::<SidebarButton, _>(item.view.id(), cx, |_| {
+                        ConstrainedBox::new(
+                            Align::new(
+                                ConstrainedBox::new(
+                                    Svg::new(item.icon_path).with_color(theme.color).boxed(),
+                                )
+                                .with_height(line_height)
+                                .boxed(),
+                            )
+                            .boxed(),
+                        )
+                        .with_height(line_height + 16.0)
+                        .boxed()
+                    })
+                    .on_click(move |cx| {
+                        cx.dispatch_action("workspace:toggle_sidebar_item", (side, item_ix))
+                    })
+                    .boxed()
+                }))
+                .boxed(),
+        )
+        .with_style(&settings.theme.sidebar)
+        .boxed()
+    }
+}