Overhaul search bar layout

Max Brunsfeld created

* Use a single row, instead of centering the search bar within a double-row toolbar.
* Search query controls on the left, navigation on the right
* Semantic is the final mode, for greater stability between buffer and project search.
* Prevent query editor from moving when toggling path filters

Change summary

crates/search/src/buffer_search.rs  |  74 +++++-----
crates/search/src/mode.rs           |  23 ---
crates/search/src/project_search.rs | 216 +++++++++++++++---------------
crates/search/src/search_bar.rs     |  27 ++-
crates/theme/src/theme.rs           |   2 
crates/workspace/src/toolbar.rs     |  13 -
styles/src/style_tree/search.ts     |  41 -----
7 files changed, 164 insertions(+), 232 deletions(-)

Detailed changes

crates/search/src/buffer_search.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     history::SearchHistory,
-    mode::{next_mode, SearchMode},
+    mode::{next_mode, SearchMode, Side},
     search_bar::{render_nav_button, render_search_mode_button},
     CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches,
     SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
@@ -156,11 +156,12 @@ impl View for BufferSearchBar {
         self.query_editor.update(cx, |editor, cx| {
             editor.set_placeholder_text(new_placeholder_text, cx);
         });
-        let search_button_for_mode = |mode, cx: &mut ViewContext<BufferSearchBar>| {
+        let search_button_for_mode = |mode, side, cx: &mut ViewContext<BufferSearchBar>| {
             let is_active = self.current_mode == mode;
 
             render_search_mode_button(
                 mode,
+                side,
                 is_active,
                 move |_, this, cx| {
                     this.activate_search_mode(mode, cx);
@@ -212,20 +213,11 @@ impl View for BufferSearchBar {
             )
         };
 
-        let icon_style = theme.search.editor_icon.clone();
-        let nav_column = Flex::row()
-            .with_child(self.render_action_button("all", cx))
-            .with_child(nav_button_for_direction("<", Direction::Prev, cx))
-            .with_child(nav_button_for_direction(">", Direction::Next, cx))
-            .with_child(Flex::row().with_children(match_count))
-            .constrained()
-            .with_height(theme.search.search_bar_row_height);
-
-        let query = Flex::row()
+        let query_column = Flex::row()
             .with_child(
-                Svg::for_style(icon_style.icon)
+                Svg::for_style(theme.search.editor_icon.clone().icon)
                     .contained()
-                    .with_style(icon_style.container),
+                    .with_style(theme.search.editor_icon.clone().container),
             )
             .with_child(ChildView::new(&self.query_editor, cx).flex(1., true))
             .with_child(
@@ -244,42 +236,45 @@ impl View for BufferSearchBar {
                     .contained(),
             )
             .align_children_center()
-            .flex(1., true);
-        let editor_column = Flex::row()
-            .with_child(
-                query
-                    .contained()
-                    .with_style(query_container_style)
-                    .constrained()
-                    .with_min_width(theme.search.editor.min_width)
-                    .with_max_width(theme.search.editor.max_width)
-                    .with_height(theme.search.search_bar_row_height)
-                    .flex(1., false),
-            )
             .contained()
+            .with_style(query_container_style)
             .constrained()
+            .with_min_width(theme.search.editor.min_width)
+            .with_max_width(theme.search.editor.max_width)
             .with_height(theme.search.search_bar_row_height)
             .flex(1., false);
+
         let mode_column = Flex::row()
-            .with_child(
-                Flex::row()
-                    .with_child(search_button_for_mode(SearchMode::Text, cx))
-                    .with_child(search_button_for_mode(SearchMode::Regex, cx))
-                    .contained()
-                    .with_style(theme.search.modes_container),
-            )
+            .with_child(search_button_for_mode(
+                SearchMode::Text,
+                Some(Side::Left),
+                cx,
+            ))
+            .with_child(search_button_for_mode(
+                SearchMode::Regex,
+                Some(Side::Right),
+                cx,
+            ))
+            .contained()
+            .with_style(theme.search.modes_container)
+            .constrained()
+            .with_height(theme.search.search_bar_row_height);
+
+        let nav_column = Flex::row()
+            .with_child(Flex::row().with_children(match_count))
+            .with_child(self.render_action_button("all", cx))
+            .with_child(nav_button_for_direction("<", Direction::Prev, cx))
+            .with_child(nav_button_for_direction(">", Direction::Next, cx))
             .constrained()
             .with_height(theme.search.search_bar_row_height)
-            .aligned()
-            .right()
             .flex_float();
+
         Flex::row()
-            .with_child(editor_column)
-            .with_child(nav_column)
+            .with_child(query_column)
             .with_child(mode_column)
+            .with_child(nav_column)
             .contained()
             .with_style(theme.search.container)
-            .aligned()
             .into_any_named("search bar")
     }
 }
@@ -333,8 +328,9 @@ impl ToolbarItemView for BufferSearchBar {
             ToolbarItemLocation::Hidden
         }
     }
+
     fn row_count(&self, _: &ViewContext<Self>) -> usize {
-        2
+        1
     }
 }
 

crates/search/src/mode.rs 🔗

@@ -48,29 +48,6 @@ impl SearchMode {
             SearchMode::Regex => Box::new(ActivateRegexMode),
         }
     }
