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