1use client::telemetry::Telemetry;
2use fs::Fs;
3use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
4use gpui::{
5 actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, UpdateGlobal, View,
6 ViewContext, VisualContext, WeakView,
7};
8use picker::{Picker, PickerDelegate};
9use settings::{update_settings_file, SettingsStore};
10use std::sync::Arc;
11use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
12use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
13use util::ResultExt;
14use workspace::{ui::HighlightedLabel, ModalView, Workspace};
15use zed_actions::theme_selector::Toggle;
16
17actions!(theme_selector, [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: &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 let delegate = ThemeSelectorDelegate::new(
33 cx.view().downgrade(),
34 fs,
35 telemetry,
36 toggle.themes_filter.as_ref(),
37 cx,
38 );
39 ThemeSelector::new(delegate, cx)
40 });
41}
42
43impl ModalView for ThemeSelector {}
44
45pub struct ThemeSelector {
46 picker: View<Picker<ThemeSelectorDelegate>>,
47}
48
49impl EventEmitter<DismissEvent> for ThemeSelector {}
50
51impl FocusableView for ThemeSelector {
52 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
53 self.picker.focus_handle(cx)
54 }
55}
56
57impl Render for ThemeSelector {
58 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
59 v_flex().w(rems(34.)).child(self.picker.clone())
60 }
61}
62
63impl ThemeSelector {
64 pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
65 let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
66 Self { picker }
67 }
68}
69
70pub struct ThemeSelectorDelegate {
71 fs: Arc<dyn Fs>,
72 themes: Vec<ThemeMeta>,
73 matches: Vec<StringMatch>,
74 original_theme: Arc<Theme>,
75 selection_completed: bool,
76 selected_index: usize,
77 telemetry: Arc<Telemetry>,
78 view: WeakView<ThemeSelector>,
79}
80
81impl ThemeSelectorDelegate {
82 fn new(
83 weak_view: WeakView<ThemeSelector>,
84 fs: Arc<dyn Fs>,
85 telemetry: Arc<Telemetry>,
86 themes_filter: Option<&Vec<String>>,
87 cx: &mut ViewContext<ThemeSelector>,
88 ) -> Self {
89 let original_theme = cx.theme().clone();
90
91 let registry = ThemeRegistry::global(cx);
92 let mut themes = registry
93 .list()
94 .into_iter()
95 .filter(|meta| {
96 if let Some(theme_filter) = themes_filter {
97 theme_filter.contains(&meta.name.to_string())
98 } else {
99 true
100 }
101 })
102 .collect::<Vec<_>>();
103
104 themes.sort_unstable_by(|a, b| {
105 a.appearance
106 .is_light()
107 .cmp(&b.appearance.is_light())
108 .then(a.name.cmp(&b.name))
109 });
110 let matches = themes
111 .iter()
112 .map(|meta| StringMatch {
113 candidate_id: 0,
114 score: 0.0,
115 positions: Default::default(),
116 string: meta.name.to_string(),
117 })
118 .collect();
119 let mut this = Self {
120 fs,
121 themes,
122 matches,
123 original_theme: original_theme.clone(),
124 selected_index: 0,
125 selection_completed: false,
126 telemetry,
127 view: weak_view,
128 };
129
130 this.select_if_matching(&original_theme.name);
131 this
132 }
133
134 fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
135 if let Some(mat) = self.matches.get(self.selected_index) {
136 let registry = ThemeRegistry::global(cx);
137 match registry.get(&mat.string) {
138 Ok(theme) => {
139 Self::set_theme(theme, cx);
140 }
141 Err(error) => {
142 log::error!("error loading theme {}: {}", mat.string, error)
143 }
144 }
145 }
146 }
147
148 fn select_if_matching(&mut self, theme_name: &str) {
149 self.selected_index = self
150 .matches
151 .iter()
152 .position(|mat| mat.string == theme_name)
153 .unwrap_or(self.selected_index);
154 }
155
156 fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
157 SettingsStore::update_global(cx, |store, cx| {
158 let mut theme_settings = store.get::<ThemeSettings>(None).clone();
159 theme_settings.active_theme = theme;
160 theme_settings.apply_theme_overrides();
161 store.override_global(theme_settings);
162 cx.refresh();
163 });
164 }
165}
166
167impl PickerDelegate for ThemeSelectorDelegate {
168 type ListItem = ui::ListItem;
169
170 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
171 "Select Theme...".into()
172 }
173
174 fn match_count(&self) -> usize {
175 self.matches.len()
176 }
177
178 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
179 self.selection_completed = true;
180
181 let theme_name = cx.theme().name.clone();
182
183 self.telemetry
184 .report_setting_event("theme", theme_name.to_string());
185
186 let appearance = Appearance::from(cx.appearance());
187
188 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
189 settings.set_theme(theme_name.to_string(), appearance);
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}