1use std::sync::Arc;
2
3use gpui::{App, FontFeatures, FontWeight};
4use project::project_settings::ProjectSettings;
5use settings::{EditableSettingControl, Settings, SettingsContent};
6use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
7use ui::{
8 CheckboxWithLabel, ContextMenu, DropdownMenu, SettingsContainer, SettingsGroup, prelude::*,
9};
10
11use crate::EditorSettings;
12
13#[derive(IntoElement)]
14pub struct EditorSettingsControls {}
15
16impl Default for EditorSettingsControls {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21
22impl EditorSettingsControls {
23 pub fn new() -> Self {
24 Self {}
25 }
26}
27
28impl RenderOnce for EditorSettingsControls {
29 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
30 SettingsContainer::new()
31 .child(
32 SettingsGroup::new("Font")
33 .child(
34 h_flex()
35 .gap_2()
36 .justify_between()
37 .child(BufferFontFamilyControl)
38 .child(BufferFontWeightControl),
39 )
40 .child(BufferFontSizeControl)
41 .child(BufferFontLigaturesControl),
42 )
43 .child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
44 .child(
45 SettingsGroup::new("Gutter").child(
46 h_flex()
47 .gap_2()
48 .justify_between()
49 .child(LineNumbersControl)
50 .child(RelativeLineNumbersControl),
51 ),
52 )
53 }
54}
55
56#[derive(IntoElement)]
57struct BufferFontFamilyControl;
58
59impl EditableSettingControl for BufferFontFamilyControl {
60 type Value = SharedString;
61
62 fn name(&self) -> SharedString {
63 "Buffer Font Family".into()
64 }
65
66 fn read(cx: &App) -> Self::Value {
67 let settings = ThemeSettings::get_global(cx);
68 settings.buffer_font.family.clone()
69 }
70
71 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
72 settings.theme.buffer_font_family = Some(FontFamilyName(value.into()));
73 }
74}
75
76impl RenderOnce for BufferFontFamilyControl {
77 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
78 let value = Self::read(cx);
79
80 h_flex()
81 .gap_2()
82 .child(Icon::new(IconName::Font))
83 .child(DropdownMenu::new(
84 "buffer-font-family",
85 value,
86 ContextMenu::build(window, cx, |mut menu, _, cx| {
87 let font_family_cache = FontFamilyCache::global(cx);
88
89 for font_name in font_family_cache.list_font_families(cx) {
90 menu = menu.custom_entry(
91 {
92 let font_name = font_name.clone();
93 move |_window, _cx| Label::new(font_name.clone()).into_any_element()
94 },
95 {
96 let font_name = font_name.clone();
97 move |_window, cx| {
98 Self::write(font_name.clone(), cx);
99 }
100 },
101 )
102 }
103
104 menu
105 }),
106 ))
107 }
108}
109
110#[derive(IntoElement)]
111struct BufferFontSizeControl;
112
113impl EditableSettingControl for BufferFontSizeControl {
114 type Value = Pixels;
115
116 fn name(&self) -> SharedString {
117 "Buffer Font Size".into()
118 }
119
120 fn read(cx: &App) -> Self::Value {
121 ThemeSettings::get_global(cx).buffer_font_size(cx)
122 }
123
124 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
125 settings.theme.buffer_font_size = Some(value.into());
126 }
127}
128
129impl RenderOnce for BufferFontSizeControl {
130 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
131 let _value = Self::read(cx);
132
133 h_flex()
134 .gap_2()
135 .child(Icon::new(IconName::FontSize))
136 .child(div()) // TODO: Re-evaluate this whole crate once settings UI is complete
137 }
138}
139
140#[derive(IntoElement)]
141struct BufferFontWeightControl;
142
143impl EditableSettingControl for BufferFontWeightControl {
144 type Value = FontWeight;
145
146 fn name(&self) -> SharedString {
147 "Buffer Font Weight".into()
148 }
149
150 fn read(cx: &App) -> Self::Value {
151 let settings = ThemeSettings::get_global(cx);
152 settings.buffer_font.weight
153 }
154
155 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
156 settings.theme.buffer_font_weight = Some(value.0);
157 }
158}
159
160impl RenderOnce for BufferFontWeightControl {
161 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
162 let value = Self::read(cx);
163
164 h_flex()
165 .gap_2()
166 .child(Icon::new(IconName::FontWeight))
167 .child(DropdownMenu::new(
168 "buffer-font-weight",
169 value.0.to_string(),
170 ContextMenu::build(window, cx, |mut menu, _window, _cx| {
171 for weight in FontWeight::ALL {
172 menu = menu.custom_entry(
173 move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(),
174 {
175 move |_, cx| {
176 Self::write(weight, cx);
177 }
178 },
179 )
180 }
181
182 menu
183 }),
184 ))
185 }
186}
187
188#[derive(IntoElement)]
189struct BufferFontLigaturesControl;
190
191impl EditableSettingControl for BufferFontLigaturesControl {
192 type Value = bool;
193
194 fn name(&self) -> SharedString {
195 "Buffer Font Ligatures".into()
196 }
197
198 fn read(cx: &App) -> Self::Value {
199 let settings = ThemeSettings::get_global(cx);
200 settings
201 .buffer_font
202 .features
203 .is_calt_enabled()
204 .unwrap_or(true)
205 }
206
207 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
208 let value = if value { 1 } else { 0 };
209
210 let mut features = settings
211 .theme
212 .buffer_font_features
213 .as_ref()
214 .map(|features| features.tag_value_list().to_vec())
215 .unwrap_or_default();
216
217 if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
218 features[calt_index].1 = value;
219 } else {
220 features.push(("calt".into(), value));
221 }
222
223 settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
224 }
225}
226
227impl RenderOnce for BufferFontLigaturesControl {
228 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
229 let value = Self::read(cx);
230
231 CheckboxWithLabel::new(
232 "buffer-font-ligatures",
233 Label::new(self.name()),
234 value.into(),
235 |selection, _, cx| {
236 Self::write(
237 match selection {
238 ToggleState::Selected => true,
239 ToggleState::Unselected | ToggleState::Indeterminate => false,
240 },
241 cx,
242 );
243 },
244 )
245 }
246}
247
248#[derive(IntoElement)]
249struct InlineGitBlameControl;
250
251impl EditableSettingControl for InlineGitBlameControl {
252 type Value = bool;
253
254 fn name(&self) -> SharedString {
255 "Inline Git Blame".into()
256 }
257
258 fn read(cx: &App) -> Self::Value {
259 let settings = ProjectSettings::get_global(cx);
260 settings.git.inline_blame.enabled
261 }
262
263 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
264 settings
265 .git
266 .get_or_insert_default()
267 .inline_blame
268 .get_or_insert_default()
269 .enabled = Some(value)
270 }
271}
272
273impl RenderOnce for InlineGitBlameControl {
274 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
275 let value = Self::read(cx);
276
277 CheckboxWithLabel::new(
278 "inline-git-blame",
279 Label::new(self.name()),
280 value.into(),
281 |selection, _, cx| {
282 Self::write(
283 match selection {
284 ToggleState::Selected => true,
285 ToggleState::Unselected | ToggleState::Indeterminate => false,
286 },
287 cx,
288 );
289 },
290 )
291 }
292}
293
294#[derive(IntoElement)]
295struct LineNumbersControl;
296
297impl EditableSettingControl for LineNumbersControl {
298 type Value = bool;
299
300 fn name(&self) -> SharedString {
301 "Line Numbers".into()
302 }
303
304 fn read(cx: &App) -> Self::Value {
305 let settings = EditorSettings::get_global(cx);
306 settings.gutter.line_numbers
307 }
308
309 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
310 settings.editor.gutter.get_or_insert_default().line_numbers = Some(value);
311 }
312}
313
314impl RenderOnce for LineNumbersControl {
315 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
316 let value = Self::read(cx);
317
318 CheckboxWithLabel::new(
319 "line-numbers",
320 Label::new(self.name()),
321 value.into(),
322 |selection, _, cx| {
323 Self::write(
324 match selection {
325 ToggleState::Selected => true,
326 ToggleState::Unselected | ToggleState::Indeterminate => false,
327 },
328 cx,
329 );
330 },
331 )
332 }
333}
334
335#[derive(IntoElement)]
336struct RelativeLineNumbersControl;
337
338impl EditableSettingControl for RelativeLineNumbersControl {
339 type Value = bool;
340
341 fn name(&self) -> SharedString {
342 "Relative Line Numbers".into()
343 }
344
345 fn read(cx: &App) -> Self::Value {
346 let settings = EditorSettings::get_global(cx);
347 settings.relative_line_numbers
348 }
349
350 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
351 settings.editor.relative_line_numbers = Some(value);
352 }
353}
354
355impl RenderOnce for RelativeLineNumbersControl {
356 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
357 let value = Self::read(cx);
358
359 DropdownMenu::new(
360 "relative-line-numbers",
361 if value { "Relative" } else { "Ascending" },
362 ContextMenu::build(window, cx, |menu, _window, _cx| {
363 menu.custom_entry(
364 |_window, _cx| Label::new("Ascending").into_any_element(),
365 move |_, cx| Self::write(false, cx),
366 )
367 .custom_entry(
368 |_window, _cx| Label::new("Relative").into_any_element(),
369 move |_, cx| Self::write(true, cx),
370 )
371 }),
372 )
373 }
374}