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, _| {
207 let mut theme_settings = store.get::<ThemeSettings>(None).clone();
208 let name = theme.as_ref().name.clone().into();
209 theme_settings.theme = theme::ThemeSelection::Static(theme::ThemeName(name));
210 store.override_global(theme_settings);
211 });
212 }
213}
214
215impl PickerDelegate for ThemeSelectorDelegate {
216 type ListItem = ui::ListItem;
217
218 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
219 "Select Theme...".into()
220 }
221
222 fn match_count(&self) -> usize {
223 self.matches.len()
224 }
225
226 fn confirm(
227 &mut self,
228 _: bool,
229 window: &mut Window,
230 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
231 ) {
232 self.selection_completed = true;
233
234 let theme_name = cx.theme().name.clone();
235
236 telemetry::event!("Settings Changed", setting = "theme", value = theme_name);
237
238 let appearance = Appearance::from(window.appearance());
239
240 update_settings_file(self.fs.clone(), cx, move |settings, _| {
241 theme::set_theme(settings, theme_name.to_string(), appearance);
242 });
243
244 self.selector
245 .update(cx, |_, cx| {
246 cx.emit(DismissEvent);
247 })
248 .ok();
249 }
250
251 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
252 if !self.selection_completed {
253 Self::set_theme(self.original_theme.clone(), cx);
254 self.selection_completed = true;
255 }
256
257 self.selector
258 .update(cx, |_, cx| cx.emit(DismissEvent))
259 .log_err();
260 }
261
262 fn selected_index(&self) -> usize {
263 self.selected_index
264 }
265
266 fn set_selected_index(
267 &mut self,
268 ix: usize,
269 _: &mut Window,
270 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
271 ) {
272 self.selected_index = ix;
273 self.selected_theme = self.show_selected_theme(cx);
274 }
275
276 fn update_matches(
277 &mut self,
278 query: String,
279 window: &mut Window,
280 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
281 ) -> gpui::Task<()> {
282 let background = cx.background_executor().clone();
283 let candidates = self
284 .themes
285 .iter()
286 .enumerate()
287 .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name))
288 .collect::<Vec<_>>();
289
290 cx.spawn_in(window, async move |this, cx| {
291 let matches = if query.is_empty() {
292 candidates
293 .into_iter()
294 .enumerate()
295 .map(|(index, candidate)| StringMatch {
296 candidate_id: index,
297 string: candidate.string,
298 positions: Vec::new(),
299 score: 0.0,
300 })
301 .collect()
302 } else {
303 match_strings(
304 &candidates,
305 &query,
306 false,
307 true,
308 100,
309 &Default::default(),
310 background,
311 )
312 .await
313 };
314
315 this.update(cx, |this, cx| {
316 this.delegate.matches = matches;
317 if query.is_empty() && this.delegate.selected_theme.is_none() {
318 this.delegate.selected_index = this
319 .delegate
320 .selected_index
321 .min(this.delegate.matches.len().saturating_sub(1));
322 } else if let Some(selected) = this.delegate.selected_theme.as_ref() {
323 this.delegate.selected_index = this
324 .delegate
325 .matches
326 .iter()
327 .enumerate()
328 .find(|(_, mtch)| mtch.string == selected.name)
329 .map(|(ix, _)| ix)
330 .unwrap_or_default();
331 } else {
332 this.delegate.selected_index = 0;
333 }
334 this.delegate.selected_theme = this.delegate.show_selected_theme(cx);
335 })
336 .log_err();
337 })
338 }
339
340 fn render_match(
341 &self,
342 ix: usize,
343 selected: bool,
344 _window: &mut Window,
345 _cx: &mut Context<Picker<Self>>,
346 ) -> Option<Self::ListItem> {
347 let theme_match = &self.matches.get(ix)?;
348
349 Some(
350 ListItem::new(ix)
351 .inset(true)
352 .spacing(ListItemSpacing::Sparse)
353 .toggle_state(selected)
354 .child(HighlightedLabel::new(
355 theme_match.string.clone(),
356 theme_match.positions.clone(),
357 )),
358 )
359 }
360
361 fn render_footer(
362 &self,
363 _: &mut Window,
364 cx: &mut Context<Picker<Self>>,
365 ) -> Option<gpui::AnyElement> {
366 Some(
367 h_flex()
368 .p_2()
369 .w_full()
370 .justify_between()
371 .gap_2()
372 .border_t_1()
373 .border_color(cx.theme().colors().border_variant)
374 .child(
375 Button::new("docs", "View Theme Docs")
376 .icon(IconName::ArrowUpRight)
377 .icon_position(IconPosition::End)
378 .icon_size(IconSize::Small)
379 .icon_color(Color::Muted)
380 .on_click(cx.listener(|_, _, _, cx| {
381 cx.open_url("https://zed.dev/docs/themes");
382 })),
383 )
384 .child(
385 Button::new("more-themes", "Install Themes").on_click(cx.listener({
386 move |_, _, window, cx| {
387 window.dispatch_action(
388 Box::new(Extensions {
389 category_filter: Some(ExtensionCategoryFilter::Themes),
390 id: None,
391 }),
392 cx,
393 );
394 }
395 })),
396 )
397 .into_any_element(),
398 )
399 }
400}