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