-
-    pub(crate) fn border_right(&self) -> bool {
-        match self {
-            SearchMode::Regex => true,
-            SearchMode::Text => true,
-            SearchMode::Semantic => true,
-        }
-    }
-
-    pub(crate) fn border_left(&self) -> bool {
-        match self {
-            SearchMode::Text => true,
-            _ => false,
-        }
-    }
-
-    pub(crate) fn button_side(&self) -> Option<Side> {
-        match self {
-            SearchMode::Text => Some(Side::Left),
-            SearchMode::Semantic => None,
-            SearchMode::Regex => Some(Side::Right),
-        }
-    }
 }
 
 pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {

crates/search/src/project_search.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     history::SearchHistory,
-    mode::SearchMode,
+    mode::{SearchMode, Side},
     search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
     ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions,
     SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
@@ -1424,8 +1424,13 @@ impl View for ProjectSearchBar {
                 },
                 cx,
             );
+
             let search = _search.read(cx);
+            let is_semantic_available = SemanticIndex::enabled(cx);
             let is_semantic_disabled = search.semantic_state.is_none();
+            let icon_style = theme.search.editor_icon.clone();
+            let is_active = search.active_match_index.is_some();
+
             let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
                 crate::search_bar::render_option_button_icon(
                     self.is_option_enabled(option, cx),
@@ -1451,28 +1456,23 @@ impl View for ProjectSearchBar {
                 render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx)
             });
 
-            let search = _search.read(cx);
-            let icon_style = theme.search.editor_icon.clone();
-
-            // Editor Functionality
-            let query = Flex::row()
-                .with_child(
-                    Svg::for_style(icon_style.icon)
-                        .contained()
-                        .with_style(icon_style.container),
-                )
-                .with_child(ChildView::new(&search.query_editor, cx).flex(1., true))
-                .with_child(
-                    Flex::row()
-                        .with_child(filter_button)
-                        .with_children(case_sensitive)
-                        .with_children(whole_word)
-                        .flex(1., false)
-                        .constrained()
-                        .contained(),
+            let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
+                let is_active = if let Some(search) = self.active_project_search.as_ref() {
+                    let search = search.read(cx);
+                    search.current_mode == mode
+                } else {
+                    false
+                };
+                render_search_mode_button(
+                    mode,
+                    side,
+                    is_active,
+                    move |_, this, cx| {
+                        this.activate_search_mode(mode, cx);
+                    },
+                    cx,
                 )
-                .align_children_center()
-                .flex(1., true);
+            };
 
             let search = _search.read(cx);
 
@@ -1490,50 +1490,6 @@ impl View for ProjectSearchBar {
                     theme.search.include_exclude_editor.input.container
                 };
 
