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