Merge pull request #1854 from zed-industries/command-palette-improvements

Antonio Scandurra created

Improve styling of command palettes

Change summary

Cargo.lock                                    |  1 
crates/collab_ui/src/contact_finder.rs        |  4 
crates/collab_ui/src/contact_list.rs          |  4 
crates/command_palette/src/command_palette.rs |  2 
crates/editor/src/blink_manager.rs            |  3 
crates/editor/src/editor.rs                   | 19 ++--
crates/editor/src/element.rs                  |  2 
crates/file_finder/src/file_finder.rs         |  2 
crates/go_to_line/src/go_to_line.rs           | 11 +
crates/outline/src/outline.rs                 |  4 
crates/picker/Cargo.toml                      |  2 
crates/picker/src/picker.rs                   | 87 ++++++++++++++------
crates/project_panel/src/project_panel.rs     |  5 
crates/project_symbols/src/project_symbols.rs |  2 
crates/search/src/buffer_search.rs            |  8 +
crates/search/src/project_search.rs           |  7 +
crates/theme/src/theme.rs                     |  6 
crates/theme_selector/src/theme_selector.rs   |  2 
styles/src/styleTree/contactFinder.ts         | 49 ++++++-----
styles/src/styleTree/contactList.ts           |  2 
styles/src/styleTree/picker.ts                | 47 +++++++----
21 files changed, 171 insertions(+), 98 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4012,6 +4012,7 @@ dependencies = [
  "env_logger",
  "gpui",
  "menu",
+ "parking_lot 0.11.2",
  "serde_json",
  "settings",
  "theme",

crates/collab_ui/src/contact_finder.rs 🔗

@@ -170,8 +170,8 @@ impl ContactFinder {
         let this = cx.weak_handle();
         Self {
             picker: cx.add_view(|cx| {
-                Picker::new(this, cx)
-                    .with_theme(|cx| &cx.global::<Settings>().theme.contact_finder.picker)
+                Picker::new("Search collaborator by username...", this, cx)
+                    .with_theme(|theme| theme.contact_finder.picker.clone())
             }),
             potential_contacts: Arc::from([]),
             user_store,

crates/collab_ui/src/contact_list.rs 🔗

@@ -175,7 +175,9 @@ impl ContactList {
     ) -> Self {
         let filter_editor = cx.add_view(|cx| {
             let mut editor = Editor::single_line(
-                Some(|theme| theme.contact_list.user_query_editor.clone()),
+                Some(Arc::new(|theme| {
+                    theme.contact_list.user_query_editor.clone()
+                })),
                 cx,
             );
             editor.set_placeholder_text("Filter contacts", cx);

crates/command_palette/src/command_palette.rs 🔗

@@ -70,7 +70,7 @@ impl CommandPalette {
             })
             .collect();
 
-        let picker = cx.add_view(|cx| Picker::new(this, cx));
+        let picker = cx.add_view(|cx| Picker::new("Execute a command...", this, cx));
         Self {
             picker,
             actions,
@@ -93,6 +93,9 @@ impl BlinkManager {
 
     pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
         self.enabled = true;
+        // Set cursors as invisible and start blinking: this causes cursors
+        // to be visible during the next render.
+        self.visible = false;
         self.blink_cursors(self.blink_epoch, cx);
     }
 

crates/editor/src/editor.rs 🔗

@@ -437,8 +437,7 @@ pub struct EditorStyle {
 
 type CompletionId = usize;
 
-pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor;
-
+type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
 type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
 #[derive(Clone, Copy)]
@@ -523,7 +522,7 @@ pub struct Editor {
     scroll_top_anchor: Anchor,
     autoscroll_request: Option<(Autoscroll, bool)>,
     soft_wrap_mode_override: Option<settings::SoftWrap>,
-    get_field_editor_theme: Option<GetFieldEditorTheme>,
+    get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
     override_text_style: Option<Box<OverrideTextStyle>>,
     project: Option<ModelHandle<Project>>,
     focused: bool,
@@ -1070,7 +1069,7 @@ enum GotoDefinitionKind {
 
 impl Editor {
     pub fn single_line(
-        field_editor_style: Option<GetFieldEditorTheme>,
+        field_editor_style: Option<Arc<GetFieldEditorTheme>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
@@ -1080,7 +1079,7 @@ impl Editor {
 
     pub fn auto_height(
         max_lines: usize,
-        field_editor_style: Option<GetFieldEditorTheme>,
+        field_editor_style: Option<Arc<GetFieldEditorTheme>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
@@ -1116,7 +1115,7 @@ impl Editor {
             self.mode,
             self.buffer.clone(),
             self.project.clone(),
-            self.get_field_editor_theme,
+            self.get_field_editor_theme.clone(),
             cx,
         );
         self.display_map.update(cx, |display_map, cx| {
@@ -1136,12 +1135,12 @@ impl Editor {
         mode: EditorMode,
         buffer: ModelHandle<MultiBuffer>,
         project: Option<ModelHandle<Project>>,
-        get_field_editor_theme: Option<GetFieldEditorTheme>,
+        get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let display_map = cx.add_model(|cx| {
             let settings = cx.global::<Settings>();
-            let style = build_style(&*settings, get_field_editor_theme, None, cx);
+            let style = build_style(&*settings, get_field_editor_theme.as_deref(), None, cx);
             DisplayMap::new(
                 buffer.clone(),
                 style.text.font_id,
@@ -1289,7 +1288,7 @@ impl Editor {
     fn style(&self, cx: &AppContext) -> EditorStyle {
         build_style(
             cx.global::<Settings>(),
-            self.get_field_editor_theme,
+            self.get_field_editor_theme.as_deref(),
             self.override_text_style.as_deref(),
             cx,
         )
@@ -6846,7 +6845,7 @@ impl View for Editor {
 
 fn build_style(
     settings: &Settings,
-    get_field_editor_theme: Option<GetFieldEditorTheme>,
+    get_field_editor_theme: Option<&GetFieldEditorTheme>,
     override_text_style: Option<&OverrideTextStyle>,
     cx: &AppContext,
 ) -> EditorStyle {

crates/editor/src/element.rs 🔗

@@ -1186,7 +1186,7 @@ impl EditorElement {
         }
 
         // When the editor is empty and unfocused, then show the placeholder.
-        if snapshot.is_empty() && !snapshot.is_focused() {
+        if snapshot.is_empty() {
             let placeholder_style = self
                 .style
                 .placeholder_text

crates/file_finder/src/file_finder.rs 🔗

@@ -119,7 +119,7 @@ impl FileFinder {
         cx.observe(&project, Self::project_updated).detach();
         Self {
             project,
-            picker: cx.add_view(|cx| Picker::new(handle, cx)),
+            picker: cx.add_view(|cx| Picker::new("Search project files...", handle, cx)),
             search_count: 0,
             latest_search_id: 0,
             latest_search_did_cancel: false,

crates/go_to_line/src/go_to_line.rs 🔗

@@ -1,3 +1,5 @@
+use std::sync::Arc;
+
 use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
 use gpui::{
     actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, Axis, Entity,
@@ -31,7 +33,10 @@ pub enum Event {
 impl GoToLine {
     pub fn new(active_editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) -> Self {
         let line_editor = cx.add_view(|cx| {
-            Editor::single_line(Some(|theme| theme.picker.input_editor.clone()), cx)
+            Editor::single_line(
+                Some(Arc::new(|theme| theme.picker.input_editor.clone())),
+                cx,
+            )
         });
         cx.subscribe(&line_editor, Self::on_line_editor_event)
             .detach();
@@ -170,8 +175,8 @@ impl View for GoToLine {
                             .boxed(),
                     )
                     .with_child(
-                        Container::new(Label::new(label, theme.empty.label.clone()).boxed())
-                            .with_style(theme.empty.container)
+                        Container::new(Label::new(label, theme.no_matches.label.clone()).boxed())
+                            .with_style(theme.no_matches.container)
                             .boxed(),
                     )
                     .boxed(),

crates/outline/src/outline.rs 🔗

@@ -67,7 +67,9 @@ impl OutlineView {
     ) -> Self {
         let handle = cx.weak_handle();
         Self {
-            picker: cx.add_view(|cx| Picker::new(handle, cx).with_max_size(800., 1200.)),
+            picker: cx.add_view(|cx| {
+                Picker::new("Search buffer symbols...", handle, cx).with_max_size(800., 1200.)
+            }),
             last_query: Default::default(),
             matches: Default::default(),
             selected_match_index: 0,

crates/picker/Cargo.toml 🔗

@@ -16,6 +16,8 @@ util = { path = "../util" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 
+parking_lot = "0.11.1"
+
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }

crates/picker/src/picker.rs 🔗

@@ -1,25 +1,22 @@
 use editor::Editor;
 use gpui::{
-    elements::{
-        ChildView, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, UniformList,
-        UniformListState,
-    },
+    elements::*,
     geometry::vector::{vec2f, Vector2F},
     keymap,
     platform::CursorStyle,
-    AnyViewHandle, AppContext, Axis, Element, ElementBox, Entity, MouseButton, MouseState,
-    MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
+    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
-use settings::Settings;
-use std::cmp;
+use parking_lot::Mutex;
+use std::{cmp, sync::Arc};
 
 pub struct Picker<D: PickerDelegate> {
     delegate: WeakViewHandle<D>,
     query_editor: ViewHandle<Editor>,
     list_state: UniformListState,
     max_size: Vector2F,
-    theme: Box<dyn FnMut(&AppContext) -> &theme::Picker>,
+    theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
     confirmed: bool,
 }
 
@@ -52,8 +49,8 @@ impl<D: PickerDelegate> View for Picker<D> {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
-        let theme = (self.theme)(cx);
-        let container_style = theme.container;
+        let theme = (self.theme.lock())(&cx.global::<settings::Settings>().theme);
+        let query = self.query(cx);
         let delegate = self.delegate.clone();
         let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
             delegate.read(cx).match_count()
@@ -61,19 +58,36 @@ impl<D: PickerDelegate> View for Picker<D> {
             0
         };
 
+        let container_style;
+        let editor_style;
+        if query.is_empty() && match_count == 0 {
+            container_style = theme.empty_container;
+            editor_style = theme.empty_input_editor.container;
+        } else {
+            container_style = theme.container;
+            editor_style = theme.input_editor.container;
+        };
+
         Flex::new(Axis::Vertical)
             .with_child(
                 ChildView::new(&self.query_editor, cx)
                     .contained()
-                    .with_style(theme.input_editor.container)
+                    .with_style(editor_style)
                     .boxed(),
             )
-            .with_child(
-                if match_count == 0 {
-                    Label::new("No matches".into(), theme.empty.label.clone())
-                        .contained()
-                        .with_style(theme.empty.container)
+            .with_children(if match_count == 0 {
+                if query.is_empty() {
+                    None
                 } else {
+                    Some(
+                        Label::new("No matches".into(), theme.no_matches.label.clone())
+                            .contained()
+                            .with_style(theme.no_matches.container)
+                            .boxed(),
+                    )
+                }
+            } else {
+                Some(
                     UniformList::new(
                         self.list_state.clone(),
                         match_count,
@@ -98,10 +112,10 @@ impl<D: PickerDelegate> View for Picker<D> {
                     )
                     .contained()
                     .with_margin_top(6.0)
-                }
-                .flex(1., false)
-                .boxed(),
-            )
+                    .flex(1., false)
+                    .boxed(),
+                )
+            })
             .contained()
             .with_style(container_style)
             .constrained()
@@ -134,9 +148,26 @@ impl<D: PickerDelegate> Picker<D> {
         cx.add_action(Self::cancel);
     }
 
-    pub fn new(delegate: WeakViewHandle<D>, cx: &mut ViewContext<Self>) -> Self {
-        let query_editor = cx.add_view(|cx| {
-            Editor::single_line(Some(|theme| theme.picker.input_editor.clone()), cx)
+    pub fn new<P>(placeholder: P, delegate: WeakViewHandle<D>, cx: &mut ViewContext<Self>) -> Self
+    where
+        P: Into<Arc<str>>,
+    {
+        let theme = Arc::new(Mutex::new(
+            Box::new(|theme: &theme::Theme| theme.picker.clone())
+                as Box<dyn Fn(&theme::Theme) -> theme::Picker>,
+        ));
+        let query_editor = cx.add_view({
+            let picker_theme = theme.clone();
+            |cx| {
+                let mut editor = Editor::single_line(
+                    Some(Arc::new(move |theme| {
+                        (picker_theme.lock())(theme).input_editor.clone()
+                    })),
+                    cx,
+                );
+                editor.set_placeholder_text(placeholder, cx);
+                editor
+            }
         });
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();
@@ -145,7 +176,7 @@ impl<D: PickerDelegate> Picker<D> {
             list_state: Default::default(),
             delegate,
             max_size: vec2f(540., 420.),
-            theme: Box::new(|cx| &cx.global::<Settings>().theme.picker),
+            theme,
             confirmed: false,
         };
         cx.defer(|this, cx| {
@@ -162,11 +193,11 @@ impl<D: PickerDelegate> Picker<D> {
         self
     }
 
-    pub fn with_theme<F>(mut self, theme: F) -> Self
+    pub fn with_theme<F>(self, theme: F) -> Self
     where
-        F: 'static + FnMut(&AppContext) -> &theme::Picker,
+        F: 'static + Fn(&theme::Theme) -> theme::Picker,
     {
-        self.theme = Box::new(theme);
+        *self.theme.lock() = Box::new(theme);
         self
     }
 

crates/project_panel/src/project_panel.rs 🔗

@@ -23,6 +23,7 @@ use std::{
     ffi::OsStr,
     ops::Range,
     path::{Path, PathBuf},
+    sync::Arc,
 };
 use unicase::UniCase;
 use workspace::Workspace;
@@ -175,11 +176,11 @@ impl ProjectPanel {
 
             let filename_editor = cx.add_view(|cx| {
                 Editor::single_line(
-                    Some(|theme| {
+                    Some(Arc::new(|theme| {
                         let mut style = theme.project_panel.filename_editor.clone();
                         style.container.background_color.take();
                         style
-                    }),
+                    })),
                     cx,
                 )
             });

crates/project_symbols/src/project_symbols.rs 🔗

@@ -63,7 +63,7 @@ impl ProjectSymbolsView {
         let handle = cx.weak_handle();
         Self {
             project,
-            picker: cx.add_view(|cx| Picker::new(handle, cx)),
+            picker: cx.add_view(|cx| Picker::new("Search project symbols...", handle, cx)),
             selected_match_index: 0,
             symbols: Default::default(),
             visible_match_candidates: Default::default(),

crates/search/src/buffer_search.rs 🔗

@@ -12,7 +12,7 @@ use gpui::{
 use project::search::SearchQuery;
 use serde::Deserialize;
 use settings::Settings;
-use std::any::Any;
+use std::{any::Any, sync::Arc};
 use workspace::{
     searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
     ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView,
@@ -232,7 +232,11 @@ impl ToolbarItemView for BufferSearchBar {
 impl BufferSearchBar {
     pub fn new(cx: &mut ViewContext<Self>) -> Self {
         let query_editor = cx.add_view(|cx| {
-            Editor::auto_height(2, Some(|theme| theme.search.editor.input.clone()), cx)
+            Editor::auto_height(
+                2,
+                Some(Arc::new(|theme| theme.search.editor.input.clone())),
+                cx,
+            )
         });
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();

crates/search/src/project_search.rs 🔗

@@ -20,6 +20,7 @@ use std::{
     any::{Any, TypeId},
     ops::Range,
     path::PathBuf,
+    sync::Arc,
 };
 use util::ResultExt as _;
 use workspace::{
@@ -378,8 +379,10 @@ impl ProjectSearchView {
             .detach();
 
         let query_editor = cx.add_view(|cx| {
-            let mut editor =
-                Editor::single_line(Some(|theme| theme.search.editor.input.clone()), cx);
+            let mut editor = Editor::single_line(
+                Some(Arc::new(|theme| theme.search.editor.input.clone())),
+                cx,
+            );
             editor.set_text(query_text, cx);
             editor
         });

crates/theme/src/theme.rs 🔗

@@ -423,12 +423,14 @@ pub struct ChannelName {
     pub name: TextStyle,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Clone, Deserialize, Default)]
 pub struct Picker {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub empty: ContainedLabel,
+    pub empty_container: ContainerStyle,
     pub input_editor: FieldEditor,
+    pub empty_input_editor: FieldEditor,
+    pub no_matches: ContainedLabel,
     pub item: Interactive<ContainedLabel>,
 }
 

crates/theme_selector/src/theme_selector.rs 🔗

@@ -38,7 +38,7 @@ pub enum Event {
 impl ThemeSelector {
     fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
         let handle = cx.weak_handle();
-        let picker = cx.add_view(|cx| Picker::new(handle, cx));
+        let picker = cx.add_view(|cx| Picker::new("Select Theme...", handle, cx));
         let settings = cx.global::<Settings>();
 
         let original_theme = settings.theme.clone();

styles/src/styleTree/contactFinder.ts 🔗

@@ -3,7 +3,7 @@ import { ColorScheme } from "../themes/common/colorScheme";
 import { background, border, foreground, text } from "./components";
 
 export default function contactFinder(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
+  let layer = colorScheme.middle;
 
   const sideMargin = 6;
   const contactButton = {
@@ -14,31 +14,36 @@ export default function contactFinder(colorScheme: ColorScheme) {
     cornerRadius: 8,
   };
 
+  const pickerStyle = picker(colorScheme);
+  const pickerInput = {
+    background: background(layer, "on"),
+    cornerRadius: 6,
+    text: text(layer, "mono",),
+    placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
+    selection: colorScheme.players[0],
+    border: border(layer),
+    padding: {
+      bottom: 4,
+      left: 8,
+      right: 8,
+      top: 4,
+    },
+    margin: {
+      left: sideMargin,
+      right: sideMargin,
+    }
+  };
+
   return {
     picker: {
+      emptyContainer: {},
       item: {
-        ...picker(colorScheme).item,
-        margin: { left: sideMargin, right: sideMargin }
+        ...pickerStyle.item,
+        margin: { left: sideMargin, right: sideMargin },
       },
-      empty: picker(colorScheme).empty,
-      inputEditor: {
-        background: background(layer, "on"),
-        cornerRadius: 6,
-        text: text(layer, "mono",),
-        placeholderText: text(layer, "mono", "variant", { size: "sm" }),
-        selection: colorScheme.players[0],
-        border: border(layer),
-        padding: {
-          bottom: 4,
-          left: 8,
-          right: 8,
-          top: 4,
-        },
-        margin: {
-          left: sideMargin,
-          right: sideMargin,
-        }
-      }
+      noMatches: pickerStyle.noMatches,
+      inputEditor: pickerInput,
+      emptyInputEditor: pickerInput
     },
     rowHeight: 28,
     contactAvatar: {

styles/src/styleTree/contactList.ts 🔗

@@ -53,7 +53,7 @@ export default function contactsPanel(colorScheme: ColorScheme) {
       background: background(layer, "on"),
       cornerRadius: 6,
       text: text(layer, "mono", "on"),
-      placeholderText: text(layer, "mono", "on", "disabled", { size: "sm" }),
+      placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
       selection: colorScheme.players[0],
       border: border(layer, "on"),
       padding: {

styles/src/styleTree/picker.ts 🔗

@@ -3,13 +3,39 @@ import { background, border, text } from "./components";
 
 export default function picker(colorScheme: ColorScheme) {
   let layer = colorScheme.lowest;
-  return {
+  const container = {
     background: background(layer),
     border: border(layer),
     shadow: colorScheme.modalShadow,
     cornerRadius: 12,
     padding: {
       bottom: 4,
+    }
+  };
+  const inputEditor = {
+    placeholderText: text(layer, "sans", "on", "disabled"),
+    selection: colorScheme.players[0],
+    text: text(layer, "mono", "on"),
+    border: border(layer, { bottom: true }),
+    padding: {
+      bottom: 8,
+      left: 16,
+      right: 16,
+      top: 8,
+    },
+    margin: {
+      bottom: 4,
+    },
+  };
+  const emptyInputEditor = { ...inputEditor };
+  delete emptyInputEditor.border;
+  delete emptyInputEditor.margin;
+
+  return {
+    ...container,
+    emptyContainer: {
+      ...container,
+      padding: {}
     },
     item: {
       padding: {
@@ -37,7 +63,9 @@ export default function picker(colorScheme: ColorScheme) {
         background: background(layer, "hovered"),
       },
     },
-    empty: {
+    inputEditor,
+    emptyInputEditor,
+    noMatches: {
       text: text(layer, "sans", "variant"),
       padding: {
         bottom: 8,
@@ -46,20 +74,5 @@ export default function picker(colorScheme: ColorScheme) {
         top: 8,
       },
     },
-    inputEditor: {
-      placeholderText: text(layer, "sans", "on", "disabled"),
-      selection: colorScheme.players[0],
-      text: text(layer, "mono", "on"),
-      border: border(layer, { bottom: true }),
-      padding: {
-        bottom: 8,
-        left: 16,
-        right: 16,
-        top: 8,
-      },
-      margin: {
-        bottom: 4,
-      },
-    },
   };
 }