1use client::telemetry::Telemetry;
2use feature_flags::FeatureFlagAppExt;
3use fs::Fs;
4use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
5use gpui::{
6 actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, View, ViewContext,
7 VisualContext, WeakView,
8};
9use picker::{Picker, PickerDelegate};
10use settings::{update_settings_file, SettingsStore};
11use std::sync::Arc;
12use theme::{
13 Appearance, Theme, ThemeMeta, ThemeMode, ThemeRegistry, ThemeSelection, ThemeSettings,
14};
15use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
16use util::ResultExt;
17use workspace::{ui::HighlightedLabel, ModalView, Workspace};
18
19actions!(theme_selector, [Toggle, Reload]);
20
21pub fn init(cx: &mut AppContext) {
22 cx.observe_new_views(
23 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
24 workspace.register_action(toggle);
25 },
26 )
27 .detach();
28}
29
30pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
31 let fs = workspace.app_state().fs.clone();
32 let telemetry = workspace.client().telemetry().clone();
33 workspace.toggle_modal(cx, |cx| {
34 ThemeSelector::new(
35 ThemeSelectorDelegate::new(cx.view().downgrade(), fs, telemetry, cx),
36 cx,
37 )
38 });
39}
40
41impl ModalView for ThemeSelector {}
42
43pub struct ThemeSelector {
44 picker: View<Picker<ThemeSelectorDelegate>>,
45}
46
47impl EventEmitter<DismissEvent> for ThemeSelector {}
48
49impl FocusableView for ThemeSelector {
50 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
51 self.picker.focus_handle(cx)
52 }
53}
54
55impl Render for ThemeSelector {
56 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
57 v_flex().w(rems(34.)).child(self.picker.clone())
58 }
59}
60
61impl ThemeSelector {
62 pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
63 let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
64 Self { picker }
65 }
66}
67
68pub struct ThemeSelectorDelegate {
69 fs: Arc<dyn Fs>,
70 themes: Vec<ThemeMeta>,
71 matches: Vec<StringMatch>,
72 original_theme: Arc<Theme>,
73 selection_completed: bool,
74 selected_index: usize,
75 telemetry: Arc<Telemetry>,
76 view: WeakView<ThemeSelector>,
77}
78
79impl ThemeSelectorDelegate {
80 fn new(
81 weak_view: WeakView<ThemeSelector>,
82 fs: Arc<dyn Fs>,
83 telemetry: Arc<Telemetry>,
84 cx: &mut ViewContext<ThemeSelector>,
85 ) -> Self {
86 let original_theme = cx.theme().clone();
87
88 let staff_mode = cx.is_staff();
89 let registry = ThemeRegistry::global(cx);
90 let mut themes = registry.list(staff_mode);
91 themes.sort_unstable_by(|a, b| {
92 a.appearance
93 .is_light()
94 .cmp(&b.appearance.is_light())
95 .then(a.name.cmp(&b.name))
96 });
97 let matches = themes
98 .iter()
99 .map(|meta| StringMatch {
100 candidate_id: 0,
101 score: 0.0,
102 positions: Default::default(),
103 string: meta.name.to_string(),
104 })
105 .collect();
106 let mut this = Self {
107 fs,
108 themes,
109 matches,
110 original_theme: original_theme.clone(),
111 selected_index: 0,
112 selection_completed: false,
113 telemetry,
114 view: weak_view,
115 };
116 this.select_if_matching(&original_theme.name);
117 this
118 }
119
120 fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
121 if let Some(mat) = self.matches.get(self.selected_index) {
122 let registry = ThemeRegistry::global(cx);
123 match registry.get(&mat.string) {
124 Ok(theme) => {
125 Self::set_theme(theme, cx);
126 }
127 Err(error) => {
128 log::error!("error loading theme {}: {}", mat.string, error)
129 }
130 }
131 }
132 }
133
134 fn select_if_matching(&mut self, theme_name: &str) {
135 self.selected_index = self
136 .matches
137 .iter()
138 .position(|mat| mat.string == theme_name)
139 .unwrap_or(self.selected_index);
140 }
141
142 fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
143 cx.update_global(|store: &mut SettingsStore, cx| {
144 let mut theme_settings = store.get::<ThemeSettings>(None).clone();
145 theme_settings.active_theme = theme;
146 theme_settings.apply_theme_overrides();
147 store.override_global(theme_settings);
148 cx.refresh();
149 });
150 }
151}
152
153impl PickerDelegate for ThemeSelectorDelegate {
154 type ListItem = ui::ListItem;
155
156 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
157 "Select Theme...".into()
158 }
159
160 fn match_count(&self) -> usize {
161 self.matches.len()
162 }
163
164 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
165 self.selection_completed = true;
166
167 let theme_name = cx.theme().name.clone();
168
169 self.telemetry
170 .report_setting_event("theme", theme_name.to_string());
171
172 let appearance = Appearance::from(cx.appearance());
173
174 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
175 if let Some(selection) = settings.theme.as_mut() {
176 let theme_to_update = match selection {
177 ThemeSelection::Static(theme) => theme,
178 ThemeSelection::Dynamic { mode, light, dark } => match mode {
179 ThemeMode::Light => light,
180 ThemeMode::Dark => dark,
181 ThemeMode::System => match appearance {
182 Appearance::Light => light,
183 Appearance::Dark => dark,
184 },
185 },
186 };
187
188 *theme_to_update = theme_name.to_string();
189 } else {
190 settings.theme = Some(ThemeSelection::Static(theme_name.to_string()));
191 }
192 });
193
194 self.view
195 .update(cx, |_, cx| {
196 cx.emit(DismissEvent);
197 })
198 .ok();
199 }
200
201 fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
202 if !self.selection_completed {
203 Self::set_theme(self.original_theme.clone(), cx);
204 self.selection_completed = true;
205 }
206
207 self.view
208 .update(cx, |_, cx| cx.emit(DismissEvent))
209 .log_err();
210 }
211
212 fn selected_index(&self) -> usize {
213 self.selected_index
214 }
215
216 fn set_selected_index(
217 &mut self,
218 ix: usize,
219 cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
220 ) {
221 self.selected_index = ix;
222 self.show_selected_theme(cx);
223 }
224
225 fn update_matches(
226 &mut self,
227 query: String,
228 cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
229 ) -> gpui::Task<()> {
230 let background = cx.background_executor().clone();
231 let candidates = self
232 .themes
233 .iter()
234 .enumerate()
235 .map(|(id, meta)| StringMatchCandidate {
236 id,
237 char_bag: meta.name.as_ref().into(),
238 string: meta.name.to_string(),
239 })
240 .collect::<Vec<_>>();
241
242 cx.spawn(|this, mut cx| async move {
243 let matches = if query.is_empty() {
244 candidates
245 .into_iter()
246 .enumerate()
247 .map(|(index, candidate)| StringMatch {
248 candidate_id: index,
249 string: candidate.string,
250 positions: Vec::new(),
251 score: 0.0,
252 })
253 .collect()
254 } else {
255 match_strings(
256 &candidates,
257 &query,
258 false,
259 100,
260 &Default::default(),
261 background,
262 )
263 .await
264 };
265
266 this.update(&mut cx, |this, cx| {
267 this.delegate.matches = matches;
268 this.delegate.selected_index = this
269 .delegate
270 .selected_index
271 .min(this.delegate.matches.len().saturating_sub(1));
272 this.delegate.show_selected_theme(cx);
273 })
274 .log_err();
275 })
276 }
277
278 fn render_match(
279 &self,
280 ix: usize,
281 selected: bool,
282 _cx: &mut ViewContext<Picker<Self>>,
283 ) -> Option<Self::ListItem> {
284 let theme_match = &self.matches[ix];
285
286 Some(
287 ListItem::new(ix)
288 .inset(true)
289 .spacing(ListItemSpacing::Sparse)
290 .selected(selected)
291 .child(HighlightedLabel::new(
292 theme_match.string.clone(),
293 theme_match.positions.clone(),
294 )),
295 )
296 }
297}