1use client::TelemetrySettings;
2use fs::Fs;
3use gpui::{App, Entity, IntoElement, Window};
4use settings::{BaseKeymap, Settings, update_settings_file};
5use theme::{Appearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, ThemeSettings};
6use ui::{
7 ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup,
8 ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px,
9};
10use vim_mode_setting::VimModeSetting;
11
12use crate::theme_preview::ThemePreviewTile;
13
14/// separates theme "mode" ("dark" | "light" | "system") into two separate states
15/// - appearance = "dark" | "light"
16/// - "system" true/false
17/// when system selected:
18/// - toggling between light and dark does not change theme.mode, just which variant will be changed
19/// when system not selected:
20/// - toggling between light and dark does change theme.mode
21/// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme,
22///
23/// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not
24/// it does not support setting theme to a static value
25fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
26 let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone();
27 let system_appearance = theme::SystemAppearance::global(cx);
28 let appearance_state = window.use_state(cx, |_, _cx| {
29 theme_selection
30 .as_ref()
31 .and_then(|selection| selection.mode())
32 .and_then(|mode| match mode {
33 ThemeMode::System => None,
34 ThemeMode::Light => Some(Appearance::Light),
35 ThemeMode::Dark => Some(Appearance::Dark),
36 })
37 .unwrap_or(*system_appearance)
38 });
39 let appearance = *appearance_state.read(cx);
40 let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic {
41 mode: match *system_appearance {
42 Appearance::Light => ThemeMode::Light,
43 Appearance::Dark => ThemeMode::Dark,
44 },
45 light: ThemeName("One Light".into()),
46 dark: ThemeName("One Dark".into()),
47 });
48 let theme_registry = ThemeRegistry::global(cx);
49
50 let current_theme_name = theme_selection.theme(appearance);
51 let theme_mode = theme_selection.mode();
52
53 let selected_index = match appearance {
54 Appearance::Light => 0,
55 Appearance::Dark => 1,
56 };
57
58 let theme_seed = 0xBEEF as f32;
59
60 const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
61 const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
62
63 let theme_names = match appearance {
64 Appearance::Light => LIGHT_THEMES,
65 Appearance::Dark => DARK_THEMES,
66 };
67 let themes = theme_names
68 .map(|theme_name| theme_registry.get(theme_name))
69 .map(Result::unwrap);
70
71 let theme_previews = themes.map(|theme| {
72 let is_selected = theme.name == current_theme_name;
73 let name = theme.name.clone();
74 let colors = cx.theme().colors();
75 v_flex()
76 .id(name.clone())
77 .on_click({
78 let theme_name = theme.name.clone();
79 move |_, _, cx| {
80 let fs = <dyn Fs>::global(cx);
81 let theme_name = theme_name.clone();
82 update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
83 settings.set_theme(theme_name, appearance);
84 });
85 }
86 })
87 .flex_1()
88 .child(
89 div()
90 .border_2()
91 .border_color(colors.border_transparent)
92 .rounded(ThemePreviewTile::CORNER_RADIUS)
93 .hover(|mut style| {
94 if !is_selected {
95 style.border_color = Some(colors.element_hover);
96 }
97 style
98 })
99 .when(is_selected, |this| {
100 this.border_color(colors.border_selected)
101 })
102 .cursor_pointer()
103 .child(ThemePreviewTile::new(theme, theme_seed)),
104 )
105 .child(
106 h_flex()
107 .justify_center()
108 .items_baseline()
109 .child(Label::new(name).color(Color::Muted)),
110 )
111 });
112
113 return v_flex()
114 .child(
115 h_flex().justify_between().child(Label::new("Theme")).child(
116 h_flex()
117 .gap_2()
118 .child(
119 ToggleButtonGroup::single_row(
120 "theme-selector-onboarding-dark-light",
121 [
122 ToggleButtonSimple::new("Light", {
123 let appearance_state = appearance_state.clone();
124 move |_, _, cx| {
125 write_appearance_change(
126 &appearance_state,
127 Appearance::Light,
128 cx,
129 );
130 }
131 }),
132 ToggleButtonSimple::new("Dark", {
133 let appearance_state = appearance_state.clone();
134 move |_, _, cx| {
135 write_appearance_change(
136 &appearance_state,
137 Appearance::Dark,
138 cx,
139 );
140 }
141 }),
142 ],
143 )
144 .selected_index(selected_index)
145 .style(ui::ToggleButtonGroupStyle::Outlined)
146 .button_width(rems_from_px(64.)),
147 )
148 .child(
149 ToggleButtonGroup::single_row(
150 "theme-selector-onboarding-system",
151 [ToggleButtonSimple::new("System", {
152 let theme = theme_selection.clone();
153 move |_, _, cx| {
154 toggle_system_theme_mode(theme.clone(), appearance, cx);
155 }
156 })],
157 )
158 .selected_index((theme_mode != Some(ThemeMode::System)) as usize)
159 .style(ui::ToggleButtonGroupStyle::Outlined)
160 .button_width(rems_from_px(64.)),
161 ),
162 ),
163 )
164 .child(h_flex().justify_between().children(theme_previews));
165
166 fn write_appearance_change(
167 appearance_state: &Entity<Appearance>,
168 new_appearance: Appearance,
169 cx: &mut App,
170 ) {
171 appearance_state.update(cx, |appearance, _| {
172 *appearance = new_appearance;
173 });
174 let fs = <dyn Fs>::global(cx);
175
176 update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
177 if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) {
178 return;
179 }
180 let new_mode = match new_appearance {
181 Appearance::Light => ThemeMode::Light,
182 Appearance::Dark => ThemeMode::Dark,
183 };
184 settings.set_mode(new_mode);
185 });
186 }
187
188 fn toggle_system_theme_mode(
189 theme_selection: ThemeSelection,
190 appearance: Appearance,
191 cx: &mut App,
192 ) {
193 let fs = <dyn Fs>::global(cx);
194
195 update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
196 settings.theme = Some(match theme_selection {
197 ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic {
198 mode: ThemeMode::System,
199 light: theme_name.clone(),
200 dark: theme_name.clone(),
201 },
202 ThemeSelection::Dynamic {
203 mode: ThemeMode::System,
204 light,
205 dark,
206 } => {
207 let mode = match appearance {
208 Appearance::Light => ThemeMode::Light,
209 Appearance::Dark => ThemeMode::Dark,
210 };
211 ThemeSelection::Dynamic { mode, light, dark }
212 }
213
214 ThemeSelection::Dynamic {
215 mode: _,
216 light,
217 dark,
218 } => ThemeSelection::Dynamic {
219 mode: ThemeMode::System,
220 light,
221 dark,
222 },
223 });
224 });
225 }
226}
227
228fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
229 let fs = <dyn Fs>::global(cx);
230
231 update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
232 *setting = Some(keymap_base);
233 });
234}
235
236fn render_telemetry_section(cx: &App) -> impl IntoElement {
237 let fs = <dyn Fs>::global(cx);
238
239 v_flex()
240 .gap_4()
241 .child(Label::new("Telemetry").size(LabelSize::Large))
242 .child(SwitchField::new(
243 "onboarding-telemetry-metrics",
244 "Help Improve Zed",
245 Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()),
246 if TelemetrySettings::get_global(cx).metrics {
247 ui::ToggleState::Selected
248 } else {
249 ui::ToggleState::Unselected
250 },
251 {
252 let fs = fs.clone();
253 move |selection, _, cx| {
254 let enabled = match selection {
255 ToggleState::Selected => true,
256 ToggleState::Unselected => false,
257 ToggleState::Indeterminate => { return; },
258 };
259
260 update_settings_file::<TelemetrySettings>(
261 fs.clone(),
262 cx,
263 move |setting, _| setting.metrics = Some(enabled),
264 );
265 }},
266 ))
267 .child(SwitchField::new(
268 "onboarding-telemetry-crash-reports",
269 "Help Fix Zed",
270 Some("Send crash reports so we can fix critical issues fast.".into()),
271 if TelemetrySettings::get_global(cx).diagnostics {
272 ui::ToggleState::Selected
273 } else {
274 ui::ToggleState::Unselected
275 },
276 {
277 let fs = fs.clone();
278 move |selection, _, cx| {
279 let enabled = match selection {
280 ToggleState::Selected => true,
281 ToggleState::Unselected => false,
282 ToggleState::Indeterminate => { return; },
283 };
284
285 update_settings_file::<TelemetrySettings>(
286 fs.clone(),
287 cx,
288 move |setting, _| setting.diagnostics = Some(enabled),
289 );
290 }
291 }
292 ))
293}
294
295pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
296 let base_keymap = match BaseKeymap::get_global(cx) {
297 BaseKeymap::VSCode => Some(0),
298 BaseKeymap::JetBrains => Some(1),
299 BaseKeymap::SublimeText => Some(2),
300 BaseKeymap::Atom => Some(3),
301 BaseKeymap::Emacs => Some(4),
302 BaseKeymap::Cursor => Some(5),
303 BaseKeymap::TextMate | BaseKeymap::None => None,
304 };
305
306 v_flex()
307 .gap_6()
308 .child(render_theme_section(window, cx))
309 .child(
310 v_flex().gap_2().child(Label::new("Base Keymap")).child(
311 ToggleButtonGroup::two_rows(
312 "multiple_row_test",
313 [
314 ToggleButtonWithIcon::new("VS Code", IconName::AiZed, |_, _, cx| {
315 write_keymap_base(BaseKeymap::VSCode, cx);
316 }),
317 ToggleButtonWithIcon::new("Jetbrains", IconName::AiZed, |_, _, cx| {
318 write_keymap_base(BaseKeymap::JetBrains, cx);
319 }),
320 ToggleButtonWithIcon::new("Sublime Text", IconName::AiZed, |_, _, cx| {
321 write_keymap_base(BaseKeymap::SublimeText, cx);
322 }),
323 ],
324 [
325 ToggleButtonWithIcon::new("Atom", IconName::AiZed, |_, _, cx| {
326 write_keymap_base(BaseKeymap::Atom, cx);
327 }),
328 ToggleButtonWithIcon::new("Emacs", IconName::AiZed, |_, _, cx| {
329 write_keymap_base(BaseKeymap::Emacs, cx);
330 }),
331 ToggleButtonWithIcon::new("Cursor (Beta)", IconName::AiZed, |_, _, cx| {
332 write_keymap_base(BaseKeymap::Cursor, cx);
333 }),
334 ],
335 )
336 .when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap))
337 .button_width(rems_from_px(230.))
338 .style(ui::ToggleButtonGroupStyle::Outlined)
339 ),
340 )
341 .child(SwitchField::new(
342 "onboarding-vim-mode",
343 "Vim Mode",
344 Some("Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.".into()),
345 if VimModeSetting::get_global(cx).0 {
346 ui::ToggleState::Selected
347 } else {
348 ui::ToggleState::Unselected
349 },
350 {
351 let fs = <dyn Fs>::global(cx);
352 move |selection, _, cx| {
353 let enabled = match selection {
354 ToggleState::Selected => true,
355 ToggleState::Unselected => false,
356 ToggleState::Indeterminate => { return; },
357 };
358
359 update_settings_file::<VimModeSetting>(
360 fs.clone(),
361 cx,
362 move |setting, _| *setting = Some(enabled),
363 );
364 }
365 },
366 ))
367 .child(render_telemetry_section(cx))
368}