diff --git a/assets/icons/sparkle.svg b/assets/icons/sparkle.svg new file mode 100644 index 0000000000..f420f527f1 --- /dev/null +++ b/assets/icons/sparkle.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/sparkle_filled.svg b/assets/icons/sparkle_filled.svg new file mode 100644 index 0000000000..96837f618d --- /dev/null +++ b/assets/icons/sparkle_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 0c96d0edb2..e8322a9285 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/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, cx: &mut ViewContext>) -> 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() - .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" - } else { - "Add to Default Prompt" - }, - &ToggleDefaultPrompt, - cx, + .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, + ) + }), ) - }) - .on_click(|_, cx| { - cx.dispatch_action(Box::new(ToggleDefaultPrompt)); - }), + }, + |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( + 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 { + 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)); + }), + ), + ), ), ) })) diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 7f87bfda7f..3ed0080fe5 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -103,6 +103,19 @@ pub trait PickerDelegate: Sized + 'static { None } + fn render_editor(&self, editor: &View, _cx: &mut ViewContext>) -> 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 Render for Picker { .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| { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 79b4d87750..cbda8cb12f 100644 --- a/crates/ui/src/components/icon.rs +++ b/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",