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