Merge branch 'main' into divs
This commit is contained in:
commit
d375f7992d
277 changed files with 19044 additions and 8896 deletions
|
@ -30,11 +30,11 @@ serde_derive.workspace = true
|
|||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
globset.workspace = true
|
||||
|
||||
serde_json.workspace = true
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use crate::{
|
||||
NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches,
|
||||
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
|
||||
history::SearchHistory,
|
||||
mode::{next_mode, SearchMode},
|
||||
search_bar::{render_nav_button, render_search_mode_button},
|
||||
CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches,
|
||||
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
|
@ -16,6 +19,7 @@ use gpui::{
|
|||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::ItemHandle,
|
||||
|
@ -36,7 +40,7 @@ pub enum Event {
|
|||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(BufferSearchBar::deploy);
|
||||
cx.add_action(BufferSearchBar::deploy_bar);
|
||||
cx.add_action(BufferSearchBar::dismiss);
|
||||
cx.add_action(BufferSearchBar::focus_editor);
|
||||
cx.add_action(BufferSearchBar::select_next_match);
|
||||
|
@ -48,9 +52,10 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.add_action(BufferSearchBar::handle_editor_cancel);
|
||||
cx.add_action(BufferSearchBar::next_history_query);
|
||||
cx.add_action(BufferSearchBar::previous_history_query);
|
||||
cx.add_action(BufferSearchBar::cycle_mode);
|
||||
cx.add_action(BufferSearchBar::cycle_mode_on_pane);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
||||
add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
|
||||
}
|
||||
|
||||
fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
|
||||
|
@ -79,6 +84,7 @@ pub struct BufferSearchBar {
|
|||
query_contains_error: bool,
|
||||
dismissed: bool,
|
||||
search_history: SearchHistory,
|
||||
current_mode: SearchMode,
|
||||
}
|
||||
|
||||
impl Entity for BufferSearchBar {
|
||||
|
@ -98,7 +104,7 @@ impl View for BufferSearchBar {
|
|||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = theme::current(cx).clone();
|
||||
let editor_container = if self.query_contains_error {
|
||||
let query_container_style = if self.query_contains_error {
|
||||
theme.search.invalid_editor
|
||||
} else {
|
||||
theme.search.editor.input.container
|
||||
|
@ -150,81 +156,137 @@ 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 is_active = self.current_mode == mode;
|
||||
|
||||
Flex::row()
|
||||
render_search_mode_button(
|
||||
mode,
|
||||
is_active,
|
||||
move |_, this, cx| {
|
||||
this.activate_search_mode(mode, cx);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
};
|
||||
let search_option_button = |option| {
|
||||
let is_active = self.search_options.contains(option);
|
||||
option.as_button(
|
||||
is_active,
|
||||
theme.tooltip.clone(),
|
||||
theme.search.option_button_component.clone(),
|
||||
)
|
||||
};
|
||||
let match_count = self
|
||||
.active_searchable_item
|
||||
.as_ref()
|
||||
.and_then(|searchable_item| {
|
||||
if self.query(cx).is_empty() {
|
||||
return None;
|
||||
}
|
||||
let matches = self
|
||||
.searchable_items_with_matches
|
||||
.get(&searchable_item.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(),
|
||||
)
|
||||
});
|
||||
let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
|
||||
render_nav_button(
|
||||
label,
|
||||
direction,
|
||||
self.active_match_index.is_some(),
|
||||
move |_, this, cx| match direction {
|
||||
Direction::Prev => this.select_prev_match(&Default::default(), cx),
|
||||
Direction::Next => this.select_next_match(&Default::default(), cx),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
};
|
||||
|
||||
let icon_style = theme.search.editor_icon.clone();
|
||||
let nav_column = Flex::row()
|
||||
.with_child(self.render_action_button("Select 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(
|
||||
Svg::for_style(icon_style.icon)
|
||||
.contained()
|
||||
.with_style(icon_style.container),
|
||||
)
|
||||
.with_child(ChildView::new(&self.query_editor, cx).flex(1., true))
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&self.query_editor, cx)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children(self.active_searchable_item.as_ref().and_then(
|
||||
|searchable_item| {
|
||||
let matches = self
|
||||
.searchable_items_with_matches
|
||||
.get(&searchable_item.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(),
|
||||
)
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(editor_container)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_min_width(theme.search.editor.min_width)
|
||||
.with_max_width(theme.search.editor.max_width)
|
||||
.flex(1., false),
|
||||
.with_children(
|
||||
supported_options
|
||||
.case
|
||||
.then(|| search_option_button(SearchOptions::CASE_SENSITIVE)),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||
.with_child(self.render_action_button("Select All", cx))
|
||||
.aligned(),
|
||||
.with_children(
|
||||
supported_options
|
||||
.word
|
||||
.then(|| search_option_button(SearchOptions::WHOLE_WORD)),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.case,
|
||||
"Case",
|
||||
SearchOptions::CASE_SENSITIVE,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.word,
|
||||
"Word",
|
||||
SearchOptions::WHOLE_WORD,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.regex,
|
||||
"Regex",
|
||||
SearchOptions::REGEX,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
.with_style(theme.search.option_button_group)
|
||||
.aligned(),
|
||||
)
|
||||
.flex(1., true),
|
||||
.flex_float()
|
||||
.contained(),
|
||||
)
|
||||
.with_child(self.render_close_button(&theme.search, cx))
|
||||
.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()
|
||||
.constrained()
|
||||
.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(super::search_bar::render_close_button(
|
||||
"Dismiss Buffer Search",
|
||||
&theme.search,
|
||||
cx,
|
||||
|_, this, cx| this.dismiss(&Default::default(), cx),
|
||||
Some(Box::new(Dismiss)),
|
||||
))
|
||||
.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(mode_column)
|
||||
.contained()
|
||||
.with_style(theme.search.container)
|
||||
.aligned()
|
||||
.into_any_named("search bar")
|
||||
}
|
||||
}
|
||||
|
@ -278,6 +340,9 @@ impl ToolbarItemView for BufferSearchBar {
|
|||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
fn row_count(&self, _: &ViewContext<Self>) -> usize {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferSearchBar {
|
||||
|
@ -304,6 +369,7 @@ impl BufferSearchBar {
|
|||
query_contains_error: false,
|
||||
dismissed: true,
|
||||
search_history: SearchHistory::default(),
|
||||
current_mode: SearchMode::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,6 +393,19 @@ impl BufferSearchBar {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.show(cx) {
|
||||
self.search_suggested(cx);
|
||||
if deploy.focus {
|
||||
self.select_query(cx);
|
||||
cx.focus_self();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.active_searchable_item.is_none() {
|
||||
return false;
|
||||
|
@ -402,91 +481,6 @@ impl BufferSearchBar {
|
|||
self.update_matches(cx)
|
||||
}
|
||||
|
||||
fn render_search_option(
|
||||
&self,
|
||||
option_supported: bool,
|
||||
icon: &'static str,
|
||||
option: SearchOptions,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
if !option_supported {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let is_active = self.search_options.contains(option);
|
||||
Some(
|
||||
MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme
|
||||
.search
|
||||
.option_button
|
||||
.in_state(is_active)
|
||||
.style_for(state);
|
||||
Label::new(icon, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_search_option(option, cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self>(
|
||||
option.bits as usize,
|
||||
format!("Toggle {}", option.label()),
|
||||
Some(option.to_toggle_action()),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_nav_button(
|
||||
&self,
|
||||
icon: &'static str,
|
||||
direction: Direction,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let action: Box<dyn Action>;
|
||||
let tooltip;
|
||||
match direction {
|
||||
Direction::Prev => {
|
||||
action = Box::new(SelectPrevMatch);
|
||||
tooltip = "Select Previous Match";
|
||||
}
|
||||
Direction::Next => {
|
||||
action = Box::new(SelectNextMatch);
|
||||
tooltip = "Select Next Match";
|
||||
}
|
||||
};
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
|
||||
enum NavButton {}
|
||||
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.search.option_button.inactive_state().style_for(state);
|
||||
Label::new(icon, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, {
|
||||
move |_, this, cx| match direction {
|
||||
Direction::Prev => this.select_prev_match(&Default::default(), cx),
|
||||
Direction::Next => this.select_next_match(&Default::default(), cx),
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<NavButton>(
|
||||
direction as usize,
|
||||
tooltip.to_string(),
|
||||
Some(action),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_action_button(
|
||||
&self,
|
||||
icon: &'static str,
|
||||
|
@ -495,19 +489,29 @@ impl BufferSearchBar {
|
|||
let tooltip = "Select All Matches";
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let action_type_id = 0_usize;
|
||||
|
||||
let has_matches = self.active_match_index.is_some();
|
||||
let cursor_style = if has_matches {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::default()
|
||||
};
|
||||
enum ActionButton {}
|
||||
MouseEventHandler::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
|
||||
MouseEventHandler::new::<ActionButton, _>(action_type_id, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.search.action_button.style_for(state);
|
||||
let style = theme
|
||||
.search
|
||||
.action_button
|
||||
.in_state(has_matches)
|
||||
.style_for(state);
|
||||
Label::new(icon, style.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.select_all_matches(&SelectAllMatches, cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_cursor_style(cursor_style)
|
||||
.with_tooltip::<ActionButton>(
|
||||
action_type_id,
|
||||
tooltip.to_string(),
|
||||
|
@ -518,56 +522,29 @@ impl BufferSearchBar {
|
|||
.into_any()
|
||||
}
|
||||
|
||||
fn render_close_button(
|
||||
&self,
|
||||
theme: &theme::Search,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let tooltip = "Dismiss Buffer Search";
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
|
||||
enum CloseButton {}
|
||||
MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<CloseButton>(
|
||||
0,
|
||||
tooltip.to_string(),
|
||||
Some(Box::new(Dismiss)),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
|
||||
assert_ne!(
|
||||
mode,
|
||||
SearchMode::Semantic,
|
||||
"Semantic search is not supported in buffer search"
|
||||
);
|
||||
if mode == self.current_mode {
|
||||
return;
|
||||
}
|
||||
self.current_mode = mode;
|
||||
let _ = self.update_matches(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
|
||||
fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
|
||||
let mut propagate_action = true;
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
if search_bar.show(cx) {
|
||||
search_bar.search_suggested(cx);
|
||||
if action.focus {
|
||||
search_bar.select_query(cx);
|
||||
cx.focus_self();
|
||||
}
|
||||
if search_bar.deploy(action, cx) {
|
||||
propagate_action = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if propagate_action {
|
||||
cx.propagate_action();
|
||||
}
|
||||
|
@ -727,8 +704,9 @@ impl BufferSearchBar {
|
|||
self.active_match_index.take();
|
||||
active_searchable_item.clear_matches(cx);
|
||||
let _ = done_tx.send(());
|
||||
cx.notify();
|
||||
} else {
|
||||
let query = if self.search_options.contains(SearchOptions::REGEX) {
|
||||
let query = if self.current_mode == SearchMode::Regex {
|
||||
match SearchQuery::regex(
|
||||
query,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
|
@ -823,6 +801,26 @@ impl BufferSearchBar {
|
|||
let _ = self.search(&new_query, Some(self.search_options), cx);
|
||||
}
|
||||
}
|
||||
fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
|
||||
self.activate_search_mode(next_mode(&self.current_mode, false), cx);
|
||||
}
|
||||
fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
|
||||
let mut should_propagate = true;
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |bar, cx| {
|
||||
if bar.show(cx) {
|
||||
should_propagate = false;
|
||||
bar.cycle_mode(action, cx);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
if should_propagate {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
184
crates/search/src/history.rs
Normal file
184
crates/search/src/history.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
use smallvec::SmallVec;
|
||||
const SEARCH_HISTORY_LIMIT: usize = 20;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SearchHistory {
|
||||
history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl SearchHistory {
|
||||
pub fn add(&mut self, search_string: String) {
|
||||
if let Some(i) = self.selected {
|
||||
if search_string == self.history[i] {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(previously_searched) = self.history.last_mut() {
|
||||
if search_string.find(previously_searched.as_str()).is_some() {
|
||||
*previously_searched = search_string;
|
||||
self.selected = Some(self.history.len() - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.history.push(search_string);
|
||||
if self.history.len() > SEARCH_HISTORY_LIMIT {
|
||||
self.history.remove(0);
|
||||
}
|
||||
self.selected = Some(self.history.len() - 1);
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<&str> {
|
||||
let history_size = self.history.len();
|
||||
if history_size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected = self.selected?;
|
||||
if selected == history_size - 1 {
|
||||
return None;
|
||||
}
|
||||
let next_index = selected + 1;
|
||||
self.selected = Some(next_index);
|
||||
Some(&self.history[next_index])
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&str> {
|
||||
Some(&self.history[self.selected?])
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) -> Option<&str> {
|
||||
let history_size = self.history.len();
|
||||
if history_size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prev_index = match self.selected {
|
||||
Some(selected_index) => {
|
||||
if selected_index == 0 {
|
||||
return None;
|
||||
} else {
|
||||
selected_index - 1
|
||||
}
|
||||
}
|
||||
None => history_size - 1,
|
||||
};
|
||||
|
||||
self.selected = Some(prev_index);
|
||||
Some(&self.history[prev_index])
|
||||
}
|
||||
|
||||
pub fn reset_selection(&mut self) {
|
||||
self.selected = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
assert_eq!(
|
||||
search_history.current(),
|
||||
None,
|
||||
"No current selection should be set fo the default search history"
|
||||
);
|
||||
|
||||
search_history.add("rust".to_string());
|
||||
assert_eq!(
|
||||
search_history.current(),
|
||||
Some("rust"),
|
||||
"Newly added item should be selected"
|
||||
);
|
||||
|
||||
// check if duplicates are not added
|
||||
search_history.add("rust".to_string());
|
||||
assert_eq!(
|
||||
search_history.history.len(),
|
||||
1,
|
||||
"Should not add a duplicate"
|
||||
);
|
||||
assert_eq!(search_history.current(), Some("rust"));
|
||||
|
||||
// check if new string containing the previous string replaces it
|
||||
search_history.add("rustlang".to_string());
|
||||
assert_eq!(
|
||||
search_history.history.len(),
|
||||
1,
|
||||
"Should replace previous item if it's a substring"
|
||||
);
|
||||
assert_eq!(search_history.current(), Some("rustlang"));
|
||||
|
||||
// push enough items to test SEARCH_HISTORY_LIMIT
|
||||
for i in 0..SEARCH_HISTORY_LIMIT * 2 {
|
||||
search_history.add(format!("item{i}"));
|
||||
}
|
||||
assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_next_and_previous() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
assert_eq!(
|
||||
search_history.next(),
|
||||
None,
|
||||
"Default search history should not have a next item"
|
||||
);
|
||||
|
||||
search_history.add("Rust".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
search_history.add("JavaScript".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
search_history.add("TypeScript".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
|
||||
assert_eq!(search_history.previous(), Some("JavaScript"));
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
|
||||
assert_eq!(search_history.previous(), Some("Rust"));
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
assert_eq!(search_history.previous(), None);
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
assert_eq!(search_history.next(), Some("JavaScript"));
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
|
||||
assert_eq!(search_history.next(), Some("TypeScript"));
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
|
||||
assert_eq!(search_history.next(), None);
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_selection() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
search_history.add("Rust".to_string());
|
||||
search_history.add("JavaScript".to_string());
|
||||
search_history.add("TypeScript".to_string());
|
||||
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
search_history.reset_selection();
|
||||
assert_eq!(search_history.current(), None);
|
||||
assert_eq!(
|
||||
search_history.previous(),
|
||||
Some("TypeScript"),
|
||||
"Should start from the end after reset on previous item query"
|
||||
);
|
||||
|
||||
search_history.previous();
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
search_history.previous();
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
search_history.reset_selection();
|
||||
assert_eq!(search_history.current(), None);
|
||||
}
|
||||
}
|
88
crates/search/src/mode.rs
Normal file
88
crates/search/src/mode.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use gpui::Action;
|
||||
|
||||
use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
|
||||
// TODO: Update the default search mode to get from config
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub enum SearchMode {
|
||||
#[default]
|
||||
Text,
|
||||
Semantic,
|
||||
Regex,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub(crate) enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl SearchMode {
|
||||
pub(crate) fn label(&self) -> &'static str {
|
||||
match self {
|
||||
SearchMode::Text => "Text",
|
||||
SearchMode::Semantic => "Semantic",
|
||||
SearchMode::Regex => "Regex",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn region_id(&self) -> usize {
|
||||
match self {
|
||||
SearchMode::Text => 3,
|
||||
SearchMode::Semantic => 4,
|
||||
SearchMode::Regex => 5,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tooltip_text(&self) -> &'static str {
|
||||
match self {
|
||||
SearchMode::Text => "Activate Text Search",
|
||||
SearchMode::Semantic => "Activate Semantic Search",
|
||||
SearchMode::Regex => "Activate Regex Search",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn activate_action(&self) -> Box<dyn Action> {
|
||||
match self {
|
||||
SearchMode::Text => Box::new(ActivateTextMode),
|
||||
SearchMode::Semantic => Box::new(ActivateSemanticMode),
|
||||
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 {
|
||||
let next_text_state = if semantic_enabled {
|
||||
SearchMode::Semantic
|
||||
} else {
|
||||
SearchMode::Regex
|
||||
};
|
||||
|
||||
match mode {
|
||||
SearchMode::Text => next_text_state,
|
||||
SearchMode::Semantic => SearchMode::Regex,
|
||||
SearchMode::Regex => SearchMode::Text,
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,20 @@
|
|||
use bitflags::bitflags;
|
||||
pub use buffer_search::BufferSearchBar;
|
||||
use gpui::{actions, Action, AppContext};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Component, StyleableComponent, TooltipStyle},
|
||||
Action, AnyElement, AppContext, Element, View,
|
||||
};
|
||||
pub use mode::SearchMode;
|
||||
use project::search::SearchQuery;
|
||||
pub use project_search::{ProjectSearchBar, ProjectSearchView};
|
||||
use smallvec::SmallVec;
|
||||
use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle};
|
||||
|
||||
pub mod buffer_search;
|
||||
mod history;
|
||||
mod mode;
|
||||
pub mod project_search;
|
||||
pub(crate) mod search_bar;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
buffer_search::init(cx);
|
||||
|
@ -16,14 +24,17 @@ pub fn init(cx: &mut AppContext) {
|
|||
actions!(
|
||||
search,
|
||||
[
|
||||
CycleMode,
|
||||
ToggleWholeWord,
|
||||
ToggleCaseSensitive,
|
||||
ToggleRegex,
|
||||
SelectNextMatch,
|
||||
SelectPrevMatch,
|
||||
SelectAllMatches,
|
||||
NextHistoryQuery,
|
||||
PreviousHistoryQuery,
|
||||
ActivateTextMode,
|
||||
ActivateSemanticMode,
|
||||
ActivateRegexMode
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -33,7 +44,6 @@ bitflags! {
|
|||
const NONE = 0b000;
|
||||
const WHOLE_WORD = 0b001;
|
||||
const CASE_SENSITIVE = 0b010;
|
||||
const REGEX = 0b100;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +52,14 @@ impl SearchOptions {
|
|||
match *self {
|
||||
SearchOptions::WHOLE_WORD => "Match Whole Word",
|
||||
SearchOptions::CASE_SENSITIVE => "Match Case",
|
||||
SearchOptions::REGEX => "Use Regular Expression",
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> &'static str {
|
||||
match *self {
|
||||
SearchOptions::WHOLE_WORD => "icons/word_search_12.svg",
|
||||
SearchOptions::CASE_SENSITIVE => "icons/case_insensitive_12.svg",
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +68,6 @@ impl SearchOptions {
|
|||
match *self {
|
||||
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
|
||||
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
|
||||
SearchOptions::REGEX => Box::new(ToggleRegex),
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
}
|
||||
}
|
||||
|
@ -64,191 +80,24 @@ impl SearchOptions {
|
|||
let mut options = SearchOptions::NONE;
|
||||
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
|
||||
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
|
||||
options.set(SearchOptions::REGEX, query.is_regex());
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
const SEARCH_HISTORY_LIMIT: usize = 20;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SearchHistory {
|
||||
history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl SearchHistory {
|
||||
pub fn add(&mut self, search_string: String) {
|
||||
if let Some(i) = self.selected {
|
||||
if search_string == self.history[i] {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(previously_searched) = self.history.last_mut() {
|
||||
if search_string.find(previously_searched.as_str()).is_some() {
|
||||
*previously_searched = search_string;
|
||||
self.selected = Some(self.history.len() - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.history.push(search_string);
|
||||
if self.history.len() > SEARCH_HISTORY_LIMIT {
|
||||
self.history.remove(0);
|
||||
}
|
||||
self.selected = Some(self.history.len() - 1);
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<&str> {
|
||||
let history_size = self.history.len();
|
||||
if history_size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected = self.selected?;
|
||||
if selected == history_size - 1 {
|
||||
return None;
|
||||
}
|
||||
let next_index = selected + 1;
|
||||
self.selected = Some(next_index);
|
||||
Some(&self.history[next_index])
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&str> {
|
||||
Some(&self.history[self.selected?])
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) -> Option<&str> {
|
||||
let history_size = self.history.len();
|
||||
if history_size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prev_index = match self.selected {
|
||||
Some(selected_index) => {
|
||||
if selected_index == 0 {
|
||||
return None;
|
||||
} else {
|
||||
selected_index - 1
|
||||
}
|
||||
}
|
||||
None => history_size - 1,
|
||||
};
|
||||
|
||||
self.selected = Some(prev_index);
|
||||
Some(&self.history[prev_index])
|
||||
}
|
||||
|
||||
pub fn reset_selection(&mut self) {
|
||||
self.selected = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
assert_eq!(
|
||||
search_history.current(),
|
||||
None,
|
||||
"No current selection should be set fo the default search history"
|
||||
);
|
||||
|
||||
search_history.add("rust".to_string());
|
||||
assert_eq!(
|
||||
search_history.current(),
|
||||
Some("rust"),
|
||||
"Newly added item should be selected"
|
||||
);
|
||||
|
||||
// check if duplicates are not added
|
||||
search_history.add("rust".to_string());
|
||||
assert_eq!(
|
||||
search_history.history.len(),
|
||||
1,
|
||||
"Should not add a duplicate"
|
||||
);
|
||||
assert_eq!(search_history.current(), Some("rust"));
|
||||
|
||||
// check if new string containing the previous string replaces it
|
||||
search_history.add("rustlang".to_string());
|
||||
assert_eq!(
|
||||
search_history.history.len(),
|
||||
1,
|
||||
"Should replace previous item if it's a substring"
|
||||
);
|
||||
assert_eq!(search_history.current(), Some("rustlang"));
|
||||
|
||||
// push enough items to test SEARCH_HISTORY_LIMIT
|
||||
for i in 0..SEARCH_HISTORY_LIMIT * 2 {
|
||||
search_history.add(format!("item{i}"));
|
||||
}
|
||||
assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_next_and_previous() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
assert_eq!(
|
||||
search_history.next(),
|
||||
None,
|
||||
"Default search history should not have a next item"
|
||||
);
|
||||
|
||||
search_history.add("Rust".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
search_history.add("JavaScript".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
search_history.add("TypeScript".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
|
||||
assert_eq!(search_history.previous(), Some("JavaScript"));
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
|
||||
assert_eq!(search_history.previous(), Some("Rust"));
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
assert_eq!(search_history.previous(), None);
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
assert_eq!(search_history.next(), Some("JavaScript"));
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
|
||||
assert_eq!(search_history.next(), Some("TypeScript"));
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
|
||||
assert_eq!(search_history.next(), None);
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_selection() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
search_history.add("Rust".to_string());
|
||||
search_history.add("JavaScript".to_string());
|
||||
search_history.add("TypeScript".to_string());
|
||||
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
search_history.reset_selection();
|
||||
assert_eq!(search_history.current(), None);
|
||||
assert_eq!(
|
||||
search_history.previous(),
|
||||
Some("TypeScript"),
|
||||
"Should start from the end after reset on previous item query"
|
||||
);
|
||||
|
||||
search_history.previous();
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
search_history.previous();
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
search_history.reset_selection();
|
||||
assert_eq!(search_history.current(), None);
|
||||
pub fn as_button<V: View>(
|
||||
&self,
|
||||
active: bool,
|
||||
tooltip_style: TooltipStyle,
|
||||
button_style: ToggleIconButtonStyle,
|
||||
) -> AnyElement<V> {
|
||||
ActionButton::new_dynamic(
|
||||
self.to_toggle_action(),
|
||||
format!("Toggle {}", self.label()),
|
||||
tooltip_style,
|
||||
)
|
||||
.with_contents(theme::components::svg::Svg::new(self.icon()))
|
||||
.toggleable(active)
|
||||
.with_style(button_style)
|
||||
.element()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
|
201
crates/search/src/search_bar.rs
Normal file
201
crates/search/src/search_bar.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use gpui::{
|
||||
elements::{Label, MouseEventHandler, Svg},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
scene::{CornerRadii, MouseClick},
|
||||
Action, AnyElement, Element, EventContext, View, ViewContext,
|
||||
};
|
||||
use workspace::searchable::Direction;
|
||||
|
||||
use crate::{
|
||||
mode::{SearchMode, Side},
|
||||
SelectNextMatch, SelectPrevMatch,
|
||||
};
|
||||
|
||||
pub(super) fn render_close_button<V: View>(
|
||||
tooltip: &'static str,
|
||||
theme: &theme::Search,
|
||||
cx: &mut ViewContext<V>,
|
||||
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
|
||||
dismiss_action: Option<Box<dyn Action>>,
|
||||
) -> AnyElement<V> {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
|
||||
enum CloseButton {}
|
||||
MouseEventHandler::new::<CloseButton, _>(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.search_bar_row_height)
|
||||
})
|
||||
.on_click(MouseButton::Left, on_click)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<CloseButton>(0, tooltip.to_string(), dismiss_action, tooltip_style, cx)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub(super) fn render_nav_button<V: View>(
|
||||
icon: &'static str,
|
||||
direction: Direction,
|
||||
active: bool,
|
||||
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> AnyElement<V> {
|
||||
let action: Box<dyn Action>;
|
||||
let tooltip;
|
||||
|
||||
match direction {
|
||||
Direction::Prev => {
|
||||
action = Box::new(SelectPrevMatch);
|
||||
tooltip = "Select Previous Match";
|
||||
}
|
||||
Direction::Next => {
|
||||
action = Box::new(SelectNextMatch);
|
||||
tooltip = "Select Next Match";
|
||||
}
|
||||
};
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let cursor_style = if active {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::default()
|
||||
};
|
||||
enum NavButton {}
|
||||
MouseEventHandler::new::<NavButton, _>(direction as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme
|
||||
.search
|
||||
.nav_button
|
||||
.in_state(active)
|
||||
.style_for(state)
|
||||
.clone();
|
||||
let mut container_style = style.container.clone();
|
||||
let label = Label::new(icon, style.label.clone()).aligned().contained();
|
||||
container_style.corner_radii = match direction {
|
||||
Direction::Prev => CornerRadii {
|
||||
bottom_right: 0.,
|
||||
top_right: 0.,
|
||||
..container_style.corner_radii
|
||||
},
|
||||
Direction::Next => CornerRadii {
|
||||
bottom_left: 0.,
|
||||
top_left: 0.,
|
||||
..container_style.corner_radii
|
||||
},
|
||||
};
|
||||
if direction == Direction::Prev {
|
||||
// Remove right border so that when both Next and Prev buttons are
|
||||
// next to one another, there's no double border between them.
|
||||
container_style.border.right = false;
|
||||
}
|
||||
label.with_style(container_style)
|
||||
})
|
||||
.on_click(MouseButton::Left, on_click)
|
||||
.with_cursor_style(cursor_style)
|
||||
.with_tooltip::<NavButton>(
|
||||
direction as usize,
|
||||
tooltip.to_string(),
|
||||
Some(action),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub(crate) fn render_search_mode_button<V: View>(
|
||||
mode: SearchMode,
|
||||
is_active: bool,
|
||||
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> AnyElement<V> {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
enum SearchModeButton {}
|
||||
MouseEventHandler::new::<SearchModeButton, _>(mode.region_id(), cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let mut 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() {
|
||||
if button_side == Side::Left {
|
||||
container_style.corner_radii = CornerRadii {
|
||||
bottom_right: 0.,
|
||||
top_right: 0.,
|
||||
..container_style.corner_radii
|
||||
};
|
||||
label.with_style(container_style)
|
||||
} else {
|
||||
container_style.corner_radii = CornerRadii {
|
||||
bottom_left: 0.,
|
||||
top_left: 0.,
|
||||
..container_style.corner_radii
|
||||
};
|
||||
label.with_style(container_style)
|
||||
}
|
||||
} else {
|
||||
container_style.corner_radii = CornerRadii::default();
|
||||
label.with_style(container_style)
|
||||
}
|
||||
.constrained()
|
||||
.with_height(theme.search.search_bar_row_height)
|
||||
})
|
||||
.on_click(MouseButton::Left, on_click)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<SearchModeButton>(
|
||||
mode.region_id(),
|
||||
mode.tooltip_text().to_owned(),
|
||||
Some(mode.activate_action()),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub(crate) fn render_option_button_icon<V: View>(
|
||||
is_active: bool,
|
||||
icon: &'static str,
|
||||
id: usize,
|
||||
label: impl Into<Cow<'static, str>>,
|
||||
action: Box<dyn Action>,
|
||||
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> AnyElement<V> {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
MouseEventHandler::new::<V, _>(id, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme
|
||||
.search
|
||||
.option_button
|
||||
.in_state(is_active)
|
||||
.style_for(state);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color.clone())
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.search.option_button_height)
|
||||
.with_width(style.button_width)
|
||||
})
|
||||
.on_click(MouseButton::Left, on_click)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<V>(id, label, Some(action), tooltip_style, cx)
|
||||
.into_any()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue