model_selector_components.rs

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