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 appearance_state.update(cx, |appearance, _| {
157 *appearance = new_appearance;
158 });
159 let fs = <dyn Fs>::global(cx);
160
161 update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
162 if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) {
163 return;
164 }
165 let new_mode = match new_appearance {
166 Appearance::Light => ThemeMode::Light,
167 Appearance::Dark => ThemeMode::Dark,
168 };
169 settings.set_mode(new_mode);
170 });
171 }
172
173 fn toggle_system_theme_mode(
174 theme_selection: ThemeSelection,
175 appearance: Appearance,
176 cx: &mut App,
177 ) {
178 let fs = <dyn Fs>::global(cx);
179
180 update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
181 settings.theme = Some(match theme_selection {
182 ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic {
183 mode: ThemeMode::System,
184 light: theme_name.clone(),
185 dark: theme_name.clone(),
186 },
187 ThemeSelection::Dynamic {
188 mode: ThemeMode::System,
189 light,
190 dark,
191 } => {
192 let mode = match appearance {
193 Appearance::Light => ThemeMode::Light,
194 Appearance::Dark => ThemeMode::Dark,
195 };
196 ThemeSelection::Dynamic { mode, light, dark }
197 }
198 ThemeSelection::Dynamic {
199 mode: _,
200 light,
201 dark,
202 } => ThemeSelection::Dynamic {
203 mode: ThemeMode::System,
204 light,
205 dark,
206 },
207 });
208 });
209 }
210}
211
212fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
213 let fs = <dyn Fs>::global(cx);
214
215 update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
216 *setting = Some(keymap_base);
217 });
218}
219
220fn render_telemetry_section(cx: &App) -> impl IntoElement {
221 let fs = <dyn Fs>::global(cx);
222
223 v_flex()
224 .gap_4()
225 .child(Label::new("Telemetry").size(LabelSize::Large))
226 .child(SwitchField::new(
227 "onboarding-telemetry-metrics",
228 "Help Improve Zed",
229 Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()),
230 if TelemetrySettings::get_global(cx).metrics {
231 ui::ToggleState::Selected
232 } else {
233 ui::ToggleState::Unselected
234 },
235 {
236 let fs = fs.clone();
237 move |selection, _, cx| {
238 let enabled = match selection {
239 ToggleState::Selected => true,
240 ToggleState::Unselected => false,
241 ToggleState::Indeterminate => { return; },
242 };
243
244 update_settings_file::<TelemetrySettings>(
245 fs.clone(),
246 cx,
247 move |setting, _| setting.metrics = Some(enabled),
248 );
249 }},
250 ))
251 .child(SwitchField::new(
252 "onboarding-telemetry-crash-reports",
253 "Help Fix Zed",
254 Some("Send crash reports so we can fix critical issues fast.".into()),
255 if TelemetrySettings::get_global(cx).diagnostics {
256 ui::ToggleState::Selected
257 } else {
258 ui::ToggleState::Unselected
259 },
260 {
261 let fs = fs.clone();
262 move |selection, _, cx| {
263 let enabled = match selection {
264 ToggleState::Selected => true,
265 ToggleState::Unselected => false,
266 ToggleState::Indeterminate => { return; },
267 };
268
269 update_settings_file::<TelemetrySettings>(
270 fs.clone(),
271 cx,
272 move |setting, _| setting.diagnostics = Some(enabled),
273 );
274 }
275 }
276 ))
277}
278
279pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
280 let base_keymap = match BaseKeymap::get_global(cx) {
281 BaseKeymap::VSCode => Some(0),
282 BaseKeymap::JetBrains => Some(1),
283 BaseKeymap::SublimeText => Some(2),
284 BaseKeymap::Atom => Some(3),
285 BaseKeymap::Emacs => Some(4),
286 BaseKeymap::Cursor => Some(5),
287 BaseKeymap::TextMate | BaseKeymap::None => None,
288 };
289
290 v_flex()
291 .gap_6()
292 .child(render_theme_section(window, cx))
293 .child(
294 v_flex().gap_2().child(Label::new("Base Keymap")).child(
295 ToggleButtonGroup::two_rows(
296 "multiple_row_test",
297 [
298 ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
299 write_keymap_base(BaseKeymap::VSCode, cx);
300 }),
301 ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| {
302 write_keymap_base(BaseKeymap::JetBrains, cx);
303 }),
304 ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
305 write_keymap_base(BaseKeymap::SublimeText, cx);
306 }),
307 ],
308 [
309 ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
310 write_keymap_base(BaseKeymap::Atom, cx);
311 }),
312 ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
313 write_keymap_base(BaseKeymap::Emacs, cx);
314 }),
315 ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| {
316 write_keymap_base(BaseKeymap::Cursor, cx);
317 }),
318 ],
319 )
320 .when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap))
321 .button_width(rems_from_px(216.))
322 .size(ui::ToggleButtonGroupSize::Medium)
323 .style(ui::ToggleButtonGroupStyle::Outlined)
324 ),
325 )
326 .child(SwitchField::new(
327 "onboarding-vim-mode",
328 "Vim Mode",
329 Some("Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.".into()),
330 if VimModeSetting::get_global(cx).0 {
331 ui::ToggleState::Selected
332 } else {
333 ui::ToggleState::Unselected
334 },
335 {
336 let fs = <dyn Fs>::global(cx);
337 move |selection, _, cx| {
338 let enabled = match selection {
339 ToggleState::Selected => true,
340 ToggleState::Unselected => false,
341 ToggleState::Indeterminate => { return; },
342 };
343
344 update_settings_file::<VimModeSetting>(
345 fs.clone(),
346 cx,
347 move |setting, _| *setting = Some(enabled),
348 );
349 }
350 },
351 ))
352 .child(render_telemetry_section(cx))
353}