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