Restructure `Pane` to have a single `Toolbar` with multiple items

Antonio Scandurra created

Change summary

crates/search/src/buffer_search.rs  | 263 +++++++++++++++---------------
crates/search/src/project_search.rs |   9 
crates/search/src/search.rs         |   6 
crates/theme/src/theme.rs           |  13 
crates/workspace/src/pane.rs        | 135 ++-------------
crates/workspace/src/toolbar.rs     | 131 +++++++++++++++
crates/workspace/src/workspace.rs   |   7 
crates/zed/assets/themes/_base.toml |   6 
crates/zed/src/zed.rs               |  12 +
9 files changed, 318 insertions(+), 264 deletions(-)

Detailed changes

crates/search/src/buffer_search.rs 🔗

@@ -8,7 +8,7 @@ use gpui::{
 use language::OffsetRangeExt;
 use project::search::SearchQuery;
 use std::ops::Range;
-use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace};
+use workspace::{ItemHandle, Pane, Settings, ToolbarItemView};
 
 action!(Deploy, bool);
 action!(Dismiss);
@@ -38,7 +38,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(SearchBar::select_match_on_pane);
 }
 
-struct SearchBar {
+pub struct SearchBar {
     query_editor: ViewHandle<Editor>,
     active_editor: Option<ViewHandle<Editor>>,
     active_match_index: Option<usize>,
@@ -66,69 +66,69 @@ impl View for SearchBar {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.global::<Settings>().theme.clone();
-        let editor_container = if self.query_contains_error {
-            theme.search.invalid_editor
+        if self.dismissed {
+            Empty::new().boxed()
         } else {
-            theme.search.editor.input.container
-        };
-        Flex::row()
-            .with_child(
-                ChildView::new(&self.query_editor)
-                    .contained()
-                    .with_style(editor_container)
-                    .aligned()
-                    .constrained()
-                    .with_max_width(theme.search.editor.max_width)
-                    .boxed(),
-            )
-            .with_child(
-                Flex::row()
-                    .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx))
-                    .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
-                    .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
-                    .contained()
-                    .with_style(theme.search.option_button_group)
-                    .aligned()
-                    .boxed(),
-            )
-            .with_child(
-                Flex::row()
-                    .with_child(self.render_nav_button("<", Direction::Prev, cx))
-                    .with_child(self.render_nav_button(">", Direction::Next, cx))
-                    .aligned()
-                    .boxed(),
-            )
-            .with_children(self.active_editor.as_ref().and_then(|editor| {
-                let matches = self.editors_with_matches.get(&editor.downgrade())?;
-                let message = if let Some(match_ix) = self.active_match_index {
-                    format!("{}/{}", match_ix + 1, matches.len())
-                } else {
-                    "No matches".to_string()
-                };
-
-                Some(
-                    Label::new(message, theme.search.match_index.text.clone())
+            let theme = cx.global::<Settings>().theme.clone();
+            let editor_container = if self.query_contains_error {
+                theme.search.invalid_editor
+            } else {
+                theme.search.editor.container
+            };
+            Flex::row()
+                .with_child(
+                    ChildView::new(&self.query_editor)
                         .contained()
-                        .with_style(theme.search.match_index.container)
+                        .with_style(editor_container)
                         .aligned()
+                        .constrained()
+                        .with_max_width(theme.search.max_editor_width)
                         .boxed(),
                 )
-            }))
-            .contained()
-            .with_style(theme.search.container)
-            .constrained()
-            .with_height(theme.workspace.toolbar.height)
-            .named("search bar")
+                .with_child(
+                    Flex::row()
+                        .with_child(self.render_search_option(
+                            "Case",
+                            SearchOption::CaseSensitive,
+                            cx,
+                        ))
+                        .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
+                        .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
+                        .contained()
+                        .with_style(theme.search.option_button_group)
+                        .aligned()
+                        .boxed(),
+                )
+                .with_child(
+                    Flex::row()
+                        .with_child(self.render_nav_button("<", Direction::Prev, cx))
+                        .with_child(self.render_nav_button(">", Direction::Next, cx))
+                        .aligned()
+                        .boxed(),
+                )
+                .with_children(self.active_editor.as_ref().and_then(|editor| {
+                    let matches = self.editors_with_matches.get(&editor.downgrade())?;
+                    let message = if let Some(match_ix) = self.active_match_index {
+                        format!("{}/{}", match_ix + 1, matches.len())
+                    } else {
+                        "No matches".to_string()
+                    };
+
+                    Some(
+                        Label::new(message, theme.search.match_index.text.clone())
+                            .contained()
+                            .with_style(theme.search.match_index.container)
+                            .aligned()
+                            .boxed(),
+                    )
+                }))
+                .named("search bar")
+        }
     }
 }
 
