Update prompt library styles (#12689)

Nate Butler created

- Extend Picker to allow passing a custom editor. This allows creating a
custom styled input.
- Updates various picker styles

Before:

![CleanShot 2024-06-05 at 22 08
36@2x](https://github.com/zed-industries/zed/assets/1714999/96bc62c6-839d-405b-b030-31491aab8710)

After:

![CleanShot 2024-06-05 at 22 09
15@2x](https://github.com/zed-industries/zed/assets/1714999/a4938885-e825-4880-955e-f3f47c81e1e3)

Release Notes:

- N/A

Change summary

assets/icons/sparkle.svg               |   1 
assets/icons/sparkle_filled.svg        |   1 
crates/assistant/src/prompt_library.rs | 213 ++++++++++++++++++---------
crates/picker/src/picker.rs            |  24 +-
crates/ui/src/components/icon.rs       |   8 +
5 files changed, 164 insertions(+), 83 deletions(-)

Detailed changes

assets/icons/sparkle.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkle"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>

crates/assistant/src/prompt_library.rs 🔗

@@ -13,9 +13,10 @@ use futures::{
 };
 use fuzzy::StringMatchCandidate;
 use gpui::{
-    actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
-    EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
-    UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
+    actions, percentage, point, size, Animation, AnimationExt, AnyElement, AppContext,
+    BackgroundExecutor, Bounds, DevicePixels, EventEmitter, Global, PromptLevel, ReadGlobal,
+    Subscription, Task, TitlebarOptions, Transformation, UpdateGlobal, View, WindowBounds,
+    WindowHandle, WindowOptions,
 };
 use heed::{types::SerdeBincode, Database, RoTxn};
 use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
@@ -251,7 +252,11 @@ impl PickerDelegate for PromptPickerDelegate {
         let element = match prompt {
             PromptPickerEntry::DefaultPromptsHeader => ListHeader::new("Default Prompts")
                 .inset(true)
-                .start_slot(Icon::new(IconName::ZedAssistant))
+                .start_slot(
+                    Icon::new(IconName::Sparkle)
+                        .color(Color::Muted)
+                        .size(IconSize::XSmall),
+                )
                 .selected(selected)
                 .into_any_element(),
             PromptPickerEntry::DefaultPromptsEmpty => {
@@ -262,7 +267,11 @@ impl PickerDelegate for PromptPickerDelegate {
             }
             PromptPickerEntry::AllPromptsHeader => ListHeader::new("All Prompts")
                 .inset(true)
-                .start_slot(Icon::new(IconName::Library))
+                .start_slot(
+                    Icon::new(IconName::Library)
+                        .color(Color::Muted)
+                        .size(IconSize::XSmall),
+                )
                 .selected(selected)
                 .into_any_element(),
             PromptPickerEntry::AllPromptsEmpty => ListSubHeader::new("No prompts")
@@ -276,14 +285,15 @@ impl PickerDelegate for PromptPickerDelegate {
                     .inset(true)
                     .spacing(ListItemSpacing::Sparse)
                     .selected(selected)
-                    .child(Label::new(
+                    .child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
                         prompt.title.clone().unwrap_or("Untitled".into()),
-                    ))
+                    )))
                     .end_hover_slot(
                         h_flex()
                             .gap_2()
                             .child(
                                 IconButton::new("delete-prompt", IconName::Trash)
+                                    .icon_color(Color::Muted)
                                     .shape(IconButtonShape::Square)
                                     .tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
                                     .on_click(cx.listener(move |_, _, cx| {
@@ -291,30 +301,24 @@ impl PickerDelegate for PromptPickerDelegate {
                                     })),
                             )
                             .child(
-                                IconButton::new(
-                                    "toggle-default-prompt",
-                                    if default {
-                                        IconName::ZedAssistantFilled
-                                    } else {
-                                        IconName::ZedAssistant
-                                    },
-                                )
-                                .shape(IconButtonShape::Square)
-                                .tooltip(move |cx| {
-                                    Tooltip::text(
-                                        if default {
-                                            "Remove from Default Prompt"
-                                        } else {
-                                            "Add to Default Prompt"
-                                        },
-                                        cx,
-                                    )
-                                })
-                                .on_click(cx.listener(
-                                    move |_, _, cx| {
+                                IconButton::new("toggle-default-prompt", IconName::Sparkle)
+                                    .selected(default)
+                                    .selected_icon(IconName::SparkleFilled)
+                                    .icon_color(if default { Color::Accent } else { Color::Muted })
+                                    .shape(IconButtonShape::Square)
+                                    .tooltip(move |cx| {
+                                        Tooltip::text(
+                                            if default {
+                                                "Remove from Default Prompt"
+                                            } else {
+                                                "Add to Default Prompt"
+                                            },
+                                            cx,
+                                        )
+                                    })
+                                    .on_click(cx.listener(move |_, _, cx| {
                                         cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
-                                    },
-                                )),
+                                    })),
                             ),
                     )
                     .into_any_element()
@@ -322,6 +326,18 @@ impl PickerDelegate for PromptPickerDelegate {
         };
         Some(element)
     }
+
+    fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
+        h_flex()
+            .bg(cx.theme().colors().editor_background)
+            .rounded_md()
+            .overflow_hidden()
+            .flex_none()
+            .py_1()
+            .px_2()
+            .mx_2()
+            .child(editor.clone())
+    }
 }
 
 impl PromptLibrary {
@@ -748,14 +764,13 @@ impl PromptLibrary {
             .child(
                 h_flex()
                     .p(Spacing::Small.rems(cx))
-                    .border_b_1()
-                    .border_color(cx.theme().colors().border)
                     .h(TitleBar::height(cx))
                     .w_full()
                     .flex_none()
                     .justify_end()
                     .child(
                         IconButton::new("new-prompt", IconName::Plus)
+                            .style(ButtonStyle::Transparent)
                             .shape(IconButtonShape::Square)
                             .tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
                             .on_click(|_, cx| {
@@ -777,12 +792,21 @@ impl PromptLibrary {
             .flex_none()
             .min_w_64()
             .children(self.active_prompt_id.and_then(|prompt_id| {
+                let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
                 let prompt_metadata = self.store.metadata(prompt_id)?;
                 let prompt_editor = &self.prompt_editors[&prompt_id];
+                let focus_handle = prompt_editor.editor.focus_handle(cx);
+                let current_model = CompletionProvider::global(cx).model();
+                let token_count = prompt_editor.token_count.map(|count| count.to_string());
+
                 Some(
                     h_flex()
+                        .id("prompt-editor-inner")
                         .size_full()
                         .items_start()
+                        .on_click(cx.listener(move |_, _, cx| {
+                            cx.focus(&focus_handle);
+                        }))
                         .child(
                             div()
                                 .on_action(cx.listener(Self::focus_picker))
@@ -790,8 +814,8 @@ impl PromptLibrary {
                                 .on_action(cx.listener(Self::cancel_last_inline_assist))
                                 .flex_grow()
                                 .h_full()
-                                .pt(Spacing::Large.rems(cx))
-                                .pl(Spacing::Large.rems(cx))
+                                .pt(Spacing::XXLarge.rems(cx))
+                                .pl(Spacing::XXLarge.rems(cx))
                                 .child(prompt_editor.editor.clone()),
                         )
                         .child(
@@ -799,49 +823,92 @@ impl PromptLibrary {
                                 .w_12()
                                 .py(Spacing::Large.rems(cx))
                                 .justify_start()
-                                .items_center()
-                                .gap_4()
+                                .items_end()
+                                .gap_1()
+                                .child(h_flex().h_8().font_family(buffer_font).when_some_else(
+                                    token_count,
+                                    |tokens_ready, token_count| {
+                                        tokens_ready.pr_3().justify_end().child(
+                                            // This isn't actually a button, it just let's us easily add
+                                            // a tooltip to the token count.
+                                            Button::new("token_count", token_count.clone())
+                                                .style(ButtonStyle::Transparent)
+                                                .color(Color::Muted)
+                                                .tooltip(move |cx| {
+                                                    Tooltip::with_meta(
+                                                        format!("{} tokens", token_count,),
+                                                        None,
+                                                        format!(
+                                                            "Model: {}",
+                                                            current_model.display_name()
+                                                        ),
+                                                        cx,
+                                                    )
+                                                }),
+                                        )
+                                    },
+                                    |tokens_loading| {
+                                        tokens_loading.w_12().justify_center().child(
+                                            Icon::new(IconName::ArrowCircle)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted)
+                                                .with_animation(
+                                                    "arrow-circle",
+                                                    Animation::new(Duration::from_secs(4)).repeat(),
+                                                    |icon, delta| {
+                                                        icon.transform(Transformation::rotate(
+                                                            percentage(delta),
+                                                        ))
+                                                    },
+                                                ),
+                                        )
+                                    },
+                                ))
                                 .child(
-                                    IconButton::new(
-                                        "toggle-default-prompt",
-                                        if prompt_metadata.default {
-                                            IconName::ZedAssistantFilled
-                                        } else {
-                                            IconName::ZedAssistant
-                                        },
-                                    )
-                                    .size(ButtonSize::Large)
-                                    .shape(IconButtonShape::Square)
-                                    .tooltip(move |cx| {
-                                        Tooltip::for_action(
-                                            if prompt_metadata.default {
-                                                "Remove from Default Prompt"
+                                    h_flex().justify_center().w_12().h_8().child(
+                                        IconButton::new("toggle-default-prompt", IconName::Sparkle)
+                                            .style(ButtonStyle::Transparent)
+                                            .selected(prompt_metadata.default)
+                                            .selected_icon(IconName::SparkleFilled)
+                                            .icon_color(if prompt_metadata.default {
+                                                Color::Accent
                                             } else {
-                                                "Add to Default Prompt"
-                                            },
-                                            &ToggleDefaultPrompt,
-                                            cx,
-                                        )
-                                    })
-                                    .on_click(|_, cx| {
-                                        cx.dispatch_action(Box::new(ToggleDefaultPrompt));
-                                    }),
+                                                Color::Muted
+                                            })
+                                            .shape(IconButtonShape::Square)
+                                            .tooltip(move |cx| {
+                                                Tooltip::text(
+                                                    if prompt_metadata.default {
+                                                        "Remove from Default Prompt"
+                                                    } else {
+                                                        "Add to Default Prompt"
+                                                    },
+                                                    cx,
+                                                )
+                                            })
+                                            .on_click(|_, cx| {
+                                                cx.dispatch_action(Box::new(ToggleDefaultPrompt));
+                                            }),
+                                    ),
                                 )
                                 .child(
-                                    IconButton::new("delete-prompt", IconName::Trash)
-                                        .shape(IconButtonShape::Square)
-                                        .tooltip(move |cx| {
-                                            Tooltip::for_action("Delete Prompt", &DeletePrompt, cx)
-                                        })
-                                        .on_click(|_, cx| {
-                                            cx.dispatch_action(Box::new(DeletePrompt));
-                                        }),
-                                )
-                                .children(prompt_editor.token_count.map(|token_count| {
-                                    h_flex()
-                                        .justify_center()
-                                        .child(Label::new(token_count.to_string()))
-                                })),
+                                    h_flex().justify_center().w_12().h_8().child(
+                                        IconButton::new("delete-prompt", IconName::Trash)
+                                            .size(ButtonSize::Large)
+                                            .style(ButtonStyle::Transparent)
+                                            .shape(IconButtonShape::Square)
+                                            .tooltip(move |cx| {
+                                                Tooltip::for_action(
+                                                    "Delete Prompt",
+                                                    &DeletePrompt,
+                                                    cx,
+                                                )
+                                            })
+                                            .on_click(|_, cx| {
+                                                cx.dispatch_action(Box::new(DeletePrompt));
+                                            }),
+                                    ),
+                                ),
                         ),
                 )
             }))

crates/picker/src/picker.rs 🔗

@@ -103,6 +103,19 @@ pub trait PickerDelegate: Sized + 'static {
         None
     }
 
+    fn render_editor(&self, editor: &View<Editor>, _cx: &mut ViewContext<Picker<Self>>) -> Div {
+        v_flex()
+            .child(
+                h_flex()
+                    .overflow_hidden()
+                    .flex_none()
+                    .h_9()
+                    .px_4()
+                    .child(editor.clone()),
+            )
+            .child(Divider::horizontal())
+    }
+
     fn render_match(
         &self,
         ix: usize,
@@ -552,16 +565,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
             .on_action(cx.listener(Self::use_selected_query))
             .on_action(cx.listener(Self::confirm_input))
             .child(match &self.head {
-                Head::Editor(editor) => v_flex()
-                    .child(
-                        h_flex()
-                            .overflow_hidden()
-                            .flex_none()
-                            .h_9()
-                            .px_4()
-                            .child(editor.clone()),
-                    )
-                    .child(Divider::horizontal()),
+                Head::Editor(editor) => self.delegate.render_editor(&editor.clone(), cx),
                 Head::Empty(empty_head) => div().child(empty_head.clone()),
             })
             .when(self.delegate.match_count() > 0, |el| {

crates/ui/src/components/icon.rs 🔗

@@ -54,10 +54,14 @@ pub enum IconDecoration {
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconSize {
+    /// 10px
     Indicator,
+    /// 12px
     XSmall,
+    /// 14px
     Small,
     #[default]
+    /// 16px
     Medium,
 }
 
@@ -176,6 +180,8 @@ pub enum IconName {
     Sliders,
     Snip,
     Space,
+    Sparkle,
+    SparkleFilled,
     Spinner,
     Split,
     Star,
@@ -301,6 +307,8 @@ impl IconName {
             IconName::Sliders => "icons/sliders.svg",
             IconName::Snip => "icons/snip.svg",
             IconName::Space => "icons/space.svg",
+            IconName::Sparkle => "icons/sparkle.svg",
+            IconName::SparkleFilled => "icons/sparkle_filled.svg",
             IconName::Spinner => "icons/spinner.svg",
             IconName::Split => "icons/split.svg",
             IconName::Star => "icons/star.svg",