Update Tab Bar & Toolbar (#3503)

Marshall Bowers created

- Work on default light theme
- Update tab bar and tabs
- Port quick_action_bar crate to zed2
- Add `Indicator` component
- Add `v_stack` & `h_stack` to ui::prelude::*

Release Notes:

- N/A

Change summary

Cargo.lock                                       |  12 
Cargo.toml                                       |   1 
crates/editor2/src/items.rs                      |  43 -
crates/quick_action_bar2/Cargo.toml              |  22 +
crates/quick_action_bar2/src/quick_action_bar.rs | 288 +++++++++++++
crates/theme2/src/default_colors.rs              |  24 
crates/theme2/src/default_theme.rs               |  80 ++-
crates/theme2/src/registry.rs                    |   6 
crates/theme2/src/styles/syntax.rs               |  16 
crates/ui2/src/components.rs                     |   2 
crates/ui2/src/components/icon.rs                |  20 
crates/ui2/src/components/indicator.rs           |  60 ++
crates/ui2/src/prelude.rs                        |   1 
crates/workspace2/src/pane.rs                    | 376 +++++++++--------
crates/workspace2/src/toolbar.rs                 |  24 -
crates/zed2/Cargo.toml                           |   2 
crates/zed2/src/zed2.rs                          |  10 
17 files changed, 689 insertions(+), 298 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7071,6 +7071,17 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "quick_action_bar2"
+version = "0.1.0"
+dependencies = [
+ "editor2",
+ "gpui2",
+ "search2",
+ "ui2",
+ "workspace2",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.33"
@@ -11890,6 +11901,7 @@ dependencies = [
  "postage",
  "project2",
  "project_panel2",
+ "quick_action_bar2",
  "rand 0.8.5",
  "regex",
  "rope2",

Cargo.toml 🔗

@@ -89,6 +89,7 @@ members = [
     "crates/project_panel",
     "crates/project_panel2",
     "crates/project_symbols",
+    "crates/quick_action_bar2",
     "crates/recent_projects",
     "crates/rope",
     "crates/rpc",

crates/editor2/src/items.rs 🔗

@@ -32,7 +32,7 @@ use std::{
 };
 use text::Selection;
 use theme::{ActiveTheme, Theme};
-use ui::{Color, Label};
+use ui::{h_stack, Color, Label};
 use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
 use workspace::{
     item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle},
@@ -586,28 +586,25 @@ impl Item for Editor {
     fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
         let theme = cx.theme();
 
-        AnyElement::new(
-            div()
-                .flex()
-                .flex_row()
-                .items_center()
-                .gap_2()
-                .child(Label::new(self.title(cx).to_string()))
-                .children(detail.and_then(|detail| {
-                    let path = path_for_buffer(&self.buffer, detail, false, cx)?;
-                    let description = path.to_string_lossy();
-
-                    Some(
-                        div().child(
-                            Label::new(util::truncate_and_trailoff(
-                                &description,
-                                MAX_TAB_TITLE_LEN,
-                            ))
-                            .color(Color::Muted),
-                        ),
-                    )
-                })),
-        )
+        let description = detail.and_then(|detail| {
+            let path = path_for_buffer(&self.buffer, detail, false, cx)?;
+            let description = path.to_string_lossy();
+            let description = description.trim();
+
+            if description.is_empty() {
+                return None;
+            }
+
+            Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN))
+        });
+
+        h_stack()
+            .gap_2()
+            .child(Label::new(self.title(cx).to_string()))
+            .when_some(description, |this, description| {
+                this.child(Label::new(description).color(Color::Muted))
+            })
+            .into_any_element()
     }
 
     fn for_each_project_item(

crates/quick_action_bar2/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "quick_action_bar2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/quick_action_bar.rs"
+doctest = false
+
+[dependencies]
+#assistant = { path = "../assistant" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+search = { package = "search2", path = "../search2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+ui = { package = "ui2", path = "../ui2" }
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

crates/quick_action_bar2/src/quick_action_bar.rs 🔗

@@ -0,0 +1,288 @@
+// use assistant::{assistant_panel::InlineAssist, AssistantPanel};
+use editor::Editor;
+
+use gpui::{
+    Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful,
+    Styled, Subscription, View, ViewContext, WeakView,
+};
+use search::BufferSearchBar;
+use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
+use workspace::{
+    item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+};
+
+pub struct QuickActionBar {
+    buffer_search_bar: View<BufferSearchBar>,
+    active_item: Option<Box<dyn ItemHandle>>,
+    _inlay_hints_enabled_subscription: Option<Subscription>,
+    #[allow(unused)]
+    workspace: WeakView<Workspace>,
+}
+
+impl QuickActionBar {
+    pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
+        Self {
+            buffer_search_bar,
+            active_item: None,
+            _inlay_hints_enabled_subscription: None,
+            workspace: workspace.weak_handle(),
+        }
+    }
+
+    #[allow(dead_code)]
+    fn active_editor(&self) -> Option<View<Editor>> {
+        self.active_item
+            .as_ref()
+            .and_then(|item| item.downcast::<Editor>())
+    }
+}
+
+impl Render for QuickActionBar {
+    type Element = Stateful<Div>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let search_button = QuickActionBarButton::new(
+            "toggle buffer search",
+            Icon::MagnifyingGlass,
+            !self.buffer_search_bar.read(cx).is_dismissed(),
+            Box::new(search::buffer_search::Deploy { focus: false }),
+            "Buffer Search",
+        );
+        let assistant_button = QuickActionBarButton::new(
+            "toggle inline assitant",
+            Icon::MagicWand,
+            false,
+            Box::new(gpui::NoAction),
+            "Inline assistant",
+        );
+        h_stack()
+            .id("quick action bar")
+            .p_1()
+            .gap_2()
+            .child(search_button)
+            .child(
+                div()
+                    .border()
+                    .border_color(gpui::red())
+                    .child(assistant_button),
+            )
+    }
+}
+
+impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
+
+// impl View for QuickActionBar {
+//     fn ui_name() -> &'static str {
+//         "QuickActionsBar"
+//     }
+
+//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+//         let Some(editor) = self.active_editor() else {
+//             return div();
+//         };
+
+//         let mut bar = Flex::row();
+//         if editor.read(cx).supports_inlay_hints(cx) {
+//             bar = bar.with_child(render_quick_action_bar_button(
+//                 0,
+//                 "icons/inlay_hint.svg",
+//                 editor.read(cx).inlay_hints_enabled(),
+//                 (
+//                     "Toggle Inlay Hints".to_string(),
+//                     Some(Box::new(editor::ToggleInlayHints)),
+//                 ),
+//                 cx,
+//                 |this, cx| {
+//                     if let Some(editor) = this.active_editor() {
+//                         editor.update(cx, |editor, cx| {
+//                             editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
+//                         });
+//                     }
+//                 },
+//             ));
+//         }
+
+//         if editor.read(cx).buffer().read(cx).is_singleton() {
+//             let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
+//             let search_action = buffer_search::Deploy { focus: true };
+
+//             bar = bar.with_child(render_quick_action_bar_button(
+//                 1,
+//                 "icons/magnifying_glass.svg",
+//                 search_bar_shown,
+//                 (
+//                     "Buffer Search".to_string(),
+//                     Some(Box::new(search_action.clone())),
+//                 ),
+//                 cx,
+//                 move |this, cx| {
+//                     this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
+//                         if search_bar_shown {
+//                             buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
+//                         } else {
+//                             buffer_search_bar.deploy(&search_action, cx);
+//                         }
+//                     });
+//                 },
+//             ));
+//         }
+
+//         bar.add_child(render_quick_action_bar_button(
+//             2,
+//             "icons/magic-wand.svg",
+//             false,
+//             ("Inline Assist".into(), Some(Box::new(InlineAssist))),
+//             cx,
+//             move |this, cx| {
+//                 if let Some(workspace) = this.workspace.upgrade(cx) {
+//                     workspace.update(cx, |workspace, cx| {
+//                         AssistantPanel::inline_assist(workspace, &Default::default(), cx);
+//                     });
+//                 }
+//             },
+//         ));
+
+//         bar.into_any()
+//     }
+// }
+
+#[derive(IntoElement)]
+struct QuickActionBarButton {
+    id: ElementId,
+    icon: Icon,
+    toggled: bool,
+    action: Box<dyn Action>,
+    tooltip: SharedString,
+    tooltip_meta: Option<SharedString>,
+}
+
+impl QuickActionBarButton {
+    fn new(
+        id: impl Into<ElementId>,
+        icon: Icon,
+        toggled: bool,
+        action: Box<dyn Action>,
+        tooltip: impl Into<SharedString>,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            icon,
+            toggled,
+            action,
+            tooltip: tooltip.into(),
+            tooltip_meta: None,
+        }
+    }
+
+    #[allow(dead_code)]
+    pub fn meta(mut self, meta: Option<impl Into<SharedString>>) -> Self {
+        self.tooltip_meta = meta.map(|meta| meta.into());
+        self
+    }
+}
+
+impl RenderOnce for QuickActionBarButton {
+    type Rendered = IconButton;
+
+    fn render(self, _: &mut WindowContext) -> Self::Rendered {
+        let tooltip = self.tooltip.clone();
+        let action = self.action.boxed_clone();
+        let tooltip_meta = self.tooltip_meta.clone();
+
+        IconButton::new(self.id.clone(), self.icon)
+            .size(ButtonSize::Compact)
+            .icon_size(IconSize::Small)
+            .style(ButtonStyle::Subtle)
+            .selected(self.toggled)
+            .tooltip(move |cx| {
+                if let Some(meta) = &tooltip_meta {
+                    Tooltip::with_meta(tooltip.clone(), Some(&*action), meta.clone(), cx)
+                } else {
+                    Tooltip::for_action(tooltip.clone(), &*action, cx)
+                }
+            })
+            .on_click({
+                let action = self.action.boxed_clone();
+                move |_, cx| cx.dispatch_action(action.boxed_clone())
+            })
+    }
+}
+
+// fn render_quick_action_bar_button<
+//     F: 'static + Fn(&mut QuickActionBar, &mut ViewContext<QuickActionBar>),
+// >(
+//     index: usize,
+//     icon: &'static str,
+//     toggled: bool,
+//     tooltip: (String, Option<Box<dyn Action>>),
+//     cx: &mut ViewContext<QuickActionBar>,
+//     on_click: F,
+// ) -> AnyElement<QuickActionBar> {
+//     enum QuickActionBarButton {}
+
+//     let theme = theme::current(cx);
+//     let (tooltip_text, action) = tooltip;
+
+//     MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
+//         let style = theme
+//             .workspace
+//             .toolbar
+//             .toggleable_tool
+//             .in_state(toggled)
+//             .style_for(mouse_state);
+//         Svg::new(icon)
+//             .with_color(style.color)
+//             .constrained()
+//             .with_width(style.icon_width)
+//             .aligned()
+//             .constrained()
+//             .with_width(style.button_width)
+//             .with_height(style.button_width)
+//             .contained()
+//             .with_style(style.container)
+//     })
+//     .with_cursor_style(CursorStyle::PointingHand)
+//     .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
+//     .with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
+//     .into_any_named("quick action bar button")
+// }
+
+impl ToolbarItemView for QuickActionBar {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        match active_pane_item {
+            Some(active_item) => {
+                self.active_item = Some(active_item.boxed_clone());
+                self._inlay_hints_enabled_subscription.take();
+
+                if let Some(editor) = active_item.downcast::<Editor>() {
+                    let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                    let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
+                    self._inlay_hints_enabled_subscription =
+                        Some(cx.observe(&editor, move |_, editor, cx| {
+                            let editor = editor.read(cx);
+                            let new_inlay_hints_enabled = editor.inlay_hints_enabled();
+                            let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
+                            let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
+                                || supports_inlay_hints != new_supports_inlay_hints;
+                            inlay_hints_enabled = new_inlay_hints_enabled;
+                            supports_inlay_hints = new_supports_inlay_hints;
+                            if should_notify {
+                                cx.notify()
+                            }
+                        }));
+                    ToolbarItemLocation::PrimaryRight
+                } else {
+                    ToolbarItemLocation::Hidden
+                }
+            }
+            None => {
+                self.active_item = None;
+                ToolbarItemLocation::Hidden
+            }
+        }
+    }
+}

