diff --git a/Cargo.lock b/Cargo.lock index c052ace3eb..f1a64ec5fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -550,7 +550,6 @@ dependencies = [ "time", "tree-sitter-md", "ui", - "ui_input", "unicode-script", "unicode-segmentation", "unindent", @@ -14960,7 +14959,6 @@ dependencies = [ "task", "telemetry", "ui", - "ui_input", "util", "windows-registry 0.6.1", "workspace", diff --git a/assets/icons/file_ignored.svg b/assets/icons/file_ignored.svg new file mode 100644 index 0000000000..2c3228c6ad --- /dev/null +++ b/assets/icons/file_ignored.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_multiple.svg b/assets/icons/file_multiple.svg new file mode 100644 index 0000000000..739885eb65 --- /dev/null +++ b/assets/icons/file_multiple.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/keymaps/specific-overrides-macos.json b/assets/keymaps/specific-overrides-macos.json index f8e4e1fb06..68cfb32f9b 100644 --- a/assets/keymaps/specific-overrides-macos.json +++ b/assets/keymaps/specific-overrides-macos.json @@ -11,6 +11,7 @@ "use_key_equivalents": true, "bindings": { "cmd-shift-a": "picker::ToggleActionsMenu", + "cmd-shift-s": "picker::ToggleMultiSelect", }, }, { diff --git a/assets/keymaps/specific-overrides.json b/assets/keymaps/specific-overrides.json index 0f622554f7..494fd8d59d 100644 --- a/assets/keymaps/specific-overrides.json +++ b/assets/keymaps/specific-overrides.json @@ -10,6 +10,7 @@ "context": "Picker > Editor", "bindings": { "ctrl-shift-a": "picker::ToggleActionsMenu", + "ctrl-shift-s": "picker::ToggleMultiSelect", }, }, { diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 3ccf529738..bfabeac626 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -108,7 +108,6 @@ theme.workspace = true theme_settings.workspace = true time.workspace = true ui.workspace = true -ui_input.workspace = true unicode-script.workspace = true unicode-segmentation.workspace = true url.workspace = true diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index 51d2644ac7..a9c232c7fa 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -36,7 +36,6 @@ use ui::{ Scrollbars, Tab, ThreadItem, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height, }; -use ui_input::ErasedEditor; use util::ResultExt; use util::paths::PathExt; use workspace::{ @@ -1302,22 +1301,6 @@ impl PickerDelegate for ProjectPickerDelegate { .into() } - fn render_editor( - &self, - editor: &Arc, - window: &mut Window, - cx: &mut Context>, - ) -> Div { - h_flex() - .flex_none() - .h_9() - .px_2p5() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child(editor.render(window, cx)) - } - fn match_count(&self) -> usize { self.filtered_entries.len() } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f02fb67744..71c1d03efd 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -14,9 +14,9 @@ use file_icons::FileIcons; use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy_nucleo::{PathMatch, PathMatchCandidate}; use gpui::{ - Action, AnyElement, App, Context, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, - Focusable, KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, - TaskExt, WeakEntity, Window, actions, rems, + Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, TaskExt, + WeakEntity, Window, actions, rems, }; use language::{BufferSnapshot, Point}; use open_path_prompt::{ @@ -40,7 +40,7 @@ use std::{ }, time::Duration, }; -use ui::{HighlightedLabel, Indicator, ListItem, ListItemSpacing, Tooltip, prelude::*}; +use ui::{Checkbox, HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::{ ResultExt, maybe, paths::{PathStyle, PathWithPosition}, @@ -1768,12 +1768,9 @@ impl PickerDelegate for FileFinderDelegate { "Include Ignored Files" }; - let filter_button = IconButton::new("filter-ignored", IconName::Sliders) + let filter_button = IconButton::new("filter-ignored", IconName::FileIgnored) .icon_size(IconSize::Small) .toggle_state(including_ignored) - .when(self.include_ignored.is_some(), |this| { - this.indicator(Indicator::dot().color(Color::Info)) - }) .tooltip(move |_window, cx| { Tooltip::for_action_in(tooltip_label, &ToggleIncludeIgnored, &focus_handle, cx) }) @@ -2034,65 +2031,18 @@ impl PickerDelegate for FileFinderDelegate { window: &mut Window, cx: &mut Context>, ) -> Option { - let settings = FileFinderSettings::get_global(cx); + self.render_match_impl(ix, selected, None, window, cx) + } - let path_match = self.matches.get(ix)?; - - let end_icon = match path_match { - Match::History { .. } => Icon::new(IconName::HistoryRerun) - .color(Color::Muted) - .size(IconSize::Small) - .into_any_element(), - Match::Search(_) => v_flex() - .flex_none() - .size(IconSize::Small.rems()) - .into_any_element(), - Match::Channel { .. } => v_flex() - .flex_none() - .size(IconSize::Small.rems()) - .into_any_element(), - Match::CreateNew(_) => Empty.into_any_element(), - }; - - let is_create_new = matches!(path_match, Match::CreateNew(_)); - - let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx); - - let file_icon = match path_match { - Match::Channel { .. } => Some(Icon::new(IconName::Hash).color(Color::Muted)), - _ => maybe!({ - if !settings.file_icons { - return None; - } - let abs_path = path_match.abs_path(&self.project, cx)?; - let file_name = abs_path.file_name()?; - let icon = FileIcons::get_icon(file_name.as_ref(), cx)?; - Some(Icon::from_path(icon).color(Color::Muted)) - }), - }; - - Some( - ListItem::new(ix) - .spacing(ListItemSpacing::Sparse) - .inset(true) - .toggle_state(selected) - .map(|this| { - if is_create_new { - this.start_slot(Icon::new(IconName::Plus).size(IconSize::Small)) - } else { - this.start_slot::(file_icon) - } - }) - .child( - h_flex() - .w_full() - .min_w_0() - .gap_1p5() - .child(file_name_label.truncate_middle()) - .child(full_path_label.truncate_start()), - ) - .end_slot::(end_icon), - ) + fn render_match_with_checkbox( + &self, + ix: usize, + selected: bool, + checkbox: AnyElement, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + self.render_match_impl(ix, selected, Some(checkbox), window, cx) } fn actions_menu( @@ -2117,6 +2067,92 @@ impl PickerDelegate for FileFinderDelegate { } } +impl FileFinderDelegate { + fn render_match_impl( + &self, + ix: usize, + selected: bool, + checkbox: Option, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + let settings = FileFinderSettings::get_global(cx); + + let path_match = self.matches.get(ix)?; + + let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx); + + let start_icon = match path_match { + Match::CreateNew(_) => Some(Icon::new(IconName::Plus).size(IconSize::Small)), + Match::Channel { .. } => Some(Icon::new(IconName::Hash).color(Color::Muted)), + _ => maybe!({ + if !settings.file_icons { + return None; + } + let abs_path = path_match.abs_path(&self.project, cx)?; + let file_name = abs_path.file_name()?; + let icon = FileIcons::get_icon(file_name.as_ref(), cx)?; + Some(Icon::from_path(icon).color(Color::Muted)) + }), + }; + + let checkbox = checkbox.map(|checkbox| { + if matches!(path_match, Match::CreateNew(_) | Match::Channel { .. }) { + div() + .flex_none() + .size(Checkbox::container_size()) + .into_any_element() + } else { + checkbox + } + }); + + let start_slot: Option = match (checkbox, start_icon) { + (Some(checkbox), icon) => Some( + h_flex() + .gap_1p5() + .child(checkbox) + .children(icon) + .into_any_element(), + ), + (None, icon) => icon.map(IntoElement::into_any_element), + }; + + let end_slot: Option = match path_match { + Match::History { .. } => Some( + Icon::new(IconName::HistoryRerun) + .color(Color::Muted) + .size(IconSize::Small) + .into_any_element(), + ), + Match::Search(_) | Match::Channel { .. } => Some( + div() + .flex_none() + .size(IconSize::Small.rems()) + .into_any_element(), + ), + Match::CreateNew(_) => None, + }; + + Some( + ListItem::new(ix) + .spacing(ListItemSpacing::Sparse) + .inset(true) + .toggle_state(selected) + .start_slot::(start_slot) + .child( + h_flex() + .w_full() + .min_w_0() + .gap_1p5() + .child(file_name_label.truncate_middle()) + .child(full_path_label.truncate_start()), + ) + .end_slot::(end_slot), + ) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] struct PathComponentSlice<'a> { path: Cow<'a, Path>, diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 61ed9987a2..9cc86df9a7 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -1201,79 +1201,81 @@ impl PickerDelegate for BranchListDelegate { editor: &Arc, _window: &mut Window, cx: &mut Context>, - ) -> Div { + ) -> Option
{ let picker = cx.weak_entity(); let editor = editor.as_any().downcast_ref::>().unwrap(); let show_inline_filter = self.editor_position() == PickerEditorPosition::End || !self.show_footer; - v_flex() - .when( - self.editor_position() == PickerEditorPosition::End, - |this| this.child(Divider::horizontal()), - ) - .when_some(self.branch_list_error.clone(), |this, error| { - let message = format!("Some branches could not be loaded: {error}"); - this.child( - div() - .id("branch-list-error") - .p_1p5() - .child( - Banner::new().severity(Severity::Warning).child( - Label::new(message.clone()) - .size(LabelSize::Small) - .single_line() - .truncate(), - ), - ) - .tooltip(Tooltip::text(message)), + Some( + v_flex() + .when( + self.editor_position() == PickerEditorPosition::End, + |this| this.child(Divider::horizontal()), ) - }) - .child( - h_flex() - .overflow_hidden() - .flex_none() - .h_9() - .px_2p5() - .child(editor.clone()) - .when(show_inline_filter, |this| { - let branch_filter = self.branch_filter; - let picker_for_menu_open = picker.clone(); - this.gap_1().justify_between().child( - PopoverMenu::new("branch-filter-menu") - .with_handle(self.branch_filter_menu_handle.clone()) - .on_open(Rc::new(move |_, cx| { - picker_for_menu_open - .update(cx, |picker, _| { - picker.delegate.branch_filter_menu_open = true; - }) - .log_err(); - })) - .trigger( - IconButton::new("branch-filter", IconName::ListFilter) - .toggle_state(branch_filter != BranchFilter::All) - .when(branch_filter != BranchFilter::All, |this| { - this.indicator(Indicator::dot().color(Color::Info)) - }) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Filter branches")), - ) - .menu(move |window, cx| { - Some(branch_filter_menu( - branch_filter, - picker.clone(), - window, - cx, - )) - }), - ) - }), - ) - .when( - self.editor_position() == PickerEditorPosition::Start, - |this| this.child(Divider::horizontal()), - ) + .when_some(self.branch_list_error.clone(), |this, error| { + let message = format!("Some branches could not be loaded: {error}"); + this.child( + div() + .id("branch-list-error") + .p_1p5() + .child( + Banner::new().severity(Severity::Warning).child( + Label::new(message.clone()) + .size(LabelSize::Small) + .single_line() + .truncate(), + ), + ) + .tooltip(Tooltip::text(message)), + ) + }) + .child( + h_flex() + .overflow_hidden() + .flex_none() + .h_9() + .px_2p5() + .child(editor.clone()) + .when(show_inline_filter, |this| { + let branch_filter = self.branch_filter; + let picker_for_menu_open = picker.clone(); + this.gap_1().justify_between().child( + PopoverMenu::new("branch-filter-menu") + .with_handle(self.branch_filter_menu_handle.clone()) + .on_open(Rc::new(move |_, cx| { + picker_for_menu_open + .update(cx, |picker, _| { + picker.delegate.branch_filter_menu_open = true; + }) + .log_err(); + })) + .trigger( + IconButton::new("branch-filter", IconName::ListFilter) + .toggle_state(branch_filter != BranchFilter::All) + .when(branch_filter != BranchFilter::All, |this| { + this.indicator(Indicator::dot().color(Color::Info)) + }) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Filter branches")), + ) + .menu(move |window, cx| { + Some(branch_filter_menu( + branch_filter, + picker.clone(), + window, + cx, + )) + }), + ) + }), + ) + .when( + self.editor_position() == PickerEditorPosition::Start, + |this| this.child(Divider::horizontal()), + ), + ) } fn editor_position(&self) -> PickerEditorPosition { diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 3e22896e32..3f015608b3 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -129,8 +129,10 @@ pub enum IconName { FileDoc, FileGeneric, FileGit, + FileIgnored, FileLock, FileMarkdown, + FileMultiple, FileRust, FileTextFilled, FileTextOutlined, diff --git a/crates/picker/src/footer.rs b/crates/picker/src/footer.rs index 6c0d8c8bb0..7e94e04ecf 100644 --- a/crates/picker/src/footer.rs +++ b/crates/picker/src/footer.rs @@ -8,7 +8,6 @@ use crate::PickerDelegate; use crate::SetPreviewBelow; use crate::SetPreviewRight; use crate::ToggleActionsMenu; -use crate::ToggleMultiSelect; use crate::TogglePreview; use crate::preview; @@ -98,18 +97,8 @@ impl Picker { window: &mut Window, cx: &mut Context, ) -> Option { - let mut actions = self.delegate.actions_menu(window, cx); - // The multi-select entry is picker-owned so it can reflect the mode, - // which delegates don't know about. - if self.delegate.supports_multi_select() { - if !actions.is_empty() { - actions.push(PickerAction::separator()); - } - actions.push( - PickerAction::button("Multi Select", ToggleMultiSelect.boxed_clone()) - .toggled(self.select_instead_of_open), - ); - } + let actions = self.delegate.actions_menu(window, cx); + if self.preview.is_none() && actions.is_empty() { return None; } @@ -123,9 +112,9 @@ impl Picker { .justify_between() .border_t_1() .border_color(cx.theme().colors().border_variant) - .child(div().when(self.preview.is_some(), |this| { + .when(self.preview.is_some(), |this| { this.child(self.render_preview_controls(window, cx)) - })) + }) .when(!actions.is_empty(), |this| { this.child(self.render_actions_button(actions.into(), focus_handle, window, cx)) }) @@ -206,11 +195,8 @@ impl Picker { _window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let _ = cx; PopoverMenu::new("picker-actions-menu") .with_handle(self.actions_menu_handle.clone()) - .attach(gpui::Anchor::TopRight) - .anchor(gpui::Anchor::BottomRight) .trigger( Button::new("picker-actions-trigger", "Actions…") .key_binding( @@ -230,5 +216,11 @@ impl Picker { menu })) }) + .attach(gpui::Anchor::TopRight) + .anchor(gpui::Anchor::BottomRight) + .offset(gpui::Point { + x: px(0.0), + y: px(-2.0), + }) } } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index dbc8f22160..f3c8b0a268 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -11,7 +11,10 @@ use serde::Deserialize; use std::{ cell::Cell, cell::RefCell, collections::HashMap, ops::Range, rc::Rc, sync::Arc, time::Duration, }; -use ui::{ContextMenu, Divider, DocumentationAside, PopoverMenuHandle, prelude::*, v_flex}; +use ui::{ + Checkbox, ContextMenu, Divider, DocumentationAside, KeyBinding, PopoverMenuHandle, Tooltip, + prelude::*, +}; use ui_input::ErasedEditorEvent; use util::ResultExt; use workspace::ModalView; @@ -323,30 +326,17 @@ pub trait PickerDelegate: Sized + 'static { None } + /// Overrides the search bar entirely. Most delegates should return `None` + /// to get the picker-rendered default (which includes + /// [`Self::searchbar_trailer`] and the multi-select toggle); override for + /// full control over the search bar. fn render_editor( &self, - editor: &Arc, - window: &mut Window, - cx: &mut Context>, - ) -> Div { - v_flex() - .when( - self.editor_position() == PickerEditorPosition::End, - |this| this.child(Divider::horizontal()), - ) - .child( - h_flex() - .overflow_hidden() - .flex_none() - .h_9() - .px_2p5() - .child(div().flex_1().child(editor.render(window, cx))) - .children(self.searchbar_trailer(window, cx)), - ) - .when( - self.editor_position() == PickerEditorPosition::Start, - |this| this.child(Divider::horizontal()), - ) + _editor: &Arc, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option
{ + None } fn try_get_preview_data_for_match(&self, _cx: &App) -> Option { @@ -365,6 +355,17 @@ pub trait PickerDelegate: Sized + 'static { cx: &mut Context>, ) -> Option; + fn render_match_with_checkbox( + &self, + _ix: usize, + _selected: bool, + _checkbox: AnyElement, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option { + None + } + fn render_header( &self, _window: &mut Window, @@ -922,6 +923,7 @@ impl Picker { cx: &mut Context, ) { if !self.delegate.supports_multi_select() { + cx.propagate(); return; } self.select_instead_of_open = !self.select_instead_of_open; @@ -1263,12 +1265,55 @@ impl Picker { let is_multi_selected = supports_multi_select && self.delegate.is_item_selected(ix); let multi_select_active = supports_multi_select && self.select_instead_of_open; + let item_with_checkbox = if multi_select_active && selectable { + let checkbox = self + .render_multi_select_indicator(ix, is_multi_selected, cx) + .into_any_element(); + self.delegate.render_match_with_checkbox( + ix, + ix == self.delegate.selected_index(), + checkbox, + window, + cx, + ) + } else { + None + }; + + let use_fallback_indicator = + multi_select_active && selectable && item_with_checkbox.is_none(); + let focus_handle = self.focus_handle(cx); + div() .id(("item", ix)) .when(selectable, |this| this.cursor_pointer()) - .when(selectable && multi_select_active, |this| { + .when(use_fallback_indicator, |this| { this.hover(|s| s.bg(cx.theme().colors().ghost_element_hover)) }) + .when(multi_select_active && selectable, |this| { + this.tooltip(Tooltip::element(move |_window, cx| { + h_flex() + .gap_2() + .child( + h_flex() + .gap_1() + .child(KeyBinding::for_action_in( + &MultiSelectNext, + &focus_handle, + cx, + )) + .child(Label::new("Select")), + ) + .child(Divider::vertical()) + .child( + h_flex() + .gap_1() + .child(KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)) + .child(Label::new("Open")), + ) + .into_any_element() + })) + }) .child( canvas( move |bounds, _window, _cx| { @@ -1310,17 +1355,19 @@ impl Picker { })) }) .map(|row| { - // Pickers without multi-select keep their element tree - // unchanged. - if supports_multi_select { + if let Some(item) = item_with_checkbox { + row.child(item) + } else if supports_multi_select { row.child( h_flex() // Headers and separators cannot be part of the // selection, so they get no indicator. - .when(multi_select_active && selectable, |this| { - this.child( - self.render_multi_select_indicator(is_multi_selected, cx), - ) + .when(use_fallback_indicator, |this| { + this.child(self.render_multi_select_indicator( + ix, + is_multi_selected, + cx, + )) }) .children(self.delegate.render_match( ix, @@ -1349,39 +1396,21 @@ impl Picker { ) } - /// The checkbox in front of items while in multi-select mode. fn render_multi_select_indicator( &self, + ix: usize, is_selected: bool, cx: &mut Context, ) -> impl IntoElement { - h_flex() - .w_6() - .flex_none() - .justify_center() - .items_center() - .child( - div() - .size_4() - .flex_none() - .rounded_sm() - .border_1() - .border_color(if is_selected { - cx.theme().colors().border_focused - } else { - cx.theme().colors().border - }) - .flex() - .items_center() - .justify_center() - .when(is_selected, |this| { - this.bg(cx.theme().colors().element_selected).child( - Icon::new(IconName::Check) - .size(IconSize::Small) - .color(Color::Accent), - ) - }), - ) + Checkbox::new(("picker-multi-select-checkbox", ix), is_selected.into()) + .fill() + .elevation(ui::ElevationIndex::ModalSurface) + .on_click(cx.listener(move |this, _: &ui::ToggleState, window, cx| { + // Don't let the row's click handler also toggle the item. + cx.stop_propagation(); + this.delegate.toggle_item_selected(ix, window, cx); + cx.notify(); + })) } fn render_element_container(&self, cx: &mut Context) -> impl IntoElement { diff --git a/crates/picker/src/render.rs b/crates/picker/src/render.rs index 499b0982ee..e61425466c 100644 --- a/crates/picker/src/render.rs +++ b/crates/picker/src/render.rs @@ -10,12 +10,17 @@ use ui::{ use crate::shape::Shape; use crate::{ - ElementContainer, Picker, PickerDelegate, PickerEditorPosition, Preview, + ElementContainer, Picker, PickerDelegate, PickerEditorPosition, Preview, ToggleMultiSelect, head::Head, preview::Layout, render::window_controls::{Bottom, Left, LeftCorner, Middle, Right, RightCorner}, }; use crate::{persistence, preview}; +use gpui::Action as _; +use gpui::Focusable as _; +use std::sync::Arc; +use ui::{Divider, Tooltip, prelude::*}; +use ui_input::ErasedEditor; pub mod window_controls; @@ -88,6 +93,54 @@ impl Render for Picker { } impl Picker { + fn render_editor( + &self, + editor: &Arc, + window: &mut Window, + cx: &mut Context, + ) -> gpui::Div { + if let Some(custom) = self.delegate.render_editor(editor, window, cx) { + return custom; + } + let editor_position = self.delegate.editor_position(); + + v_flex() + .when(editor_position == PickerEditorPosition::End, |this| { + this.child(Divider::horizontal()) + }) + .child( + h_flex() + .h_9() + .px_2p5() + .flex_none() + .overflow_hidden() + .child(div().flex_1().child(editor.render(window, cx))) + .children(self.delegate.searchbar_trailer(window, cx)) + .when(self.delegate.supports_multi_select(), |this| { + this.child(self.render_multi_select_toggle(cx)) + }), + ) + .when(editor_position == PickerEditorPosition::Start, |this| { + this.child(Divider::horizontal()) + }) + } + + /// The multi-select toggle is picker-owned so it can reflect the mode, + /// which delegates don't know about. + fn render_multi_select_toggle(&self, cx: &mut Context) -> impl IntoElement { + let active = self.select_instead_of_open; + let focus_handle = self.focus_handle(cx); + IconButton::new("picker-multi-select-toggle", IconName::FileMultiple) + .icon_size(IconSize::Small) + .toggle_state(active) + .tooltip(move |_window, cx| { + Tooltip::for_action_in("Toggle Multi Select", &ToggleMultiSelect, &focus_handle, cx) + }) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(ToggleMultiSelect.boxed_clone(), cx); + })) + } + pub(crate) fn render_results( &self, window: &mut Window, @@ -154,13 +207,14 @@ impl Picker { .children(match &self.head { Head::Editor(editor) => { if editor_position == PickerEditorPosition::Start { - Some(h_flex().w_full().child( - div().flex_1().child(self.delegate.render_editor( - &editor.clone(), - window, - cx, - )), - )) + let editor = editor.clone(); + Some( + h_flex().w_full().child( + div() + .flex_1() + .child(self.render_editor(&editor, window, cx)), + ), + ) } else { None } @@ -220,7 +274,8 @@ impl Picker { .children(match &self.head { Head::Editor(editor) => { if editor_position == PickerEditorPosition::End { - Some(self.delegate.render_editor(&editor.clone(), window, cx)) + let editor = editor.clone(); + Some(self.render_editor(&editor, window, cx)) } else { None } diff --git a/crates/picker/src/shape.rs b/crates/picker/src/shape.rs index 217ff6a8fc..034834ef78 100644 --- a/crates/picker/src/shape.rs +++ b/crates/picker/src/shape.rs @@ -1,6 +1,6 @@ use gpui::Window; use gpui::{Pixels, Rems, Size}; -use ui::{Div, Styled}; +use ui::{Div, Styled, rems_from_px}; use crate::preview::Layout; @@ -242,12 +242,12 @@ impl Default for SizeBounds { // over the lower bar so clear another 5 rems there. max_height: (RelativeHeight::FULL - Rems(10.0)) * 0.95, min_results: Size { - width: Rems(15.0), - height: Rems(20.0), + width: rems_from_px(280.), + height: rems_from_px(320.), }, min_preview: Size { - width: Rems(8.0), - height: Rems(6.0), + width: rems_from_px(128.), + height: rems_from_px(96.), }, } } diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 013e00e964..99a563aff4 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -47,7 +47,6 @@ settings.workspace = true task.workspace = true telemetry.workspace = true ui.workspace = true -ui_input.workspace = true util.workspace = true workspace.workspace = true worktree.workspace = true diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 781d85c874..369cff1cc6 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -36,7 +36,6 @@ use project::{Worktree, git_store::Repository}; pub use remote_connections::RemoteSettings; pub use remote_servers::RemoteServerProjects; use settings::{DefaultOpenBehavior, Settings, WorktreeId}; -use ui_input::ErasedEditor; use workspace::ProjectGroupKey; use dev_container::{DevContainerContext, find_devcontainer_configs}; @@ -938,22 +937,6 @@ impl PickerDelegate for RecentProjectsDelegate { "Search projects…".into() } - fn render_editor( - &self, - editor: &Arc, - window: &mut Window, - cx: &mut Context>, - ) -> Div { - h_flex() - .flex_none() - .h_9() - .px_2p5() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child(editor.render(window, cx)) - } - fn match_count(&self) -> usize { self.filtered_entries.len() } diff --git a/crates/recent_projects/src/sidebar_recent_projects.rs b/crates/recent_projects/src/sidebar_recent_projects.rs index 8955e63793..006fb7f35d 100644 --- a/crates/recent_projects/src/sidebar_recent_projects.rs +++ b/crates/recent_projects/src/sidebar_recent_projects.rs @@ -12,7 +12,6 @@ use picker::{ use remote::RemoteConnectionOptions; use settings::Settings; use ui::{ButtonLike, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; -use ui_input::ErasedEditor; use util::{ResultExt, paths::PathExt}; use workspace::{ MultiWorkspace, OpenMode, OpenOptions, ProjectGroupKey, RecentWorkspace, @@ -144,22 +143,6 @@ impl PickerDelegate for SidebarRecentProjectsDelegate { "Search projects…".into() } - fn render_editor( - &self, - editor: &Arc, - window: &mut Window, - cx: &mut Context>, - ) -> Div { - h_flex() - .flex_none() - .h_9() - .px_2p5() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child(editor.render(window, cx)) - } - fn match_count(&self) -> usize { self.filtered_workspaces.len() } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index dc71ec5c4a..cef989dbdf 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -5,8 +5,7 @@ use editor::SearchSettings; use gpui::{Action, App, ClickEvent, Entity, FocusHandle, IntoElement, actions}; use project::search::SearchQuery; pub use project_search::ProjectSearchView; -use ui::{ButtonStyle, IconButton, IconButtonShape}; -use ui::{Tooltip, prelude::*}; +use ui::{IconButtonShape, Tooltip, prelude::*}; use util::paths::PathMatcher; use workspace::notifications::NotificationId; use workspace::{Toast, Workspace}; @@ -115,10 +114,10 @@ impl SearchOption { pub fn icon(&self) -> ui::IconName { match self { - SearchOption::WholeWord => ui::IconName::WholeWord, - SearchOption::CaseSensitive => ui::IconName::CaseSensitive, - SearchOption::IncludeIgnored => ui::IconName::Sliders, - SearchOption::Regex => ui::IconName::Regex, + SearchOption::WholeWord => IconName::WholeWord, + SearchOption::CaseSensitive => IconName::CaseSensitive, + SearchOption::IncludeIgnored => IconName::FileIgnored, + SearchOption::Regex => IconName::Regex, _ => panic!("{self:?} is not a named SearchOption"), } } @@ -141,6 +140,7 @@ impl SearchOption { ) -> impl IntoElement { let action = self.to_toggle_action(); let label = self.label(); + IconButton::new( (label, matches!(search_source, SearchSource::Buffer) as u32), self.icon(), @@ -162,7 +162,6 @@ impl SearchOption { })) } }) - .style(ButtonStyle::Subtle) .shape(IconButtonShape::Square) .toggle_state(active.contains(self.as_options())) .tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx)) diff --git a/crates/search/src/text_finder/delegate.rs b/crates/search/src/text_finder/delegate.rs index 195223782f..8acc4338d4 100644 --- a/crates/search/src/text_finder/delegate.rs +++ b/crates/search/src/text_finder/delegate.rs @@ -45,8 +45,8 @@ use smol::future::yield_now; use text::Anchor; use theme_settings::ThemeSettings; use ui::{ - Disclosure, Divider, FluentBuilder, IconButtonShape, ListItem, ListItemSpacing, Toggleable, - Tooltip, prelude::*, text_for_keystroke, + Disclosure, Divider, FluentBuilder, ListItem, ListItemSpacing, Toggleable, Tooltip, prelude::*, + text_for_keystroke, }; use util::ResultExt; use workspace::SplitDirection; @@ -719,12 +719,12 @@ impl PickerDelegate for Delegate { let label = option.label(); let focus_handle = focus_handle.clone(); let picker = picker.clone(); + IconButton::new( ("text-finder-search-option", option as usize), option.icon(), ) .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) .toggle_state(active.contains(options)) .tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx)) .on_click(move |_, window, cx| { @@ -737,8 +737,9 @@ impl PickerDelegate for Delegate { Some( h_flex() - .gap_1() + .gap_px() .children(filter_buttons) + .child(Divider::vertical().ml_px().mr_0p5()) .children(picker::parts::project_scan_indicator( self.active_query.is_some(), self.project(cx), @@ -772,7 +773,7 @@ impl PickerDelegate for Delegate { picker::PickerAction::separator(), picker::PickerAction::button( if self.selected_matches.len() > 1 { - "Open multiple" + "Open Multiple" } else { "Open File" }, @@ -1009,9 +1010,33 @@ impl PickerDelegate for Delegate { &self, ix: usize, selected: bool, - _: &mut Window, + window: &mut Window, cx: &mut Context>, ) -> Option { + self.render_match_impl(ix, selected, None, window, cx) + } + + fn render_match_with_checkbox( + &self, + ix: usize, + selected: bool, + checkbox: AnyElement, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + self.render_match_impl(ix, selected, Some(checkbox), window, cx) + } +} + +impl Delegate { + fn render_match_impl( + &self, + ix: usize, + selected: bool, + checkbox: Option, + _: &mut Window, + cx: &mut Context>, + ) -> Option { match self.entries.get(ix)? { Entry::Separator => Some( div() @@ -1129,6 +1154,7 @@ impl PickerDelegate for Delegate { .spacing(ListItemSpacing::Sparse) .inset(true) .toggle_state(selected) + .when_some(checkbox, |this, checkbox| this.start_slot(checkbox)) .child( h_flex() .w_full()