Detailed changes
@@ -12,14 +12,11 @@ use gpui::{
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
-use ui::{
- DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem,
- ListItemSpacing, prelude::*,
-};
+use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, prelude::*};
use util::ResultExt;
use zed_actions::agent::OpenSettings;
-use crate::ui::HoldForDefault;
+use crate::ui::{HoldForDefault, ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem};
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
@@ -236,39 +233,19 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn render_match(
&self,
ix: usize,
- selected: bool,
+ is_focused: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
match self.filtered_entries.get(ix)? {
- AcpModelPickerEntry::Separator(title) => Some(
- div()
- .px_2()
- .pb_1()
- .when(ix > 1, |this| {
- this.mt_1()
- .pt_2()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- })
- .child(
- Label::new(title)
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- .into_any_element(),
- ),
+ AcpModelPickerEntry::Separator(title) => {
+ Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
+ }
AcpModelPickerEntry::Model(model_info) => {
let is_selected = Some(model_info) == self.selected_model.as_ref();
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
- let model_icon_color = if is_selected {
- Color::Accent
- } else {
- Color::Muted
- };
-
Some(
div()
.id(("model-picker-menu-child", ix))
@@ -284,30 +261,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
}))
})
.child(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .toggle_state(selected)
- .child(
- h_flex()
- .w_full()
- .gap_1p5()
- .when_some(model_info.icon, |this, icon| {
- this.child(
- Icon::new(icon)
- .color(model_icon_color)
- .size(IconSize::Small)
- )
- })
- .child(Label::new(model_info.name.clone()).truncate()),
- )
- .end_slot(div().pr_3().when(is_selected, |this| {
- this.child(
- Icon::new(IconName::Check)
- .color(Color::Accent)
- .size(IconSize::Small),
- )
- })),
+ ModelSelectorListItem::new(ix, model_info.name.clone())
+ .is_focused(is_focused)
+ .is_selected(is_selected)
+ .when_some(model_info.icon, |this, icon| this.icon(icon)),
)
.into_any_element()
)
@@ -343,7 +300,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn render_footer(
&self,
_window: &mut Window,
- cx: &mut Context<Picker<Self>>,
+ _cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
let focus_handle = self.focus_handle.clone();
@@ -351,26 +308,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
return None;
}
- Some(
- h_flex()
- .w_full()
- .p_1p5()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- .child(
- Button::new("configure", "Configure")
- .full_width()
- .style(ButtonStyle::Outlined)
- .key_binding(
- KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(|_, window, cx| {
- window.dispatch_action(OpenSettings.boxed_clone(), cx);
- }),
- )
- .into_any(),
- )
+ Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
}
}
@@ -11,9 +11,11 @@ use language_model::{
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
-use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
+use ui::prelude::*;
use zed_actions::agent::OpenSettings;
+use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem};
+
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
@@ -459,28 +461,14 @@ impl PickerDelegate for LanguageModelPickerDelegate {
fn render_match(
&self,
ix: usize,
- selected: bool,
+ is_focused: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
match self.filtered_entries.get(ix)? {
- LanguageModelPickerEntry::Separator(title) => Some(
- div()
- .px_2()
- .pb_1()
- .when(ix > 1, |this| {
- this.mt_1()
- .pt_2()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- })
- .child(
- Label::new(title)
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- .into_any_element(),
- ),
+ LanguageModelPickerEntry::Separator(title) => {
+ Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
+ }
LanguageModelPickerEntry::Model(model_info) => {
let active_model = (self.get_active_model)(cx);
let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
@@ -489,35 +477,11 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
&& Some(model_info.model.id()) == active_model_id;
- let model_icon_color = if is_selected {
- Color::Accent
- } else {
- Color::Muted
- };
-
Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .toggle_state(selected)
- .child(
- h_flex()
- .w_full()
- .gap_1p5()
- .child(
- Icon::new(model_info.icon)
- .color(model_icon_color)
- .size(IconSize::Small),
- )
- .child(Label::new(model_info.model.name().0).truncate()),
- )
- .end_slot(div().pr_3().when(is_selected, |this| {
- this.child(
- Icon::new(IconName::Check)
- .color(Color::Accent)
- .size(IconSize::Small),
- )
- }))
+ ModelSelectorListItem::new(ix, model_info.model.name().0)
+ .is_focused(is_focused)
+ .is_selected(is_selected)
+ .icon(model_info.icon)
.into_any_element(),
)
}
@@ -527,34 +491,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
fn render_footer(
&self,
_window: &mut Window,
- cx: &mut Context<Picker<Self>>,
+ _cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
- let focus_handle = self.focus_handle.clone();
-
if !self.popover_styles {
return None;
}
- Some(
- h_flex()
- .w_full()
- .p_1p5()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- .child(
- Button::new("configure", "Configure")
- .full_width()
- .style(ButtonStyle::Outlined)
- .key_binding(
- KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(|_, window, cx| {
- window.dispatch_action(OpenSettings.boxed_clone(), cx);
- }),
- )
- .into_any(),
- )
+ let focus_handle = self.focus_handle.clone();
+
+ Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
}
}
@@ -4,8 +4,8 @@ mod burn_mode_tooltip;
mod claude_code_onboarding_modal;
mod end_trial_upsell;
mod hold_for_default;
+mod model_selector_components;
mod onboarding_modal;
-
mod usage_callout;
pub use acp_onboarding_modal::*;
@@ -14,6 +14,6 @@ pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*;
pub use end_trial_upsell::*;
pub use hold_for_default::*;
+pub use model_selector_components::*;
pub use onboarding_modal::*;
-
pub use usage_callout::*;
@@ -0,0 +1,147 @@
+use gpui::{Action, FocusHandle, prelude::*};
+use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
+
+#[derive(IntoElement)]
+pub struct ModelSelectorHeader {
+ title: SharedString,
+ has_border: bool,
+}
+
+impl ModelSelectorHeader {
+ pub fn new(title: impl Into<SharedString>, has_border: bool) -> Self {
+ Self {
+ title: title.into(),
+ has_border,
+ }
+ }
+}
+
+impl RenderOnce for ModelSelectorHeader {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ div()
+ .px_2()
+ .pb_1()
+ .when(self.has_border, |this| {
+ this.mt_1()
+ .pt_2()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ })
+ .child(
+ Label::new(self.title)
+ .size(LabelSize::XSmall)
+ .color(Color::Muted),
+ )
+ }
+}
+
+#[derive(IntoElement)]
+pub struct ModelSelectorListItem {
+ index: usize,
+ title: SharedString,
+ icon: Option<IconName>,
+ is_selected: bool,
+ is_focused: bool,
+}
+
+impl ModelSelectorListItem {
+ pub fn new(index: usize, title: impl Into<SharedString>) -> Self {
+ Self {
+ index,
+ title: title.into(),
+ icon: None,
+ is_selected: false,
+ is_focused: false,
+ }
+ }
+
+ pub fn icon(mut self, icon: IconName) -> Self {
+ self.icon = Some(icon);
+ self
+ }
+
+ pub fn is_selected(mut self, is_selected: bool) -> Self {
+ self.is_selected = is_selected;
+ self
+ }
+
+ pub fn is_focused(mut self, is_focused: bool) -> Self {
+ self.is_focused = is_focused;
+ self
+ }
+}
+
+impl RenderOnce for ModelSelectorListItem {
+ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ let model_icon_color = if self.is_selected {
+ Color::Accent
+ } else {
+ Color::Muted
+ };
+
+ ListItem::new(self.index)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(self.is_focused)
+ .child(
+ h_flex()
+ .w_full()
+ .gap_1p5()
+ .when_some(self.icon, |this, icon| {
+ this.child(
+ Icon::new(icon)
+ .color(model_icon_color)
+ .size(IconSize::Small),
+ )
+ })
+ .child(Label::new(self.title).truncate()),
+ )
+ .end_slot(div().pr_2().when(self.is_selected, |this| {
+ this.child(
+ Icon::new(IconName::Check)
+ .color(Color::Accent)
+ .size(IconSize::Small),
+ )
+ }))
+ }
+}
+
+#[derive(IntoElement)]
+pub struct ModelSelectorFooter {
+ action: Box<dyn Action>,
+ focus_handle: FocusHandle,
+}
+
+impl ModelSelectorFooter {
+ pub fn new(action: Box<dyn Action>, focus_handle: FocusHandle) -> Self {
+ Self {
+ action,
+ focus_handle,
+ }
+ }
+}
+
+impl RenderOnce for ModelSelectorFooter {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let action = self.action;
+ let focus_handle = self.focus_handle;
+
+ h_flex()
+ .w_full()
+ .p_1p5()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(
+ Button::new("configure", "Configure")
+ .full_width()
+ .style(ButtonStyle::Outlined)
+ .key_binding(
+ KeyBinding::for_action_in(action.as_ref(), &focus_handle, cx)
+ .map(|kb| kb.size(rems_from_px(12.))),
+ )
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(action.boxed_clone(), cx);
+ }),
+ )
+ }
+}