crates/theme2/src/default_colors.rs 🔗

@@ -5,7 +5,7 @@ use crate::ColorScale;
 use crate::{SystemColors, ThemeColors};
 
 pub(crate) fn neutral() -> ColorScaleSet {
-    slate()
+    sand()
 }
 
 impl ThemeColors {
@@ -29,12 +29,12 @@ impl ThemeColors {
             element_disabled: neutral().light_alpha().step_3(),
             drop_target_background: blue().light_alpha().step_2(),
             ghost_element_background: system.transparent,
-            ghost_element_hover: neutral().light_alpha().step_4(),
-            ghost_element_active: neutral().light_alpha().step_5(),
+            ghost_element_hover: neutral().light_alpha().step_3(),
+            ghost_element_active: neutral().light_alpha().step_4(),
             ghost_element_selected: neutral().light_alpha().step_5(),
             ghost_element_disabled: neutral().light_alpha().step_3(),
-            text: yellow().light().step_9(),
-            text_muted: neutral().light().step_11(),
+            text: neutral().light().step_12(),
+            text_muted: neutral().light().step_10(),
             text_placeholder: neutral().light().step_10(),
             text_disabled: neutral().light().step_9(),
             text_accent: blue().light().step_11(),
@@ -53,13 +53,13 @@ impl ThemeColors {
             editor_gutter_background: neutral().light().step_1(), // todo!("pick the right colors")
             editor_subheader_background: neutral().light().step_2(),
             editor_active_line_background: neutral().light_alpha().step_3(),
-            editor_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors")
-            editor_active_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors")
-            editor_highlighted_line_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
-            editor_invisible: neutral().light_alpha().step_4(), // todo!("pick the right colors")
-            editor_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors")
-            editor_active_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors")
-            editor_document_highlight_read_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
+            editor_line_number: neutral().light().step_10(),
+            editor_active_line_number: neutral().light().step_11(),
+            editor_highlighted_line_background: neutral().light_alpha().step_3(),
+            editor_invisible: neutral().light().step_10(),
+            editor_wrap_guide: neutral().light_alpha().step_7(),
+            editor_active_wrap_guide: neutral().light_alpha().step_8(), // todo!("pick the right colors")
+            editor_document_highlight_read_background: neutral().light_alpha().step_3(), // todo!("pick the right colors")
             editor_document_highlight_write_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
             terminal_background: neutral().light().step_1(),
             terminal_ansi_black: black().light().step_12(),

crates/theme2/src/default_theme.rs 🔗

@@ -1,47 +1,51 @@
+use std::sync::Arc;
+
 use crate::{
+    default_color_scales,
     one_themes::{one_dark, one_family},
-    Theme, ThemeFamily,
+    Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors,
+    ThemeFamily, ThemeStyles,
 };
 
-// fn zed_pro_daylight() -> Theme {
-//     Theme {
-//         id: "zed_pro_daylight".to_string(),
-//         name: "Zed Pro Daylight".into(),
-//         appearance: Appearance::Light,
-//         styles: ThemeStyles {
-//             system: SystemColors::default(),
-//             colors: ThemeColors::light(),
-//             status: StatusColors::light(),
-//             player: PlayerColors::light(),
-//             syntax: Arc::new(SyntaxTheme::light()),
-//         },
-//     }
-// }
+fn zed_pro_daylight() -> Theme {
+    Theme {
+        id: "zed_pro_daylight".to_string(),
+        name: "Zed Pro Daylight".into(),
+        appearance: Appearance::Light,
+        styles: ThemeStyles {
+            system: SystemColors::default(),
+            colors: ThemeColors::light(),
+            status: StatusColors::light(),
+            player: PlayerColors::light(),
+            syntax: Arc::new(SyntaxTheme::light()),
+        },
+    }
+}
 
-// pub(crate) fn zed_pro_moonlight() -> Theme {
-//     Theme {
-//         id: "zed_pro_moonlight".to_string(),
-//         name: "Zed Pro Moonlight".into(),
-//         appearance: Appearance::Dark,
-//         styles: ThemeStyles {
-//             system: SystemColors::default(),
-//             colors: ThemeColors::dark(),
-//             status: StatusColors::dark(),
-//             player: PlayerColors::dark(),
-//             syntax: Arc::new(SyntaxTheme::dark()),
-//         },
-//     }
-// }
+pub(crate) fn zed_pro_moonlight() -> Theme {
+    Theme {
+        id: "zed_pro_moonlight".to_string(),
+        name: "Zed Pro Moonlight".into(),
+        appearance: Appearance::Dark,
+        styles: ThemeStyles {
+            system: SystemColors::default(),
+            colors: ThemeColors::dark(),
+            status: StatusColors::dark(),
+            player: PlayerColors::dark(),
+            syntax: Arc::new(SyntaxTheme::dark()),
+        },
+    }
+}
 
-// pub fn zed_pro_family() -> ThemeFamily {
-//     ThemeFamily {
-//         id: "zed_pro".to_string(),
-//         name: "Zed Pro".into(),
-//         author: "Zed Team".into(),
-//         themes: vec![zed_pro_daylight(), zed_pro_moonlight()],
-//         scales: default_color_scales(),
-//     }
-// }
+pub fn zed_pro_family() -> ThemeFamily {
+    ThemeFamily {
+        id: "zed_pro".to_string(),
+        name: "Zed Pro".into(),
+        author: "Zed Team".into(),
+        themes: vec![zed_pro_daylight(), zed_pro_moonlight()],
+        scales: default_color_scales(),
+    }
+}
 
 impl Default for ThemeFamily {
     fn default() -> Self {

crates/theme2/src/registry.rs 🔗

@@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString};
 use refineable::Refineable;
 
 use crate::{
-    one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors,
-    Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
+    one_themes::one_family, zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme,
+    SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
 };
 
 pub struct ThemeRegistry {
@@ -117,7 +117,7 @@ impl Default for ThemeRegistry {
             themes: HashMap::default(),
         };
 
-        this.insert_theme_families([one_family()]);
+        this.insert_theme_families([zed_pro_family(), one_family()]);
 
         this
     }

crates/theme2/src/styles/syntax.rs 🔗

@@ -22,8 +22,8 @@ impl SyntaxTheme {
             highlights: vec![
                 ("attribute".into(), cyan().light().step_11().into()),
                 ("boolean".into(), tomato().light().step_11().into()),
-                ("comment".into(), neutral().light().step_11().into()),
-                ("comment.doc".into(), iris().light().step_12().into()),
+                ("comment".into(), neutral().light().step_10().into()),
+                ("comment.doc".into(), iris().light().step_11().into()),
                 ("constant".into(), red().light().step_9().into()),
                 ("constructor".into(), red().light().step_9().into()),
                 ("embedded".into(), red().light().step_9().into()),
@@ -32,11 +32,11 @@ impl SyntaxTheme {
                 ("enum".into(), red().light().step_9().into()),
                 ("function".into(), red().light().step_9().into()),
                 ("hint".into(), red().light().step_9().into()),
-                ("keyword".into(), orange().light().step_11().into()),
+                ("keyword".into(), orange().light().step_9().into()),
                 ("label".into(), red().light().step_9().into()),
                 ("link_text".into(), red().light().step_9().into()),
                 ("link_uri".into(), red().light().step_9().into()),
-                ("number".into(), red().light().step_9().into()),
+                ("number".into(), purple().light().step_10().into()),
                 ("operator".into(), red().light().step_9().into()),
                 ("predictive".into(), red().light().step_9().into()),
                 ("preproc".into(), red().light().step_9().into()),
@@ -49,16 +49,16 @@ impl SyntaxTheme {
                 ),
                 (
                     "punctuation.delimiter".into(),
-                    neutral().light().step_11().into(),
+                    neutral().light().step_10().into(),
                 ),
                 (
                     "punctuation.list_marker".into(),
                     blue().light().step_11().into(),
                 ),
                 ("punctuation.special".into(), red().light().step_9().into()),
-                ("string".into(), jade().light().step_11().into()),
+                ("string".into(), jade().light().step_9().into()),
                 ("string.escape".into(), red().light().step_9().into()),
-                ("string.regex".into(), tomato().light().step_11().into()),
+                ("string.regex".into(), tomato().light().step_9().into()),
                 ("string.special".into(), red().light().step_9().into()),
                 (
                     "string.special.symbol".into(),
@@ -67,7 +67,7 @@ impl SyntaxTheme {
                 ("tag".into(), red().light().step_9().into()),
                 ("text.literal".into(), red().light().step_9().into()),
                 ("title".into(), red().light().step_9().into()),
-                ("type".into(), red().light().step_9().into()),
+                ("type".into(), cyan().light().step_9().into()),
                 ("variable".into(), red().light().step_9().into()),
                 ("variable.special".into(), red().light().step_9().into()),
                 ("variant".into(), red().light().step_9().into()),

crates/ui2/src/components.rs 🔗

@@ -5,6 +5,7 @@ mod context_menu;
 mod disclosure;
 mod divider;
 mod icon;
+mod indicator;
 mod keybinding;
 mod label;
 mod list;
@@ -24,6 +25,7 @@ pub use context_menu::*;
 pub use disclosure::*;
 pub use divider::*;
 pub use icon::*;
+pub use indicator::*;
 pub use keybinding::*;
 pub use label::*;
 pub use list::*;

crates/ui2/src/components/icon.rs 🔗

@@ -1,15 +1,26 @@
-use gpui::{rems, svg, IntoElement, Svg};
+use gpui::{rems, svg, IntoElement, Rems, Svg};
 use strum::EnumIter;
 
 use crate::prelude::*;
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconSize {
+    XSmall,
     Small,
     #[default]
     Medium,
 }
 
+impl IconSize {
+    pub fn rems(self) -> Rems {
+        match self {
+            IconSize::XSmall => rems(12. / 16.),
+            IconSize::Small => rems(14. / 16.),
+            IconSize::Medium => rems(16. / 16.),
+        }
+    }
+}
+
 #[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
 pub enum Icon {
     Ai,
@@ -170,13 +181,8 @@ impl RenderOnce for IconElement {
     type Rendered = Svg;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let svg_size = match self.size {
-            IconSize::Small => rems(14. / 16.),
-            IconSize::Medium => rems(16. / 16.),
-        };
-
         svg()
-            .size(svg_size)
+            .size(self.size.rems())
             .flex_none()
             .path(self.path)
             .text_color(self.color.color(cx))

crates/ui2/src/components/indicator.rs 🔗

@@ -0,0 +1,60 @@
+use gpui::{Div, Position};
+
+use crate::prelude::*;
+
+#[derive(Default)]
+pub enum IndicatorStyle {
+    #[default]
+    Dot,
+    Bar,
+}
+
+#[derive(IntoElement)]
+pub struct Indicator {
+    position: Position,
+    style: IndicatorStyle,
+    color: Color,
+}
+
+impl Indicator {
+    pub fn dot() -> Self {
+        Self {
+            position: Position::Relative,
+            style: IndicatorStyle::Dot,
+            color: Color::Default,
+        }
+    }
+
+    pub fn bar() -> Self {
+        Self {
+            position: Position::Relative,
+            style: IndicatorStyle::Dot,
+            color: Color::Default,
+        }
+    }
+
+    pub fn color(mut self, color: Color) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn absolute(mut self) -> Self {
+        self.position = Position::Absolute;
+        self
+    }
+}
+
+impl RenderOnce for Indicator {
+    type Rendered = Div;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .flex_none()
+            .map(|this| match self.style {
+                IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(),
+                IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(),
+            })
+            .when(self.position == Position::Absolute, |this| this.absolute())
+            .bg(self.color.color(cx))
+    }
+}

crates/ui2/src/prelude.rs 🔗

@@ -8,5 +8,6 @@ pub use crate::clickable::*;
 pub use crate::disableable::*;
 pub use crate::fixed::*;
 pub use crate::selectable::*;
+pub use crate::{h_stack, v_stack};
 pub use crate::{ButtonCommon, Color, StyledExt};
 pub use theme::ActiveTheme;

crates/workspace2/src/pane.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
+    item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
     toolbar::Toolbar,
     workspace_settings::{AutosaveSetting, WorkspaceSettings},
     NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
@@ -27,7 +27,8 @@ use std::{
 };
 
 use ui::{
-    h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip,
+    h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize,
+    Indicator, Label, Tooltip,
 };
 use ui::{v_stack, ContextMenu};
 use util::truncate_and_remove_front;
@@ -1418,22 +1419,7 @@ impl Pane {
         cx: &mut ViewContext<'_, Pane>,
     ) -> impl IntoElement {
         let label = item.tab_content(Some(detail), cx);
-        let close_icon = || {
-            let id = item.item_id();
-
-            div()
-                .id(ix)
-                .invisible()
-                .group_hover("", |style| style.visible())
-                .child(
-                    IconButton::new("close_tab", Icon::Close).on_click(cx.listener(
-                        move |pane, _, cx| {
-                            pane.close_item_by_id(id, SaveIntent::Close, cx)
-                                .detach_and_log_err(cx);
-                        },
-                    )),
-                )
-        };
+        let close_side = &ItemSettings::get_global(cx).close_position;
 
         let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
             false => (
@@ -1450,102 +1436,129 @@ impl Pane {
             ),
         };
 
-        let close_right = ItemSettings::get_global(cx).close_position.right();
         let is_active = ix == self.active_item_index;
 
+        let indicator = {
+            let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
+                (true, _) => Some(Color::Warning),
+                (_, true) => Some(Color::Accent),
+                (false, false) => None,
+            };
+
+            h_stack()
+                .w_3()
+                .h_3()
+                .justify_center()
+                .absolute()
+                .map(|this| match close_side {
+                    ClosePosition::Left => this.right_1(),
+                    ClosePosition::Right => this.left_1(),
+                })
+                .when_some(indicator_color, |this, indicator_color| {
+                    this.child(Indicator::dot().color(indicator_color))
+                })
+        };
+
+        let close_button = {
+            let id = item.item_id();
+
+            h_stack()
+                .invisible()
+                .w_3()
+                .h_3()
+                .justify_center()
+                .absolute()
+                .map(|this| match close_side {
+                    ClosePosition::Left => this.left_1(),
+                    ClosePosition::Right => this.right_1(),
+                })
+                .group_hover("", |style| style.visible())
+                .child(
+                    // TODO: Fix button size
+                    IconButton::new("close tab", Icon::Close)
+                        .icon_color(Color::Muted)
+                        .size(ButtonSize::None)
+                        .icon_size(IconSize::XSmall)
+                        .on_click(cx.listener(move |pane, _, cx| {
+                            pane.close_item_by_id(id, SaveIntent::Close, cx)
+                                .detach_and_log_err(cx);
+                        })),
+                )
+        };
+
         let tab = div()
-            .group("")
-            .id(ix)
-            .cursor_pointer()
-            .when_some(item.tab_tooltip_text(cx), |div, text| {
-                div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
-            })
-            .on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)))
-            // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
-            // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
-            // .on_drop(|_view, state: View<DraggedTab>, cx| {
-            //     eprintln!("{:?}", state.read(cx));
-            // })
-            .flex()
-            .items_center()
-            .justify_center()
-            // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize")
-            .map(|this| {
-                if close_right {
-                    this.pl_3().pr_1()
-                } else {
-                    this.pr_1().pr_3()
-                }
-            })
-            .py_1()
-            .bg(tab_bg)
             .border_color(cx.theme().colors().border)
-            .text_color(if is_active {
-                cx.theme().colors().text
-            } else {
-                cx.theme().colors().text_muted
-            })
+            .bg(tab_bg)
+            // 30px @ 16px/rem
+            .h(rems(1.875))
             .map(|this| {
+                let is_first_item = ix == 0;
                 let is_last_item = ix == self.items.len() - 1;
                 match ix.cmp(&self.active_item_index) {
-                    cmp::Ordering::Less => this.border_l().mr_px(),
+                    cmp::Ordering::Less => {
+                        if is_first_item {
+                            this.pl_px().pr_px().border_b()
+                        } else {
+                            this.border_l().pr_px().border_b()
+                        }
+                    }
                     cmp::Ordering::Greater => {
                         if is_last_item {
-                            this.mr_px().ml_px()
+                            this.pr_px().pl_px().border_b()
                         } else {
-                            this.border_r().ml_px()
+                            this.border_r().pl_px().border_b()
+                        }
+                    }
+                    cmp::Ordering::Equal => {
+                        if is_first_item {
+                            this.pl_px().border_r().pb_px()
+                        } else {
+                            this.border_l().border_r().pb_px()
                         }
                     }
-                    cmp::Ordering::Equal => this.border_l().border_r(),
                 }
             })
-            // .hover(|h| h.bg(tab_hover_bg))
-            // .active(|a| a.bg(tab_active_bg))
             .child(
-                div()
-                    .flex()
-                    .items_center()
+                h_stack()
+                    .group("")
+                    .id(ix)
+                    .relative()
+                    .h_full()
+                    .cursor_pointer()
+                    .when_some(item.tab_tooltip_text(cx), |div, text| {
+                        div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
+                    })
+                    .on_click(
+                        cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)),
+                    )
+                    // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
+                    // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
+                    // .on_drop(|_view, state: View<DraggedTab>, cx| {
+                    //     eprintln!("{:?}", state.read(cx));
+                    // })
+                    .px_5()
+                    // .hover(|h| h.bg(tab_hover_bg))
+                    // .active(|a| a.bg(tab_active_bg))
                     .gap_1()
                     .text_color(text_color)
-                    .children(
-                        item.has_conflict(cx)
-                            .then(|| {
-                                div().border().border_color(gpui::red()).child(
-                                    IconElement::new(Icon::ExclamationTriangle)
-                                        .size(ui::IconSize::Small)
-                                        .color(Color::Warning),
-                                )
-                            })
-                            .or(item.is_dirty(cx).then(|| {
-                                div().border().border_color(gpui::red()).child(
-                                    IconElement::new(Icon::ExclamationTriangle)
-                                        .size(ui::IconSize::Small)
-                                        .color(Color::Info),
-                                )
-                            })),
-                    )
-                    .children((!close_right).then(|| close_icon()))
-                    .child(label)
-                    .children(close_right.then(|| close_icon())),
+                    .child(indicator)
+                    .child(close_button)
+                    .child(label),
             );
 
         right_click_menu(ix).trigger(tab).menu(|cx| {
             ContextMenu::build(cx, |menu, cx| {
-                menu.action(
-                    "Close Active Item",
-                    CloseActiveItem { save_intent: None }.boxed_clone(),
-                )
-                .action("Close Inactive Items", CloseInactiveItems.boxed_clone())
-                .action("Close Clean Items", CloseCleanItems.boxed_clone())
-                .action("Close Items To The Left", CloseItemsToTheLeft.boxed_clone())
-                .action(
-                    "Close Items To The Right",
-                    CloseItemsToTheRight.boxed_clone(),
-                )
-                .action(
-                    "Close All Items",
-                    CloseAllItems { save_intent: None }.boxed_clone(),
-                )
+                menu.action("Close", CloseActiveItem { save_intent: None }.boxed_clone())
+                    .action("Close Others", CloseInactiveItems.boxed_clone())
+                    .separator()
+                    .action("Close Left", CloseItemsToTheLeft.boxed_clone())
+                    .action("Close Right", CloseItemsToTheRight.boxed_clone())
+                    .separator()
+                    .action("Close Clean", CloseCleanItems.boxed_clone())
+                    .action(
+                        "Close All",
+                        CloseAllItems { save_intent: None }.boxed_clone(),
+                    )
             })
         })
     }
@@ -1565,116 +1578,118 @@ impl Pane {
             // Left Side
             .child(
                 h_stack()
-                    .px_2()
                     .flex()
                     .flex_none()
                     .gap_1()
+                    .px_1()
+                    .border_b()
+                    .border_r()
+                    .border_color(cx.theme().colors().border)
                     // Nav Buttons
                     .child(
-                        div().border().border_color(gpui::red()).child(
-                            IconButton::new("navigate_backward", Icon::ArrowLeft)
-                                .on_click({
-                                    let view = cx.view().clone();
-                                    move |_, cx| view.update(cx, Self::navigate_backward)
-                                })
-                                .disabled(!self.can_navigate_backward()),
-                        ),
+                        IconButton::new("navigate_backward", Icon::ArrowLeft)
+                            .icon_size(IconSize::Small)
+                            .on_click({
+                                let view = cx.view().clone();
+                                move |_, cx| view.update(cx, Self::navigate_backward)
+                            })
+                            .disabled(!self.can_navigate_backward()),
                     )
                     .child(
-                        div().border().border_color(gpui::red()).child(
-                            IconButton::new("navigate_forward", Icon::ArrowRight)
-                                .on_click({
-                                    let view = cx.view().clone();
-                                    move |_, cx| view.update(cx, Self::navigate_backward)
-                                })
-                                .disabled(!self.can_navigate_forward()),
-                        ),
+                        IconButton::new("navigate_forward", Icon::ArrowRight)
+                            .icon_size(IconSize::Small)
+                            .on_click({
+                                let view = cx.view().clone();
+                                move |_, cx| view.update(cx, Self::navigate_backward)
+                            })
+                            .disabled(!self.can_navigate_forward()),
                     ),
             )
             .child(
-                div().flex_1().h_full().child(
-                    div().id("tabs").flex().overflow_x_scroll().children(
-                        self.items
-                            .iter()
-                            .enumerate()
-                            .zip(self.tab_details(cx))
-                            .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
+                div()
+                    .relative()
+                    .flex_1()
+                    .h_full()
+                    .overflow_hidden_x()
+                    .child(
+                        div()
+                            .absolute()
+                            .top_0()
+                            .left_0()
+                            .z_index(1)
+                            .size_full()
+                            .border_b()
+                            .border_color(cx.theme().colors().border),
+                    )
+                    .child(
+                        h_stack().id("tabs").z_index(2).children(
+                            self.items
+                                .iter()
+                                .enumerate()
+                                .zip(self.tab_details(cx))
+                                .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
+                        ),
                     ),
-                ),
             )
             // Right Side
             .child(
-                div()
-                    .px_1()
+                h_stack()
                     .flex()
                     .flex_none()
-                    .gap_2()
-                    // Nav Buttons
+                    .gap_1()
+                    .px_1()
+                    .border_b()
+                    .border_l()
+                    .border_color(cx.theme().colors().border)
                     .child(
                         div()
                             .flex()
                             .items_center()
                             .gap_px()
                             .child(
-                                div()
-                                    .bg(gpui::blue())
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("plus", Icon::Plus).on_click(
-                                        cx.listener(|this, _, cx| {
-                                            let menu = ContextMenu::build(cx, |menu, cx| {
-                                                menu.action("New File", NewFile.boxed_clone())
-                                                    .action(
-                                                        "New Terminal",
-                                                        NewCenterTerminal.boxed_clone(),
-                                                    )
-                                                    .action("New Search", NewSearch.boxed_clone())
-                                            });
-                                            cx.subscribe(
-                                                &menu,
-                                                |this, _, event: &DismissEvent, cx| {
-                                                    this.focus(cx);
-                                                    this.new_item_menu = None;
-                                                },
-                                            )
-                                            .detach();
-                                            this.new_item_menu = Some(menu);
-                                        }),
-                                    ))
-                                    .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
-                                        el.child(Self::render_menu_overlay(new_item_menu))
-                                    }),
+                                IconButton::new("plus", Icon::Plus)
+                                    .icon_size(IconSize::Small)
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        let menu = ContextMenu::build(cx, |menu, cx| {
+                                            menu.action("New File", NewFile.boxed_clone())
+                                                .action(
+                                                    "New Terminal",
+                                                    NewCenterTerminal.boxed_clone(),
+                                                )
+                                                .action("New Search", NewSearch.boxed_clone())
+                                        });
+                                        cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
+                                            this.focus(cx);
+                                            this.new_item_menu = None;
+                                        })
+                                        .detach();
+                                        this.new_item_menu = Some(menu);
+                                    })),
                             )
