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}