model_selector_components.rs

  1use gpui::{Action, ClickEvent, FocusHandle, prelude::*};
  2use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
  3use zed_actions::agent::ToggleModelSelector;
  4
  5use crate::CycleFavoriteModels;
  6
  7enum ModelIcon {
  8    Name(IconName),
  9    Path(SharedString),
 10}
 11
 12#[derive(IntoElement)]
 13pub struct ModelSelectorHeader {
 14    title: SharedString,
 15    has_border: bool,
 16}
 17
 18impl ModelSelectorHeader {
 19    pub fn new(title: impl Into<SharedString>, has_border: bool) -> Self {
 20        Self {
 21            title: title.into(),
 22            has_border,
 23        }
 24    }
 25}
 26
 27impl RenderOnce for ModelSelectorHeader {
 28    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 29        div()
 30            .px_2()
 31            .pb_1()
 32            .when(self.has_border, |this| {
 33                this.mt_1()
 34                    .pt_2()
 35                    .border_t_1()
 36                    .border_color(cx.theme().colors().border_variant)
 37            })
 38            .child(
 39                Label::new(self.title)
 40                    .size(LabelSize::XSmall)
 41                    .color(Color::Muted),
 42            )
 43    }
 44}
 45
 46#[derive(IntoElement)]
 47pub struct ModelSelectorListItem {
 48    index: usize,
 49    title: SharedString,
 50    icon: Option<ModelIcon>,
 51    is_selected: bool,
 52    is_focused: bool,
 53    is_favorite: bool,
 54    on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 55}
 56
 57impl ModelSelectorListItem {
 58    pub fn new(index: usize, title: impl Into<SharedString>) -> Self {
 59        Self {
 60            index,
 61            title: title.into(),
 62            icon: None,
 63            is_selected: false,
 64            is_focused: false,
 65            is_favorite: false,
 66            on_toggle_favorite: None,
 67        }
 68    }
 69
 70    pub fn icon(mut self, icon: IconName) -> Self {
 71        self.icon = Some(ModelIcon::Name(icon));
 72        self
 73    }
 74
 75    pub fn icon_path(mut self, path: SharedString) -> Self {
 76        self.icon = Some(ModelIcon::Path(path));
 77        self
 78    }
 79
 80    pub fn is_selected(mut self, is_selected: bool) -> Self {
 81        self.is_selected = is_selected;
 82        self
 83    }
 84
 85    pub fn is_focused(mut self, is_focused: bool) -> Self {
 86        self.is_focused = is_focused;
 87        self
 88    }
 89
 90    pub fn is_favorite(mut self, is_favorite: bool) -> Self {
 91        self.is_favorite = is_favorite;
 92        self
 93    }
 94
 95    pub fn on_toggle_favorite(
 96        mut self,
 97        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
 98    ) -> Self {
 99        self.on_toggle_favorite = Some(Box::new(handler));
100        self
101    }
102}
103
104impl RenderOnce for ModelSelectorListItem {
105    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
106        let model_icon_color = if self.is_selected {
107            Color::Accent
108        } else {
109            Color::Muted
110        };
111
112        let is_favorite = self.is_favorite;
113
114        ListItem::new(self.index)
115            .inset(true)
116            .spacing(ListItemSpacing::Sparse)
117            .toggle_state(self.is_focused)
118            .child(
119                h_flex()
120                    .w_full()
121                    .gap_1p5()
122                    .when_some(self.icon, |this, icon| {
123                        this.child(
124                            match icon {
125                                ModelIcon::Name(icon_name) => Icon::new(icon_name),
126                                ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
127                            }
128                            .color(model_icon_color)
129                            .size(IconSize::Small),
130                        )
131                    })
132                    .child(Label::new(self.title).truncate()),
133            )
134            .end_slot(div().pr_2().when(self.is_selected, |this| {
135                this.child(Icon::new(IconName::Check).color(Color::Accent))
136            }))
137            .end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
138                |this, handle_click| {
139                    let (icon, color, tooltip) = if is_favorite {
140                        (IconName::StarFilled, Color::Accent, "Unfavorite Model")
141                    } else {
142                        (IconName::Star, Color::Default, "Favorite Model")
143                    };
144                    this.child(
145                        IconButton::new(("toggle-favorite", self.index), icon)
146                            .layer(ElevationIndex::ElevatedSurface)
147                            .icon_color(color)
148                            .icon_size(IconSize::Small)
149                            .tooltip(Tooltip::text(tooltip))
150                            .on_click(move |event, window, cx| (handle_click)(event, window, cx)),
151                    )
152                }
153            }))
154    }
155}
156
157#[derive(IntoElement)]
158pub struct ModelSelectorFooter {
159    action: Box<dyn Action>,
160    focus_handle: FocusHandle,
161}
162
163impl ModelSelectorFooter {
164    pub fn new(action: Box<dyn Action>, focus_handle: FocusHandle) -> Self {
165        Self {
166            action,
167            focus_handle,
168        }
169    }
170}
171
172impl RenderOnce for ModelSelectorFooter {
173    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
174        let action = self.action;
175        let focus_handle = self.focus_handle;
176
177        h_flex()
178            .w_full()
179            .p_1p5()
180            .border_t_1()
181            .border_color(cx.theme().colors().border_variant)
182            .child(
183                Button::new("configure", "Configure")
184                    .full_width()
185                    .style(ButtonStyle::Outlined)
186                    .key_binding(
187                        KeyBinding::for_action_in(action.as_ref(), &focus_handle, cx)
188                            .map(|kb| kb.size(rems_from_px(12.))),
189                    )
190                    .on_click(move |_, window, cx| {
191                        window.dispatch_action(action.boxed_clone(), cx);
192                    }),
193            )
194    }
195}
196
197#[derive(IntoElement)]
198pub struct ModelSelectorTooltip {
199    focus_handle: FocusHandle,
200    show_cycle_row: bool,
201}
202
203impl ModelSelectorTooltip {
204    pub fn new(focus_handle: FocusHandle) -> Self {
205        Self {
206            focus_handle,
207            show_cycle_row: true,
208        }
209    }
210
211    pub fn show_cycle_row(mut self, show: bool) -> Self {
212        self.show_cycle_row = show;
213        self
214    }
215}
216
217impl RenderOnce for ModelSelectorTooltip {
218    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
219        v_flex()
220            .gap_1()
221            .child(
222                h_flex()
223                    .gap_2()
224                    .justify_between()
225                    .child(Label::new("Change Model"))
226                    .child(KeyBinding::for_action_in(
227                        &ToggleModelSelector,
228                        &self.focus_handle,
229                        cx,
230                    )),
231            )
232            .when(self.show_cycle_row, |this| {
233                this.child(
234                    h_flex()
235                        .pt_1()
236                        .gap_2()
237                        .border_t_1()
238                        .border_color(cx.theme().colors().border_variant)
239                        .justify_between()
240                        .child(Label::new("Cycle Favorited Models"))
241                        .child(KeyBinding::for_action_in(
242                            &CycleFavoriteModels,
243                            &self.focus_handle,
244                            cx,
245                        )),
246                )
247            })
248    }
249}