+                            .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
+                                el.child(Self::render_menu_overlay(new_item_menu))
+                            })
                             .child(
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("split", Icon::Split).on_click(
-                                        cx.listener(|this, _, cx| {
-                                            let menu = ContextMenu::build(cx, |menu, cx| {
-                                                menu.action("Split Right", SplitRight.boxed_clone())
-                                                    .action("Split Left", SplitLeft.boxed_clone())
-                                                    .action("Split Up", SplitUp.boxed_clone())
-                                                    .action("Split Down", SplitDown.boxed_clone())
-                                            });
-                                            cx.subscribe(
-                                                &menu,
-                                                |this, _, event: &DismissEvent, cx| {
-                                                    this.focus(cx);
-                                                    this.split_item_menu = None;
-                                                },
-                                            )
-                                            .detach();
-                                            this.split_item_menu = Some(menu);
-                                        }),
-                                    ))
-                                    .when_some(
-                                        self.split_item_menu.as_ref(),
-                                        |el, split_item_menu| {
-                                            el.child(Self::render_menu_overlay(split_item_menu))
-                                        },
-                                    ),
-                            ),
+                                IconButton::new("split", Icon::Split)
+                                    .icon_size(IconSize::Small)
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        let menu = ContextMenu::build(cx, |menu, cx| {
+                                            menu.action("Split Right", SplitRight.boxed_clone())
+                                                .action("Split Left", SplitLeft.boxed_clone())
+                                                .action("Split Up", SplitUp.boxed_clone())
+                                                .action("Split Down", SplitDown.boxed_clone())
+                                        });
+                                        cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
+                                            this.focus(cx);
+                                            this.split_item_menu = None;
+                                        })
+                                        .detach();
+                                        this.split_item_menu = Some(menu);
+                                    })),
+                            )
+                            .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| {
+                                el.child(Self::render_menu_overlay(split_item_menu))
+                            }),
                     ),
             )
     }
