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