-impl Toolbar for SearchBar {
-    fn active_item_changed(
-        &mut self,
-        item: Option<Box<dyn ItemHandle>>,
-        cx: &mut ViewContext<Self>,
-    ) -> bool {
+impl ToolbarItemView for SearchBar {
+    fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
         self.active_editor_subscription.take();
         self.active_editor.take();
         self.pending_search.take();
@@ -139,29 +139,19 @@ impl Toolbar for SearchBar {
                     Some(cx.subscribe(&editor, Self::on_active_editor_event));
                 self.active_editor = Some(editor);
                 self.update_matches(false, cx);
-                return true;
+                self.dismissed = false;
+                return;
             }
         }
-        false
-    }
 
-    fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
-        self.dismissed = true;
-        for (editor, _) in &self.editors_with_matches {
-            if let Some(editor) = editor.upgrade(cx) {
-                editor.update(cx, |editor, cx| {
-                    editor.clear_background_highlights::<Self>(cx)
-                });
-            }
-        }
+        self.dismiss(&Dismiss, cx);
     }
 }
 
 impl SearchBar {
-    fn new(cx: &mut ViewContext<Self>) -> Self {
-        let query_editor = cx.add_view(|cx| {
-            Editor::auto_height(2, Some(|theme| theme.search.editor.input.clone()), cx)
-        });
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        let query_editor =
+            cx.add_view(|cx| Editor::auto_height(2, Some(|theme| theme.search.editor.clone()), cx));
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();
 
@@ -176,10 +166,73 @@ impl SearchBar {
             regex: false,
             pending_search: None,
             query_contains_error: false,
-            dismissed: false,
+            dismissed: true,
         }
     }
 
+    fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
+        self.dismissed = true;
+        for (editor, _) in &self.editors_with_matches {
+            if let Some(editor) = editor.upgrade(cx) {
+                editor.update(cx, |editor, cx| {
+                    editor.clear_background_highlights::<Self>(cx)
+                });
+            }
+        }
+        if let Some(active_editor) = self.active_editor.as_ref() {
+            cx.focus(active_editor);
+        }
+        cx.notify();
+    }
+
+    fn show(&mut self, focus: bool, cx: &mut ViewContext<Self>) -> bool {
+        let editor = if let Some(editor) = self.active_editor.clone() {
+            editor
+        } else {
+            return false;
+        };
+
+        let display_map = editor
+            .update(cx, |editor, cx| editor.snapshot(cx))
+            .display_snapshot;
+        let selection = editor
+            .read(cx)
+            .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
+
+        let mut text: String;
+        if selection.start == selection.end {
+            let point = selection.start.to_display_point(&display_map);
+            let range = editor::movement::surrounding_word(&display_map, point);
+            let range = range.start.to_offset(&display_map, Bias::Left)
+                ..range.end.to_offset(&display_map, Bias::Right);
+            text = display_map.buffer_snapshot.text_for_range(range).collect();
+            if text.trim().is_empty() {
+                text = String::new();
+            }
+        } else {
+            text = display_map
+                .buffer_snapshot
+                .text_for_range(selection.start..selection.end)
+                .collect();
+        }
+
+        if !text.is_empty() {
+            self.set_query(&text, cx);
+        }
+
+        if focus {
+            let query_editor = self.query_editor.clone();
+            query_editor.update(cx, |query_editor, cx| {
+                query_editor.select_all(&editor::SelectAll, cx);
+            });
+            cx.focus_self();
+        }
+
+        self.dismissed = false;
+        cx.notify();
+        true
+    }
+
     fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
         self.query_editor.update(cx, |query_editor, cx| {
             query_editor.buffer().update(cx, |query_buffer, cx| {
@@ -238,61 +291,13 @@ impl SearchBar {
         .boxed()
     }
 
-    fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
-        workspace.active_pane().update(cx, |pane, cx| {
-            pane.show_toolbar(cx, |cx| SearchBar::new(cx));
-
-            if let Some(search_bar) = pane
-                .active_toolbar()
-                .and_then(|toolbar| toolbar.downcast::<Self>())
-            {
-                search_bar.update(cx, |search_bar, _| search_bar.dismissed = false);
-                let editor = pane.active_item().unwrap().act_as::<Editor>(cx).unwrap();
-                let display_map = editor
-                    .update(cx, |editor, cx| editor.snapshot(cx))
-                    .display_snapshot;
-                let selection = editor
-                    .read(cx)
-                    .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
-
-                let mut text: String;
-                if selection.start == selection.end {
-                    let point = selection.start.to_display_point(&display_map);
-                    let range = editor::movement::surrounding_word(&display_map, point);
-                    let range = range.start.to_offset(&display_map, Bias::Left)
-                        ..range.end.to_offset(&display_map, Bias::Right);
-                    text = display_map.buffer_snapshot.text_for_range(range).collect();
-                    if text.trim().is_empty() {
-                        text = String::new();
-                    }
-                } else {
-                    text = display_map
-                        .buffer_snapshot
-                        .text_for_range(selection.start..selection.end)
-                        .collect();
-                }
-
-                if !text.is_empty() {
-                    search_bar.update(cx, |search_bar, cx| search_bar.set_query(&text, cx));
-                }
-
-                if *focus {
-                    let query_editor = search_bar.read(cx).query_editor.clone();
-                    query_editor.update(cx, |query_editor, cx| {
-                        query_editor.select_all(&editor::SelectAll, cx);
-                    });
-                    cx.focus(&search_bar);
-                }
-            } else {
-                cx.propagate_action();
+    fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext<Pane>) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<SearchBar>() {
+            if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) {
+                return;
             }
-        });
-    }
-
-    fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext<Pane>) {
-        if pane.toolbar::<SearchBar>().is_some() {
-            pane.dismiss_toolbar(cx);
         }
+        cx.propagate_action();
     }
 
     fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
@@ -346,7 +351,7 @@ impl SearchBar {
     }
 
     fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
-        if let Some(search_bar) = pane.toolbar::<SearchBar>() {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<SearchBar>() {
             search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
         }
     }
@@ -541,7 +546,7 @@ mod tests {
 
         let search_bar = cx.add_view(Default::default(), |cx| {
             let mut search_bar = SearchBar::new(cx);
-            search_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
+            search_bar.set_active_pane_item(Some(&editor), cx);
             search_bar
         });
 

crates/search/src/project_search.rs 🔗

@@ -336,8 +336,7 @@ impl ProjectSearchView {
             .detach();
 
         let query_editor = cx.add_view(|cx| {
-            let mut editor =
-                Editor::single_line(Some(|theme| theme.search.editor.input.clone()), cx);
+            let mut editor = Editor::single_line(Some(|theme| theme.search.editor.clone()), cx);
             editor.set_text(query_text, cx);
             editor
         });
@@ -569,7 +568,7 @@ impl ProjectSearchView {
         let editor_container = if self.query_contains_error {
             theme.search.invalid_editor
         } else {
-            theme.search.editor.input.container
+            theme.search.editor.container
         };
         Flex::row()
             .with_child(
@@ -578,7 +577,7 @@ impl ProjectSearchView {
                     .with_style(editor_container)
                     .aligned()
                     .constrained()
-                    .with_max_width(theme.search.editor.max_width)
+                    .with_max_width(theme.search.max_editor_width)
                     .boxed(),
             )
             .with_child(
@@ -615,7 +614,7 @@ impl ProjectSearchView {
                 })
             })
             .contained()
-            .with_style(theme.search.container)
+            .with_style(theme.workspace.toolbar.container)
             .constrained()
             .with_height(theme.workspace.toolbar.height)
             .named("project search")

crates/search/src/search.rs 🔗

@@ -1,11 +1,11 @@
+pub use buffer_search::SearchBar;
+use editor::{Anchor, MultiBufferSnapshot};
+use gpui::{action, MutableAppContext};
 use std::{
     cmp::{self, Ordering},
     ops::Range,
 };
 
-use editor::{Anchor, MultiBufferSnapshot};
-use gpui::{action, MutableAppContext};
-
 mod buffer_search;
 mod project_search;
 

crates/theme/src/theme.rs 🔗

@@ -94,14 +94,18 @@ pub struct Tab {
 
 #[derive(Clone, Deserialize, Default)]
 pub struct Toolbar {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
     pub height: f32,
+    pub item_spacing: f32,
 }
 
 #[derive(Clone, Deserialize, Default)]
 pub struct Search {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub editor: FindEditor,
+    pub max_editor_width: f32,
+    pub editor: FieldEditor,
     pub invalid_editor: ContainerStyle,
     pub option_button_group: ContainerStyle,
     pub option_button: ContainedText,
@@ -115,13 +119,6 @@ pub struct Search {
     pub tab_icon_spacing: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
-pub struct FindEditor {
-    #[serde(flatten)]
-    pub input: FieldEditor,
-    pub max_width: f32,
-}
-
 #[derive(Deserialize, Default)]
 pub struct Sidebar {
     #[serde(flatten)]

crates/workspace/src/pane.rs 🔗

@@ -1,5 +1,5 @@
 use super::{ItemHandle, SplitDirection};
-use crate::{Item, Settings, WeakItemHandle, Workspace};
+use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
 use collections::{HashMap, VecDeque};
 use gpui::{
     action,
@@ -7,16 +7,11 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
     platform::{CursorStyle, NavigationDirection},
-    AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle,
 };
 use project::{ProjectEntryId, ProjectPath};
-use std::{
-    any::{Any, TypeId},
-    cell::RefCell,
-    cmp, mem,
-    rc::Rc,
-};
+use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
 use util::ResultExt;
 
 action!(Split, SplitDirection);
@@ -101,28 +96,7 @@ pub struct Pane {
     items: Vec<Box<dyn ItemHandle>>,
     active_item_index: usize,
     nav_history: Rc<RefCell<NavHistory>>,
-    toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
-    active_toolbar_type: Option<TypeId>,
-    active_toolbar_visible: bool,
-}
-
-pub trait Toolbar: View {
-    fn active_item_changed(
-        &mut self,
-        item: Option<Box<dyn ItemHandle>>,
-        cx: &mut ViewContext<Self>,
-    ) -> bool;
-    fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
-}
-
-trait ToolbarHandle {
-    fn active_item_changed(
-        &self,
-        item: Option<Box<dyn ItemHandle>>,
-        cx: &mut MutableAppContext,
-    ) -> bool;
-    fn on_dismiss(&self, cx: &mut MutableAppContext);
-    fn to_any(&self) -> AnyViewHandle;
+    toolbar: ViewHandle<Toolbar>,
 }
 
 pub struct ItemNavHistory {
@@ -158,14 +132,12 @@ pub struct NavigationEntry {
 }
 
 impl Pane {
-    pub fn new() -> Self {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
         Self {
             items: Vec::new(),
             active_item_index: 0,
             nav_history: Default::default(),
-            toolbars: Default::default(),
-            active_toolbar_type: Default::default(),
-            active_toolbar_visible: false,
+            toolbar: cx.add_view(|_| Toolbar::new()),
         }
     }
 
@@ -402,7 +374,7 @@ impl Pane {
                 self.items[prev_active_item_ix].deactivated(cx);
                 cx.emit(Event::ActivateItem { local });
             }
-            self.update_active_toolbar(cx);
+            self.update_toolbar(cx);
             if local {
                 self.focus_active_item(cx);
                 self.activate(cx);
@@ -487,7 +459,7 @@ impl Pane {
             self.focus_active_item(cx);
             self.activate(cx);
         }
-        self.update_active_toolbar(cx);
+        self.update_toolbar(cx);
 
         cx.notify();
     }
@@ -502,63 +474,18 @@ impl Pane {
         cx.emit(Event::Split(direction));
     }
 
-    pub fn show_toolbar<F, V>(&mut self, cx: &mut ViewContext<Self>, build_toolbar: F)
-    where
-        F: FnOnce(&mut ViewContext<V>) -> V,
-        V: Toolbar,
-    {
-        let type_id = TypeId::of::<V>();
-        if self.active_toolbar_type != Some(type_id) {
-            self.dismiss_toolbar(cx);
-
-            let active_item = self.active_item();
-            self.toolbars
-                .entry(type_id)
-                .or_insert_with(|| Box::new(cx.add_view(build_toolbar)));
-
-            self.active_toolbar_type = Some(type_id);
-            self.active_toolbar_visible =
-                self.toolbars[&type_id].active_item_changed(active_item, cx);
-            cx.notify();
-        }
-    }
-
-    pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(active_toolbar_type) = self.active_toolbar_type.take() {
-            self.toolbars
-                .get_mut(&active_toolbar_type)
-                .unwrap()
-                .on_dismiss(cx);
-            self.active_toolbar_visible = false;
-            self.focus_active_item(cx);
-            cx.notify();
-        }
+    pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
+        &self.toolbar
     }
 
-    pub fn toolbar<T: Toolbar>(&self) -> Option<ViewHandle<T>> {
-        self.toolbars
-            .get(&TypeId::of::<T>())
-            .and_then(|toolbar| toolbar.to_any().downcast())
-    }
-
-    pub fn active_toolbar(&self) -> Option<AnyViewHandle> {
-        let type_id = self.active_toolbar_type?;
-        let toolbar = self.toolbars.get(&type_id)?;
-        if self.active_toolbar_visible {
-            Some(toolbar.to_any())
-        } else {
-            None
-        }
-    }
-
-    fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
-        let active_item = self.items.get(self.active_item_index);
-        for (toolbar_type_id, toolbar) in &self.toolbars {
-            let visible = toolbar.active_item_changed(active_item.cloned(), cx);
-            if Some(*toolbar_type_id) == self.active_toolbar_type {
-                self.active_toolbar_visible = visible;
-            }
-        }
+    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
+        let active_item = self
+            .items
+            .get(self.active_item_index)
+            .map(|item| item.as_ref());
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.set_active_pane_item(active_item, cx);
+        });
     }
 
     fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
@@ -713,11 +640,7 @@ impl View for Pane {
         EventHandler::new(if let Some(active_item) = self.active_item() {
             Flex::column()
                 .with_child(self.render_tabs(cx))
-                .with_children(
-                    self.active_toolbar()
-                        .as_ref()
-                        .map(|view| ChildView::new(view).boxed()),
-                )
+                .with_child(ChildView::new(&self.toolbar).boxed())
                 .with_child(ChildView::new(active_item).flexible(1., true).boxed())
                 .boxed()
         } else {
@@ -740,24 +663,6 @@ impl View for Pane {
     }
 }
 
-impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
-    fn active_item_changed(
-        &self,
-        item: Option<Box<dyn ItemHandle>>,
-        cx: &mut MutableAppContext,
-    ) -> bool {
-        self.update(cx, |this, cx| this.active_item_changed(item, cx))
-    }
-
-    fn on_dismiss(&self, cx: &mut MutableAppContext) {
-        self.update(cx, |this, cx| this.on_dismiss(cx));
-    }
-
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
-    }
-}
-
 impl ItemNavHistory {
     pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
         Self {

crates/workspace/src/toolbar.rs 🔗

@@ -0,0 +1,131 @@
+use crate::{ItemHandle, Settings};
+use gpui::{
+    elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, View,
+    ViewContext, ViewHandle,
+};
+
+pub trait ToolbarItemView: View {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn crate::ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    );
+}
+
+trait ToolbarItemViewHandle {
+    fn to_any(&self) -> AnyViewHandle;
+    fn set_active_pane_item(
+        &self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut MutableAppContext,
+    );
+}
+
+pub struct Toolbar {
+    active_pane_item: Option<Box<dyn ItemHandle>>,
+    left_items: Vec<Box<dyn ToolbarItemViewHandle>>,
+    right_items: Vec<Box<dyn ToolbarItemViewHandle>>,
+}
+
+impl Entity for Toolbar {
+    type Event = ();
+}
+
+impl View for Toolbar {
+    fn ui_name() -> &'static str {
+        "Toolbar"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = &cx.global::<Settings>().theme.workspace.toolbar;
+        Flex::row()
+            .with_children(self.left_items.iter().map(|i| {
+                ChildView::new(i.as_ref())
+                    .aligned()
+                    .contained()
+                    .with_margin_right(theme.item_spacing)
+                    .boxed()
+            }))
+            .with_child(Empty::new().flexible(1., true).boxed())
+            .with_children(self.right_items.iter().map(|i| {
+                ChildView::new(i.as_ref())
+                    .aligned()
+                    .contained()
+                    .with_margin_left(theme.item_spacing)
+                    .boxed()
+            }))
+            .contained()
+            .with_style(theme.container)
+            .constrained()
+            .with_height(theme.height)
+            .boxed()
+    }
+}
+
+impl Toolbar {
+    pub fn new() -> Self {
+        Self {
+            active_pane_item: None,
+            left_items: Default::default(),
+            right_items: Default::default(),
+        }
+    }
+
+    pub fn add_left_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+    where
+        T: 'static + ToolbarItemView,
+    {
+        item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
+        self.left_items.push(Box::new(item));
+        cx.notify();
+    }
+
+    pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+    where
+        T: 'static + ToolbarItemView,
+    {
+        item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
+        self.right_items.push(Box::new(item));
+        cx.notify();
+    }
+
+    pub fn set_active_pane_item(
+        &mut self,
+        item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.active_pane_item = item.map(|item| item.boxed_clone());
+        for tool in self.left_items.iter().chain(&self.right_items) {
+            tool.set_active_pane_item(item, cx);
+        }
+    }
+
+    pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
+        self.left_items
+            .iter()
+            .chain(&self.right_items)
+            .find_map(|tool| tool.to_any().downcast())
+    }
+}
+
+impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
+    fn to_any(&self) -> AnyViewHandle {
+        self.into()
+    }
+
+    fn set_active_pane_item(
+        &self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut MutableAppContext,
+    ) {
+        self.update(cx, |this, cx| {
+            this.set_active_pane_item(active_pane_item, cx)
+        });
+    }
+}
+
+impl Into<AnyViewHandle> for &dyn ToolbarItemViewHandle {
+    fn into(self) -> AnyViewHandle {
+        self.to_any()
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -5,6 +5,7 @@ pub mod pane_group;
 pub mod settings;
 pub mod sidebar;
 mod status_bar;
+mod toolbar;
 
 use anyhow::{anyhow, Context, Result};
 use client::{
@@ -47,6 +48,7 @@ use std::{
     },
 };
 use theme::{Theme, ThemeRegistry};
+pub use toolbar::ToolbarItemView;
 use util::ResultExt;
 
 type ProjectItemBuilders = HashMap<
@@ -720,7 +722,7 @@ impl Workspace {
         })
         .detach();
 
-        let pane = cx.add_view(|_| Pane::new());
+        let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();
         cx.observe(&pane, move |me, _, cx| {
             let active_entry = me.active_project_path(cx);
@@ -733,6 +735,7 @@ impl Workspace {
         })
         .detach();
         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();
@@ -1051,7 +1054,7 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane = cx.add_view(|_| Pane::new());
+        let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();
         cx.observe(&pane, move |me, _, cx| {
             let active_entry = me.active_project_path(cx);

crates/zed/assets/themes/_base.toml 🔗

@@ -85,7 +85,10 @@ diagnostic_message = "$text.2"
 lsp_message = "$text.2"
 
 [workspace.toolbar]
+background = "$surface.1"
+border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false }
 height = 44
+item_spacing = 8
 
 [panel]
 padding = { top = 12, left = 12, bottom = 12, right = 12 }
@@ -353,8 +356,8 @@ tab_icon_spacing = 4
 tab_summary_spacing = 10
 
 [search]
+max_editor_width = 400
 match_background = "$state.highlighted_line"
-background = "$surface.1"
 results_status = { extends = "$text.0", size = 18 }
 tab_icon_width = 14
 tab_icon_spacing = 4
@@ -388,7 +391,6 @@ extends = "$text.1"
 padding = 6
 
 [search.editor]
-max_width = 400
 background = "$surface.0"
 corner_radius = 6
 padding = { left = 13, right = 13, top = 3, bottom = 3 }

crates/zed/src/zed.rs 🔗

@@ -21,6 +21,7 @@ pub use lsp;
 use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
+use search::SearchBar;
 use std::{path::PathBuf, sync::Arc};
 pub use workspace;
 use workspace::{AppState, Settings, Workspace, WorkspaceParams};
@@ -104,6 +105,17 @@ pub fn build_workspace(
     app_state: &Arc<AppState>,
     cx: &mut ViewContext<Workspace>,
 ) -> Workspace {
+    cx.subscribe(&cx.handle(), |_, _, event, cx| {
+        let workspace::Event::PaneAdded(pane) = event;
+        pane.update(cx, |pane, cx| {
+            pane.toolbar().update(cx, |toolbar, cx| {
+                let search_bar = cx.add_view(|cx| SearchBar::new(cx));
+                toolbar.add_right_item(search_bar, cx);
+            })
+        });
+    })
+    .detach();
+
     let workspace_params = WorkspaceParams {
         project,
         client: app_state.client.clone(),