-            let included_files_view = ChildView::new(&search.included_files_editor, cx)
-                .contained()
-                .flex(1., true);
-            let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
-                .contained()
-                .flex(1., true);
-            let filters = search.filters_enabled.then(|| {
-                Flex::row()
-                    .with_child(
-                        included_files_view
-                            .contained()
-                            .with_style(include_container_style)
-                            .constrained()
-                            .with_height(theme.search.search_bar_row_height)
-                            .with_min_width(theme.search.include_exclude_editor.min_width)
-                            .with_max_width(theme.search.include_exclude_editor.max_width),
-                    )
-                    .with_child(
-                        excluded_files_view
-                            .contained()
-                            .with_style(exclude_container_style)
-                            .constrained()
-                            .with_height(theme.search.search_bar_row_height)
-                            .with_min_width(theme.search.include_exclude_editor.min_width)
-                            .with_max_width(theme.search.include_exclude_editor.max_width),
-                    )
-                    .contained()
-                    .with_padding_top(theme.workspace.toolbar.container.padding.bottom)
-            });
-
-            let editor_column = Flex::column()
-                .with_child(
-                    query
-                        .contained()
-                        .with_style(query_container_style)
-                        .constrained()
-                        .with_min_width(theme.search.editor.min_width)
-                        .with_max_width(theme.search.editor.max_width)
-                        .with_height(theme.search.search_bar_row_height)
-                        .flex(1., false),
-                )
-                .with_children(filters)
-                .flex(1., false);
-
             let matches = search.active_match_index.map(|match_ix| {
                 Label::new(
                     format!(
@@ -1548,25 +1504,81 @@ impl View for ProjectSearchBar {
                 .aligned()
             });
 
-            let search_button_for_mode = |mode, cx: &mut ViewContext<ProjectSearchBar>| {
-                let is_active = if let Some(search) = self.active_project_search.as_ref() {
-                    let search = search.read(cx);
-                    search.current_mode == mode
-                } else {
-                    false
-                };
-                render_search_mode_button(
-                    mode,
-                    is_active,
-                    move |_, this, cx| {
-                        this.activate_search_mode(mode, cx);
-                    },
-                    cx,
+            let query_column = Flex::column()
+                .with_spacing(theme.search.search_row_spacing)
+                .with_child(
+                    Flex::row()
+                        .with_child(
+                            Svg::for_style(icon_style.icon)
+                                .contained()
+                                .with_style(icon_style.container),
+                        )
+                        .with_child(ChildView::new(&search.query_editor, cx).flex(1., true))
+                        .with_child(
+                            Flex::row()
+                                .with_child(filter_button)
+                                .with_children(case_sensitive)
+                                .with_children(whole_word)
+                                .flex(1., false)
+                                .constrained()
+                                .contained(),
+                        )
+                        .align_children_center()
+                        .contained()
+                        .with_style(query_container_style)
+                        .constrained()
+                        .with_min_width(theme.search.editor.min_width)
+                        .with_max_width(theme.search.editor.max_width)
+                        .with_height(theme.search.search_bar_row_height)
+                        .flex(1., false),
                 )
-            };
-            let is_active = search.active_match_index.is_some();
-            let semantic_index = SemanticIndex::enabled(cx)
-                .then(|| search_button_for_mode(SearchMode::Semantic, cx));
+                .with_children(search.filters_enabled.then(|| {
+                    Flex::row()
+                        .with_child(
+                            ChildView::new(&search.included_files_editor, cx)
+                                .contained()
+                                .with_style(include_container_style)
+                                .constrained()
+                                .with_height(theme.search.search_bar_row_height)
+                                .flex(1., true),
+                        )
+                        .with_child(
+                            ChildView::new(&search.excluded_files_editor, cx)
+                                .contained()
+                                .with_style(exclude_container_style)
+                                .constrained()
+                                .with_height(theme.search.search_bar_row_height)
+                                .flex(1., true),
+                        )
+                        .constrained()
+                        .with_min_width(theme.search.editor.min_width)
+                        .with_max_width(theme.search.editor.max_width)
+                        .flex(1., false)
+                }))
+                .flex(1., false);
+
+            let mode_column =
+                Flex::row()
+                    .with_child(search_button_for_mode(
+                        SearchMode::Text,
+                        Some(Side::Left),
+                        cx,
+                    ))
+                    .with_child(search_button_for_mode(
+                        SearchMode::Regex,
+                        if is_semantic_available {
+                            None
+                        } else {
+                            Some(Side::Right)
+                        },
+                        cx,
+                    ))
+                    .with_children(is_semantic_available.then(|| {
+                        search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx)
+                    }))
+                    .contained()
+                    .with_style(theme.search.modes_container);
+
             let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
                 render_nav_button(
                     label,
@@ -1582,32 +1594,17 @@ impl View for ProjectSearchBar {
             };
 
             let nav_column = Flex::row()
+                .with_child(Flex::row().with_children(matches))
                 .with_child(nav_button_for_direction("<", Direction::Prev, cx))
                 .with_child(nav_button_for_direction(">", Direction::Next, cx))
-                .with_child(Flex::row().with_children(matches))
-                .constrained()
-                .with_height(theme.search.search_bar_row_height);
-
-            let mode_column = Flex::row()
-                .with_child(
-                    Flex::row()
-                        .with_child(search_button_for_mode(SearchMode::Text, cx))
-                        .with_children(semantic_index)
-                        .with_child(search_button_for_mode(SearchMode::Regex, cx))
-                        .contained()
-                        .with_style(theme.search.modes_container),
-                )
                 .constrained()
                 .with_height(theme.search.search_bar_row_height)
-                .aligned()
-                .right()
-                .top()
                 .flex_float();
 
             Flex::row()
-                .with_child(editor_column)
-                .with_child(nav_column)
+                .with_child(query_column)
                 .with_child(mode_column)
+                .with_child(nav_column)
                 .contained()
                 .with_style(theme.search.container)
                 .into_any_named("project search")
@@ -1636,7 +1633,7 @@ impl ToolbarItemView for ProjectSearchBar {
             self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
             self.active_project_search = Some(search);
             ToolbarItemLocation::PrimaryLeft {
-                flex: Some((1., false)),
+                flex: Some((1., true)),
             }
         } else {
             ToolbarItemLocation::Hidden
@@ -1644,13 +1641,12 @@ impl ToolbarItemView for ProjectSearchBar {
     }
 
     fn row_count(&self, cx: &ViewContext<Self>) -> usize {
-        self.active_project_search
-            .as_ref()
-            .map(|search| {
-                let offset = search.read(cx).filters_enabled as usize;
-                2 + offset
-            })
-            .unwrap_or_else(|| 2)
+        if let Some(search) = self.active_project_search.as_ref() {
+            if search.read(cx).filters_enabled {
+                return 2;
+            }
+        }
+        1
     }
 }
 

crates/search/src/search_bar.rs 🔗

@@ -83,6 +83,7 @@ pub(super) fn render_nav_button<V: View>(
 
 pub(crate) fn render_search_mode_button<V: View>(
     mode: SearchMode,
+    side: Option<Side>,
     is_active: bool,
     on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
     cx: &mut ViewContext<V>,
@@ -91,41 +92,41 @@ pub(crate) fn render_search_mode_button<V: View>(
     enum SearchModeButton {}
     MouseEventHandler::new::<SearchModeButton, _>(mode.region_id(), cx, |state, cx| {
         let theme = theme::current(cx);
-        let mut style = theme
+        let style = theme
             .search
             .mode_button
             .in_state(is_active)
             .style_for(state)
             .clone();
-        style.container.border.left = mode.border_left();
-        style.container.border.right = mode.border_right();
 
-        let label = Label::new(mode.label(), style.text.clone())
-            .aligned()
-            .contained();
-        let mut container_style = style.container.clone();
-        if let Some(button_side) = mode.button_side() {
+        let mut container_style = style.container;
+        if let Some(button_side) = side {
             if button_side == Side::Left {
+                container_style.border.left = true;
                 container_style.corner_radii = CornerRadii {
                     bottom_right: 0.,
                     top_right: 0.,
                     ..container_style.corner_radii
                 };
-                label.with_style(container_style)
             } else {
+                container_style.border.left = false;
                 container_style.corner_radii = CornerRadii {
                     bottom_left: 0.,
                     top_left: 0.,
                     ..container_style.corner_radii
                 };
-                label.with_style(container_style)
             }
         } else {
+            container_style.border.left = false;
             container_style.corner_radii = CornerRadii::default();
-            label.with_style(container_style)
         }
-        .constrained()
-        .with_height(theme.search.search_bar_row_height)
+
+        Label::new(mode.label(), style.text)
+            .aligned()
+            .contained()
+            .with_style(container_style)
+            .constrained()
+            .with_height(theme.search.search_bar_row_height)
     })
     .on_click(MouseButton::Left, on_click)
     .with_cursor_style(CursorStyle::PointingHand)

crates/theme/src/theme.rs 🔗

@@ -437,11 +437,11 @@ pub struct Search {
     pub match_index: ContainedText,
     pub major_results_status: TextStyle,
     pub minor_results_status: TextStyle,
-    pub dismiss_button: Interactive<IconButton>,
     pub editor_icon: IconStyle,
     pub mode_button: Toggleable<Interactive<ContainedText>>,
     pub nav_button: Toggleable<Interactive<ContainedLabel>>,
     pub search_bar_row_height: f32,
+    pub search_row_spacing: f32,
     pub option_button_height: f32,
     pub modes_container: ContainerStyle,
 }

crates/workspace/src/toolbar.rs 🔗

@@ -81,10 +81,7 @@ impl View for Toolbar {
 
                 ToolbarItemLocation::PrimaryLeft { flex } => {
                     primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-                    let left_item = ChildView::new(item.as_any(), cx)
-                        .aligned()
-                        .contained()
-                        .with_margin_right(spacing);
+                    let left_item = ChildView::new(item.as_any(), cx).aligned();
                     if let Some((flex, expanded)) = flex {
                         primary_left_items.push(left_item.flex(flex, expanded).into_any());
                     } else {
@@ -94,11 +91,7 @@ impl View for Toolbar {
 
                 ToolbarItemLocation::PrimaryRight { flex } => {
                     primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-                    let right_item = ChildView::new(item.as_any(), cx)
-                        .aligned()
-                        .contained()
-                        .with_margin_left(spacing)
-                        .flex_float();
+                    let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
                     if let Some((flex, expanded)) = flex {
                         primary_right_items.push(right_item.flex(flex, expanded).into_any());
                     } else {
@@ -120,7 +113,7 @@ impl View for Toolbar {
         let container_style = theme.container;
         let height = theme.height * primary_items_row_count as f32;
 
-        let mut primary_items = Flex::row();
+        let mut primary_items = Flex::row().with_spacing(spacing);
         primary_items.extend(primary_left_items);
         primary_items.extend(primary_right_items);
 

styles/src/style_tree/search.ts 🔗

@@ -34,7 +34,7 @@ export default function search(): any {
     }
 
     return {
-        padding: { top: 16, bottom: 16, left: 16, right: 16 },
+        padding: { top: 4, bottom: 4 },
         // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
         match_background: with_opacity(
             foreground(theme.highest, "accent"),
@@ -210,6 +210,7 @@ export default function search(): any {
             ...text(theme.highest, "mono", "variant"),
             padding: {
                 left: 9,
+                right: 9,
             },
         },
         option_button_group: {
@@ -232,34 +233,6 @@ export default function search(): any {
             ...text(theme.highest, "mono", "variant"),
             size: 13,
         },
-        dismiss_button: interactive({
-            base: {
-                color: foreground(theme.highest, "variant"),
-                icon_width: 14,
-                button_width: 32,
-                corner_radius: 6,
-                padding: {
-                    // // top: 10,
-                    // bottom: 10,
-                    left: 10,
-                    right: 10,
-                },
-
-                background: background(theme.highest, "variant"),
-
-                border: border(theme.highest, "on"),
-            },
-            state: {
-                hovered: {
-                    color: foreground(theme.highest, "hovered"),
-                    background: background(theme.highest, "variant", "hovered")
-                },
-                clicked: {
-                    color: foreground(theme.highest, "pressed"),
-                    background: background(theme.highest, "variant", "pressed")
-                },
-            },
-        }),
         editor_icon: {
             icon: {
                 color: foreground(theme.highest, "variant"),
@@ -375,13 +348,9 @@ export default function search(): any {
                 })
             }
         }),
-        search_bar_row_height: 32,
+        search_bar_row_height: 34,
+        search_row_spacing: 8,
         option_button_height: 22,
-        modes_container: {
-            margin: {
-                right: 9
-            }
-        }
-
+        modes_container: {}
     }
 }