@@ -2092,6 +2107,8 @@ impl Render for Pane {
         v_stack()
             .key_context("Pane")
             .track_focus(&self.focus_handle)
+            .size_full()
+            .overflow_hidden()
             .on_focus_in({
                 let this = this.clone();
                 move |event, cx| {
@@ -2159,7 +2176,6 @@ impl Render for Pane {
                 pane.close_all_items(action, cx)
                     .map(|task| task.detach_and_log_err(cx));
             }))
-            .size_full()
             .on_action(
                 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
                     pane.close_active_item(action, cx)

crates/workspace2/src/toolbar.rs 🔗

@@ -1,10 +1,10 @@
 use crate::ItemHandle;
 use gpui::{
-    div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
+    AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
     ViewContext, WindowContext,
 };
 use ui::prelude::*;
-use ui::{h_stack, v_stack, Icon, IconButton};
+use ui::{h_stack, v_stack};
 
 pub enum ToolbarItemEvent {
     ChangeLocation(ToolbarItemLocation),
@@ -87,25 +87,7 @@ impl Render for Toolbar {
             .child(
                 h_stack()
                     .justify_between()
-                    // Toolbar left side
-                    .children(self.items.iter().map(|(child, _)| child.to_any()))
-                    // Toolbar right side
-                    .child(
-                        h_stack()
-                            .p_1()
-                            .child(
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("buffer-search", Icon::MagnifyingGlass)),
-                            )
-                            .child(
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("inline-assist", Icon::MagicWand)),
-                            ),
-                    ),
+                    .children(self.items.iter().map(|(child, _)| child.to_any())),
             )
     }
 }

crates/zed2/Cargo.toml 🔗

@@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" }
 project = { package = "project2", path = "../project2" }
 project_panel = { package = "project_panel2", path = "../project_panel2" }
 # project_symbols = { path = "../project_symbols" }
-# quick_action_bar = { path = "../quick_action_bar" }
+quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
 # recent_projects = { path = "../recent_projects" }
 rope = { package = "rope2", path = "../rope2"}
 rpc = { package = "rpc2", path = "../rpc2" }

crates/zed2/src/zed2.rs 🔗

@@ -19,6 +19,7 @@ pub use open_listener::*;
 
 use anyhow::{anyhow, Context as _};
 use project_panel::ProjectPanel;
+use quick_action_bar::QuickActionBar;
 use settings::{initial_local_settings_content, Settings};
 use std::{borrow::Cow, ops::Deref, sync::Arc};
 use terminal_view::terminal_panel::TerminalPanel;
@@ -100,11 +101,10 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                             toolbar.add_item(breadcrumbs, cx);
                             let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
                             toolbar.add_item(buffer_search_bar.clone(), cx);
-                            // todo!()
-                            //     let quick_action_bar = cx.add_view(|_| {
-                            //         QuickActionBar::new(buffer_search_bar, workspace)
-                            //     });
-                            //     toolbar.add_item(quick_action_bar, cx);
+
+                            let quick_action_bar = cx
+                                .build_view(|_| QuickActionBar::new(buffer_search_bar, workspace));
+                            toolbar.add_item(quick_action_bar, cx);
                             let diagnostic_editor_controls =
                                 cx.build_view(|_| diagnostics::ToolbarControls::new());
                             //     toolbar.add_item(diagnostic_editor_controls, cx);