1mod icon_theme_selector;
2
3use fs::Fs;
4use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
5use gpui::{
6 App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity,
7 Window, actions,
8};
9use picker::{Picker, PickerDelegate};
10use settings::{SettingsStore, update_settings_file};
11use std::sync::Arc;
12use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
13use ui::{ListItem, ListItemSpacing, prelude::*, v_flex};
14use util::ResultExt;
15use workspace::{ModalView, Workspace, ui::HighlightedLabel, with_active_or_new_workspace};
16use zed_actions::{ExtensionCategoryFilter, Extensions};
17
18use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate};
19
20actions!(
21 theme_selector,
22 [
23 /// Reloads all themes from disk.
24 Reload
25 ]
26);
27
28pub fn init(cx: &mut App) {
29 cx.on_action(|action: &zed_actions::theme_selector::Toggle, cx| {
30 let action = action.clone();
31 with_active_or_new_workspace(cx, move |workspace, window, cx| {
32 toggle_theme_selector(workspace, &action, window, cx);
33 });
34 });
35 cx.on_action(|action: &zed_actions::icon_theme_selector::Toggle, cx| {
36 let action = action.clone();
37 with_active_or_new_workspace(cx, move |workspace, window, cx| {
38 toggle_icon_theme_selector(workspace, &action, window, cx);
39 });
40 });
41}
42
43fn toggle_theme_selector(
44 workspace: &mut Workspace,
45 toggle: &zed_actions::theme_selector::Toggle,
46 window: &mut Window,
47 cx: &mut Context<Workspace>,
48) {
49 let fs = workspace.app_state().fs.clone();
50 workspace.toggle_modal(window, cx, |window, cx| {
51 let delegate = ThemeSelectorDelegate::new(
52 cx.entity().downgrade(),
53 fs,
54 toggle.themes_filter.as_ref(),
55 cx,
56 );
57 ThemeSelector::new(delegate, window, cx)
58 });
59}
60
61fn toggle_icon_theme_selector(
62 workspace: &mut Workspace,
63 toggle: &zed_actions::icon_theme_selector::Toggle,
64 window: &mut Window,
65 cx: &mut Context<Workspace>,
66) {
67 let fs = workspace.app_state().fs.clone();
68 workspace.toggle_modal(window, cx, |window, cx| {
69 let delegate = IconThemeSelectorDelegate::new(
70 cx.entity().downgrade(),
71 fs,
72 toggle.themes_filter.as_ref(),
73 cx,
74 );
75 IconThemeSelector::new(delegate, window, cx)
76 });
77}
78
79impl ModalView for ThemeSelector {}
80
81struct ThemeSelector {
82 picker: Entity<Picker<ThemeSelectorDelegate>>,
83}
84
85impl EventEmitter<DismissEvent> for ThemeSelector {}
86
87impl Focusable for ThemeSelector {
88 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
89 self.picker.focus_handle(cx)
90 }
91}
92
93impl Render for ThemeSelector {
94 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
95 v_flex().w(rems(34.)).child(self.picker.clone())
96 }
97}
98
99impl ThemeSelector {
100 pub fn new(
101 delegate: ThemeSelectorDelegate,
102 window: &mut Window,
103 cx: &mut Context<Self>,
104 ) -> Self {
105 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
106 Self { picker }
107 }
108}
109
110struct ThemeSelectorDelegate {
111 fs: Arc<dyn Fs>,
112 themes: Vec<ThemeMeta>,
113 matches: Vec<StringMatch>,
114 original_theme: Arc<Theme>,
115 selection_completed: bool,
116 selected_theme: Option<Arc<Theme>>,
117 selected_index: usize,
118 selector: WeakEntity<ThemeSelector>,
119}
120
121impl ThemeSelectorDelegate {
122 fn new(
123 selector: WeakEntity<ThemeSelector>,
124 fs: Arc<dyn Fs>,
125 themes_filter: Option<&Vec<String>>,
126 cx: &mut Context<ThemeSelector>,
127 ) -> Self {
128 let original_theme = cx.theme().clone();
129
130 let registry = ThemeRegistry::global(cx);
131 let mut themes = registry
132 .list()
133 .into_iter()
134 .filter(|meta| {
135 if let Some(theme_filter) = themes_filter {
136 theme_filter.contains(&meta.name.to_string())
137 } else {
138 true
139 }
140 })
141 .collect::<Vec<_>>();
142
143 themes.sort_unstable_by(|a, b| {
144 a.appearance
145 .is_light()
146 .cmp(&b.appearance.is_light())
147 .then(a.name.cmp(&b.name))
148 });
149 let matches = themes
150 .iter()
151 .map(|meta| StringMatch {
152 candidate_id: 0,
153 score: 0.0,
154 positions: Default::default(),
155 string: meta.name.to_string(),
156 })
157 .collect();
158 let mut this = Self {
159 fs,
160 themes,
161 matches,
162 original_theme: original_theme.clone(),
163 selected_index: 0,
164 selection_completed: false,
165 selected_theme: None,
166 selector,
167 };
168
169 this.select_if_matching(&original_theme.name);
170 this
171 }
172
173 fn show_selected_theme(
174 &mut self,
175 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
176 ) -> Option<Arc<Theme>> {
177 if let Some(mat) = self.matches.get(self.selected_index) {
178 let registry = ThemeRegistry::global(cx);
179 match registry.get(&mat.string) {
180 Ok(theme) => {
181 Self::set_theme(theme.clone(), cx);
182 Some(theme)
183 }
184 Err(error) => {
185 log::error!("error loading theme {}: {}", mat.string, error);
186 None
187 }
188 }
189 } else {
190 None
191 }
192 }
193
194 fn select_if_matching(&mut self, theme_name: &str) {
195 self.selected_index = self
196 .matches
197 .iter()
198 .position(|mat| mat.string == theme_name)
199 .unwrap_or(self.selected_index);
200 }
201
202 fn set_theme(theme: Arc<Theme>, cx: &mut App) {
203 SettingsStore::update_global(cx, |store, cx| {
204 let mut theme_settings = store.get::<ThemeSettings>(None).clone();
205 theme_settings.active_theme = theme;
206 theme_settings.apply_theme_overrides();
207 store.override_global(theme_settings);
208 cx.refresh_windows();
209 });
210 }
211}
212
213impl PickerDelegate for ThemeSelectorDelegate {
214 type ListItem = ui::ListItem;
215
216 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
217 "Select Theme...".into()
218 }
219
220 fn match_count(&self) -> usize {
221 self.matches.len()
222 }
223
224 fn confirm(
225 &mut self,
226 _: bool,
227 window: &mut Window,
228 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
229 ) {
230 self.selection_completed = true;
231
232 let theme_name = cx.theme().name.clone();
233
234 telemetry::event!("Settings Changed", setting = "theme", value = theme_name);
235
236 let appearance = Appearance::from(window.appearance());
237
238 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
239 settings.set_theme(theme_name.to_string(), appearance);
240 });
241
242 self.selector
243 .update(cx, |_, cx| {
244 cx.emit(DismissEvent);
245 })
246 .ok();
247 }
248
249 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
250 if !self.selection_completed {
251 Self::set_theme(self.original_theme.clone(), cx);
252 self.selection_completed = true;
253 }
254
255 self.selector
256 .update(cx, |_, cx| cx.emit(DismissEvent))
257 .log_err();
258 }
259
260 fn selected_index(&self) -> usize {
261 self.selected_index
262 }
263
264 fn set_selected_index(
265 &mut self,
266 ix: usize,
267 _: &mut Window,
268 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
269 ) {
270 self.selected_index = ix;
271 self.selected_theme = self.show_selected_theme(cx);
272 }
273
274 fn update_matches(
275 &mut self,
276 query: String,
277 window: &mut Window,
278 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
279 ) -> gpui::Task<()> {
280 let background = cx.background_executor().clone();
281 let candidates = self
282 .themes
283 .iter()
284 .enumerate()
285 .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name))
286 .collect::<Vec<_>>();
287
288 cx.spawn_in(window, async move |this, cx| {
289 let matches = if query.is_empty() {
290 candidates
291 .into_iter()
292 .enumerate()
293 .map(|(index, candidate)| StringMatch {
294 candidate_id: index,
295 string: candidate.string,
296 positions: Vec::new(),
297 score: 0.0,
298 })
299 .collect()
300 } else {
301 match_strings(
302 &candidates,
303 &query,
304 false,
305 true,
306 100,
307 &Default::default(),
308 background,
309 )
310 .await
311 };
312
313 this.update(cx, |this, cx| {
314 this.delegate.matches = matches;
315 if query.is_empty() && this.delegate.selected_theme.is_none() {
316 this.delegate.selected_index = this
317 .delegate
318 .selected_index
319 .min(this.delegate.matches.len().saturating_sub(1));
320 } else if let Some(selected) = this.delegate.selected_theme.as_ref() {
321 this.delegate.selected_index = this
322 .delegate
323 .matches
324 .iter()
325 .enumerate()
326 .find(|(_, mtch)| mtch.string == selected.name)
327 .map(|(ix, _)| ix)
328 .unwrap_or_default();
329 } else {
330 this.delegate.selected_index = 0;
331 }
332 this.delegate.selected_theme = this.delegate.show_selected_theme(cx);
333 })
334 .log_err();
335 })
336 }
337
338 fn render_match(
339 &self,
340 ix: usize,
341 selected: bool,
342 _window: &mut Window,
343 _cx: &mut Context<Picker<Self>>,
344 ) -> Option<Self::ListItem> {
345 let theme_match = &self.matches[ix];
346
347 Some(
348 ListItem::new(ix)
349 .inset(true)
350 .spacing(ListItemSpacing::Sparse)
351 .toggle_state(selected)
352 .child(HighlightedLabel::new(
353 theme_match.string.clone(),
354 theme_match.positions.clone(),
355 )),
356 )
357 }
358
359 fn render_footer(
360 &self,
361 _: &mut Window,
362 cx: &mut Context<Picker<Self>>,
363 ) -> Option<gpui::AnyElement> {
364 Some(
365 h_flex()
366 .p_2()
367 .w_full()
368 .justify_between()
369 .gap_2()
370 .border_t_1()
371 .border_color(cx.theme().colors().border_variant)
372 .child(
373 Button::new("docs", "View Theme Docs")
374 .icon(IconName::ArrowUpRight)
375 .icon_position(IconPosition::End)
376 .icon_size(IconSize::XSmall)
377 .icon_color(Color::Muted)
378 .on_click(cx.listener(|_, _, _, cx| {
379 cx.open_url("https://zed.dev/docs/themes");
380 })),
381 )
382 .child(
383 Button::new("more-themes", "Install Themes").on_click(cx.listener({
384 move |_, _, window, cx| {
385 window.dispatch_action(
386 Box::new(Extensions {
387 category_filter: Some(ExtensionCategoryFilter::Themes),
388 id: None,
389 }),
390 cx,
391 );
392 }
393 })),
394 )
395 .into_any_element(),
396 )
397 }
398}