composer.rs

  1use crate::{
  2    ui::{ActiveFileButton, ProjectIndexButton},
  3    AssistantChat, CompletionProvider,
  4};
  5use editor::{Editor, EditorElement, EditorStyle};
  6use gpui::{AnyElement, FontStyle, FontWeight, ReadGlobal, TextStyle, View, WeakView, WhiteSpace};
  7use settings::Settings;
  8use theme::ThemeSettings;
  9use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
 10
 11#[derive(IntoElement)]
 12pub struct Composer {
 13    editor: View<Editor>,
 14    project_index_button: View<ProjectIndexButton>,
 15    active_file_button: Option<View<ActiveFileButton>>,
 16    model_selector: AnyElement,
 17}
 18
 19impl Composer {
 20    pub fn new(
 21        editor: View<Editor>,
 22        project_index_button: View<ProjectIndexButton>,
 23        active_file_button: Option<View<ActiveFileButton>>,
 24        model_selector: AnyElement,
 25    ) -> Self {
 26        Self {
 27            editor,
 28            project_index_button,
 29            active_file_button,
 30            model_selector,
 31        }
 32    }
 33
 34    fn render_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
 35        h_flex().child(self.project_index_button.clone())
 36    }
 37
 38    fn render_attachment_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
 39        h_flex().children(
 40            self.active_file_button
 41                .clone()
 42                .map(|view| view.into_any_element()),
 43        )
 44    }
 45}
 46
 47impl RenderOnce for Composer {
 48    fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
 49        let font_size = TextSize::Default.rems(cx);
 50        let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
 51        let mut editor_border = cx.theme().colors().text;
 52        editor_border.fade_out(0.90);
 53
 54        // Remove the extra 1px added by the border
 55        let padding = Spacing::XLarge.rems(cx) - rems_from_px(1.);
 56
 57        h_flex()
 58            .p(Spacing::Small.rems(cx))
 59            .w_full()
 60            .items_start()
 61            .child(
 62                v_flex()
 63                    .w_full()
 64                    .rounded_lg()
 65                    .p(padding)
 66                    .border_1()
 67                    .border_color(editor_border)
 68                    .bg(cx.theme().colors().editor_background)
 69                    .child(
 70                        v_flex()
 71                            .justify_between()
 72                            .w_full()
 73                            .gap_2()
 74                            .child({
 75                                let settings = ThemeSettings::get_global(cx);
 76                                let text_style = TextStyle {
 77                                    color: cx.theme().colors().editor_foreground,
 78                                    font_family: settings.buffer_font.family.clone(),
 79                                    font_features: settings.buffer_font.features.clone(),
 80                                    font_size: font_size.into(),
 81                                    font_weight: FontWeight::NORMAL,
 82                                    font_style: FontStyle::Normal,
 83                                    line_height: line_height.into(),
 84                                    background_color: None,
 85                                    underline: None,
 86                                    strikethrough: None,
 87                                    white_space: WhiteSpace::Normal,
 88                                };
 89
 90                                EditorElement::new(
 91                                    &self.editor,
 92                                    EditorStyle {
 93                                        background: cx.theme().colors().editor_background,
 94                                        local_player: cx.theme().players().local(),
 95                                        text: text_style,
 96                                        ..Default::default()
 97                                    },
 98                                )
 99                            })
100                            .child(
101                                h_flex()
102                                    .flex_none()
103                                    .gap_2()
104                                    .justify_between()
105                                    .w_full()
106                                    .child(
107                                        h_flex().gap_1().child(
108                                            h_flex()
109                                                .gap_2()
110                                                .child(self.render_tools(cx))
111                                                .child(Divider::vertical())
112                                                .child(self.render_attachment_tools(cx)),
113                                        ),
114                                    )
115                                    .child(h_flex().gap_1().child(self.model_selector)),
116                            ),
117                    ),
118            )
119    }
120}
121
122#[derive(IntoElement)]
123pub struct ModelSelector {
124    assistant_chat: WeakView<AssistantChat>,
125    model: String,
126}
127
128impl ModelSelector {
129    pub fn new(assistant_chat: WeakView<AssistantChat>, model: String) -> Self {
130        Self {
131            assistant_chat,
132            model,
133        }
134    }
135}
136
137impl RenderOnce for ModelSelector {
138    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
139        popover_menu("model-switcher")
140            .menu(move |cx| {
141                ContextMenu::build(cx, |mut menu, cx| {
142                    for model in CompletionProvider::global(cx).available_models() {
143                        menu = menu.custom_entry(
144                            {
145                                let model = model.clone();
146                                move |_| Label::new(model.clone()).into_any_element()
147                            },
148                            {
149                                let assistant_chat = self.assistant_chat.clone();
150                                move |cx| {
151                                    _ = assistant_chat.update(cx, |assistant_chat, cx| {
152                                        assistant_chat.model.clone_from(&model);
153                                        cx.notify();
154                                    });
155                                }
156                            },
157                        );
158                    }
159                    menu
160                })
161                .into()
162            })
163            .trigger(
164                ButtonLike::new("active-model")
165                    .child(
166                        h_flex()
167                            .w_full()
168                            .gap_0p5()
169                            .child(
170                                div()
171                                    .overflow_x_hidden()
172                                    .flex_grow()
173                                    .whitespace_nowrap()
174                                    .child(
175                                        Label::new(self.model)
176                                            .size(LabelSize::Small)
177                                            .color(Color::Muted),
178                                    ),
179                            )
180                            .child(
181                                div().child(
182                                    Icon::new(IconName::ChevronDown)
183                                        .color(Color::Muted)
184                                        .size(IconSize::XSmall),
185                                ),
186                            ),
187                    )
188                    .style(ButtonStyle::Subtle)
189                    .tooltip(move |cx| Tooltip::text("Change Model", cx)),
190            )
191            .anchor(gpui::AnchorCorner::BottomRight)
192    }
193}