Overhaul search bar layout

* 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
This commit is contained in:
Max Brunsfeld 2023-08-28 14:20:09 -07:00
parent 78f9a1f280
commit 3eee282a6b
7 changed files with 164 additions and 232 deletions

View File

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

View File

@ -48,29 +48,6 @@ impl SearchMode {
SearchMode::Regex => Box::new(ActivateRegexMode), 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 { pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ export default function search(): any {
} }
return { 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 // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
match_background: with_opacity( match_background: with_opacity(
foreground(theme.highest, "accent"), foreground(theme.highest, "accent"),
@ -210,6 +210,7 @@ export default function search(): any {
...text(theme.highest, "mono", "variant"), ...text(theme.highest, "mono", "variant"),
padding: { padding: {
left: 9, left: 9,
right: 9,
}, },
}, },
option_button_group: { option_button_group: {
@ -232,34 +233,6 @@ export default function search(): any {
...text(theme.highest, "mono", "variant"), ...text(theme.highest, "mono", "variant"),
size: 13, 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: { editor_icon: {
icon: { icon: {
color: foreground(theme.highest, "variant"), 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, option_button_height: 22,
modes_container: { modes_container: {}
margin: {
right: 9
}
}
} }
} }