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().unwrap_or_default();
52
53 // let theme_mode = theme_selection.mode();
54 // TODO: Clean this up once the "System" button inside the
55 // toggle button group is done
56
57 let selected_index = match appearance {
58 Appearance::Light => 0,
59 Appearance::Dark => 1,
60 };
61
62 let theme_seed = 0xBEEF as f32;
63
64 const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
65 const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
66
67 let theme_names = match appearance {
68 Appearance::Light => LIGHT_THEMES,
69 Appearance::Dark => DARK_THEMES,
70 };
71 let themes = theme_names
72 .map(|theme_name| theme_registry.get(theme_name))
73 .map(Result::unwrap);
74
75 let theme_previews = themes.map(|theme| {
76 let is_selected = theme.name == current_theme_name;
77 let name = theme.name.clone();
78 let colors = cx.theme().colors();
79
80 v_flex()
81 .id(name.clone())
82 .w_full()
83 .items_center()
84 .gap_1()
85 .child(
86 div()
87 .w_full()
88 .border_2()
89 .border_color(colors.border_transparent)
90 .rounded(ThemePreviewTile::CORNER_RADIUS)
91 .map(|this| {
92 if is_selected {
93 this.border_color(colors.border_selected)
94 } else {
95 this.opacity(0.8).hover(|s| s.border_color(colors.border))
96 }
97 })
98 .child(ThemePreviewTile::new(theme.clone(), theme_seed)),
99 )
100 .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
101 .on_click({
102 let theme_name = theme.name.clone();
103 move |_, _, cx| {
104 let fs = <dyn Fs>::global(cx);
105 let theme_name = theme_name.clone();
106 update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
107 settings.set_theme(theme_name, appearance);
108 });
109 }
110 })
111 });
112
113 return v_flex()
114 .gap_2()
115 .child(
116 h_flex().justify_between().child(Label::new("Theme")).child(
117 ToggleButtonGroup::single_row(
118 "theme-selector-onboarding-dark-light",
119 [
120 ToggleButtonSimple::new("Light", {
121 let appearance_state = appearance_state.clone();
122 move |_, _, cx| {
123 write_appearance_change(&appearance_state, Appearance::Light, cx);
124 }
125 }),
126 ToggleButtonSimple::new("Dark", {
127 let appearance_state = appearance_state.clone();
128 move |_, _, cx| {
129 write_appearance_change(&appearance_state, Appearance::Dark, cx);
130 }
131 }),
132 // TODO: Properly put the System back as a button within this group
133 // Currently, given "System" is not an option in the Appearance enum,
134 // this button doesn't get selected
135 ToggleButtonSimple::new("System", {
136 let theme = theme_selection.clone();
137 move |_, _, cx| {
138 toggle_system_theme_mode(theme.clone(), appearance, cx);
139 }
140 })
141 .selected(theme_mode == ThemeMode::System),
142 ],
143 )
144 .selected_index(selected_index)
145 .style(ui::ToggleButtonGroupStyle::Outlined)
146 .button_width(rems_from_px(64.)),
147 ),
148 )
149 .child(h_flex().gap_4().justify_between().children(theme_previews));
150
151 fn write_appearance_change(
152 appearance_state: &Entity<Appearance>,
153 new_appearance: Appearance,
154 cx: &mut App,
155 ) {
156 let fs = <dyn Fs>::global(cx);
157 appearance_state.write(cx, new_appearance);
158
159 update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
160 if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) {
161 return;
162 }
163 let new_mode = match new_appearance {
164 Appearance::Light => ThemeMode::Light,
165 Appearance::Dark => ThemeMode::Dark,
166 };
167 settings.set_mode(new_mode);
168 });
169 }
170
171 fn toggle_system_theme_mode(
172 theme_selection: ThemeSelection,
173 appearance: Appearance,
174 cx: &mut App,
175 ) {
176 let fs = <dyn Fs>::global(cx);
177
178 update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
179 settings.theme = Some(match theme_selection {
180 ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic {
181 mode: ThemeMode::System,
182 light: theme_name.clone(),
183 dark: theme_name.clone(),
184 },
185 ThemeSelection::Dynamic {
186 mode: ThemeMode::System,
187 light,
188 dark,
189 } => {
190 let mode = match appearance {
191 Appearance::Light => ThemeMode::Light,
192 Appearance::Dark => ThemeMode::Dark,
193 };
194 ThemeSelection::Dynamic { mode, light, dark }
195 }
196 ThemeSelection::Dynamic {
197 mode: _,
198 light,
199 dark,
200 } => ThemeSelection::Dynamic {
201 mode: ThemeMode::System,
202 light,
203 dark,
204 },
205 });
206 });
207 }
208}
209
210fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
211 let fs = <dyn Fs>::global(cx);
212
213 update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
214 *setting = Some(keymap_base);
215 });
216}
217
218fn render_telemetry_section(cx: &App) -> impl IntoElement {
219 let fs = <dyn Fs>::global(cx);
220
221 v_flex()
222 .gap_4()
223 .child(Label::new("Telemetry").size(LabelSize::Large))
224 .child(SwitchField::new(
225 "onboarding-telemetry-metrics",
226 "Help Improve Zed",
227 Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()),
228 if TelemetrySettings::get_global(cx).metrics {
229 ui::ToggleState::Selected
230 } else {
231 ui::ToggleState::Unselected
232 },
233 {
234 let fs = fs.clone();
235 move |selection, _, cx| {
236 let enabled = match selection {
237 ToggleState::Selected => true,
238 ToggleState::Unselected => false,
239 ToggleState::Indeterminate => { return; },
240 };
241
242 update_settings_file::<TelemetrySettings>(
243 fs.clone(),
244 cx,
245 move |setting, _| setting.metrics = Some(enabled),
246 );
247 }},
248 ))
249 .child(SwitchField::new(
250 "onboarding-telemetry-crash-reports",
251 "Help Fix Zed",
252 Some("Send crash reports so we can fix critical issues fast.".into()),
253 if TelemetrySettings::get_global(cx).diagnostics {
254 ui::ToggleState::Selected
255 } else {
256 ui::ToggleState::Unselected
257 },
258 {
259 let fs = fs.clone();
260 move |selection, _, cx| {
261 let enabled = match selection {
262 ToggleState::Selected => true,
263 ToggleState::Unselected => false,
264 ToggleState::Indeterminate => { return; },
265 };
266
267 update_settings_file::<TelemetrySettings>(
268 fs.clone(),
269 cx,
270 move |setting, _| setting.diagnostics = Some(enabled),
271 );
272 }
273 }
274 ))
275}
276
277pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
278 let base_keymap = match BaseKeymap::get_global(cx) {
279 BaseKeymap::VSCode => Some(0),
280 BaseKeymap::JetBrains => Some(1),
281 BaseKeymap::SublimeText => Some(2),
282 BaseKeymap::Atom => Some(3),
283 BaseKeymap::Emacs => Some(4),
284 BaseKeymap::Cursor => Some(5),
285 BaseKeymap::TextMate | BaseKeymap::None => None,
286 };
287
288 v_flex()
289 .gap_6()
290 .child(render_theme_section(window, cx))
291 .child(
292 v_flex().gap_2().child(Label::new("Base Keymap")).child(
293 ToggleButtonGroup::two_rows(
294 "multiple_row_test",
295 [
296 ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
297 write_keymap_base(BaseKeymap::VSCode, cx);
298 }),
299 ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| {
300 write_keymap_base(BaseKeymap::JetBrains, cx);
301 }),
302 ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
303 write_keymap_base(BaseKeymap::SublimeText, cx);
304 }),
305 ],
306 [
307 ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
308 write_keymap_base(BaseKeymap::Atom, cx);
309 }),
310 ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
311 write_keymap_base(BaseKeymap::Emacs, cx);
312 }),
313 ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| {
314 write_keymap_base(BaseKeymap::Cursor, cx);
315 }),
316 ],
317 )
318 .when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap))
319 .button_width(rems_from_px(216.))
320 .size(ui::ToggleButtonGroupSize::Medium)
321 .style(ui::ToggleButtonGroupStyle::Outlined)
322 ),
323 )
324 .child(SwitchField::new(
325 "onboarding-vim-mode",
326 "Vim Mode",
327 Some("Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.".into()),
328 if VimModeSetting::get_global(cx).0 {
329 ui::ToggleState::Selected
330 } else {
331 ui::ToggleState::Unselected
332 },
333 {
334 let fs = <dyn Fs>::global(cx);
335 move |selection, _, cx| {
336 let enabled = match selection {
337 ToggleState::Selected => true,
338 ToggleState::Unselected => false,
339 ToggleState::Indeterminate => { return; },
340 };
341
342 update_settings_file::<VimModeSetting>(
343 fs.clone(),
344 cx,
345 move |setting, _| *setting = Some(enabled),
346 );
347 }
348 },
349 ))
350 .child(render_telemetry_section(cx))
351}