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