1mod icon_theme_selector;
2
3use fs::Fs;
4use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
5use gpui::{
6 actions, App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal,
7 WeakEntity, Window,
8};
9use picker::{Picker, PickerDelegate};
10use settings::{update_settings_file, SettingsStore};
11use std::sync::Arc;
12use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
13use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
14use util::ResultExt;
15use workspace::{ui::HighlightedLabel, ModalView, Workspace};
16
17use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate};
18
19actions!(theme_selector, [Reload]);
20
21pub fn init(cx: &mut App) {
22 cx.observe_new(
23 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
24 workspace
25 .register_action(toggle_theme_selector)
26 .register_action(toggle_icon_theme_selector);
27 },
28 )
29 .detach();
30}
31
32fn toggle_theme_selector(
33 workspace: &mut Workspace,
34 toggle: &zed_actions::theme_selector::Toggle,
35 window: &mut Window,
36 cx: &mut Context<Workspace>,
37) {
38 let fs = workspace.app_state().fs.clone();
39 workspace.toggle_modal(window, cx, |window, cx| {
40 let delegate = ThemeSelectorDelegate::new(
41 cx.entity().downgrade(),
42 fs,
43 toggle.themes_filter.as_ref(),
44 cx,
45 );
46 ThemeSelector::new(delegate, window, cx)
47 });
48}
49
50fn toggle_icon_theme_selector(
51 workspace: &mut Workspace,
52 toggle: &zed_actions::icon_theme_selector::Toggle,
53 window: &mut Window,
54 cx: &mut Context<Workspace>,
55) {
56 let fs = workspace.app_state().fs.clone();
57 workspace.toggle_modal(window, cx, |window, cx| {
58 let delegate = IconThemeSelectorDelegate::new(
59 cx.entity().downgrade(),
60 fs,
61 toggle.themes_filter.as_ref(),
62 cx,
63 );
64 IconThemeSelector::new(delegate, window, cx)
65 });
66}
67
68impl ModalView for ThemeSelector {}
69
70struct ThemeSelector {
71 picker: Entity<Picker<ThemeSelectorDelegate>>,
72}
73
74impl EventEmitter<DismissEvent> for ThemeSelector {}
75
76impl Focusable for ThemeSelector {
77 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
78 self.picker.focus_handle(cx)
79 }
80}
81
82impl Render for ThemeSelector {
83 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
84 v_flex().w(rems(34.)).child(self.picker.clone())
85 }
86}
87
88impl ThemeSelector {
89 pub fn new(
90 delegate: ThemeSelectorDelegate,
91 window: &mut Window,
92 cx: &mut Context<Self>,
93 ) -> Self {
94 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
95 Self { picker }
96 }
97}
98
99struct ThemeSelectorDelegate {
100 fs: Arc<dyn Fs>,
101 themes: Vec<ThemeMeta>,
102 matches: Vec<StringMatch>,
103 original_theme: Arc<Theme>,
104 selection_completed: bool,
105 selected_index: usize,
106 selector: WeakEntity<ThemeSelector>,
107}
108
109impl ThemeSelectorDelegate {
110 fn new(
111 selector: WeakEntity<ThemeSelector>,
112 fs: Arc<dyn Fs>,
113 themes_filter: Option<&Vec<String>>,
114 cx: &mut Context<ThemeSelector>,
115 ) -> Self {
116 let original_theme = cx.theme().clone();
117
118 let registry = ThemeRegistry::global(cx);
119 let mut themes = registry
120 .list()
121 .into_iter()
122 .filter(|meta| {
123 if let Some(theme_filter) = themes_filter {
124 theme_filter.contains(&meta.name.to_string())
125 } else {
126 true
127 }
128 })
129 .collect::<Vec<_>>();
130
131 themes.sort_unstable_by(|a, b| {
132 a.appearance
133 .is_light()
134 .cmp(&b.appearance.is_light())
135 .then(a.name.cmp(&b.name))
136 });
137 let matches = themes
138 .iter()
139 .map(|meta| StringMatch {
140 candidate_id: 0,
141 score: 0.0,
142 positions: Default::default(),
143 string: meta.name.to_string(),
144 })
145 .collect();
146 let mut this = Self {
147 fs,
148 themes,
149 matches,
150 original_theme: original_theme.clone(),
151 selected_index: 0,
152 selection_completed: false,
153 selector,
154 };
155
156 this.select_if_matching(&original_theme.name);
157 this
158 }
159
160 fn show_selected_theme(&mut self, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
161 if let Some(mat) = self.matches.get(self.selected_index) {
162 let registry = ThemeRegistry::global(cx);
163 match registry.get(&mat.string) {
164 Ok(theme) => {
165 Self::set_theme(theme, cx);
166 }
167 Err(error) => {
168 log::error!("error loading theme {}: {}", mat.string, error)
169 }
170 }
171 }
172 }
173
174 fn select_if_matching(&mut self, theme_name: &str) {
175 self.selected_index = self
176 .matches
177 .iter()
178 .position(|mat| mat.string == theme_name)
179 .unwrap_or(self.selected_index);
180 }
181
182 fn set_theme(theme: Arc<Theme>, cx: &mut App) {
183 SettingsStore::update_global(cx, |store, cx| {
184 let mut theme_settings = store.get::<ThemeSettings>(None).clone();
185 theme_settings.active_theme = theme;
186 theme_settings.apply_theme_overrides();
187 store.override_global(theme_settings);
188 cx.refresh_windows();
189 });
190 }
191}
192
193impl PickerDelegate for ThemeSelectorDelegate {
194 type ListItem = ui::ListItem;
195
196 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
197 "Select Theme...".into()
198 }
199
200 fn match_count(&self) -> usize {
201 self.matches.len()
202 }
203
204 fn confirm(
205 &mut self,
206 _: bool,
207 window: &mut Window,
208 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
209 ) {
210 self.selection_completed = true;
211
212 let theme_name = cx.theme().name.clone();
213
214 telemetry::event!("Settings Changed", setting = "theme", value = theme_name);
215
216 let appearance = Appearance::from(window.appearance());
217
218 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
219 settings.set_theme(theme_name.to_string(), appearance);
220 });
221
222 self.selector
223 .update(cx, |_, cx| {
224 cx.emit(DismissEvent);
225 })
226 .ok();
227 }
228
229 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
230 if !self.selection_completed {
231 Self::set_theme(self.original_theme.clone(), cx);
232 self.selection_completed = true;
233 }
234
235 self.selector
236 .update(cx, |_, cx| cx.emit(DismissEvent))
237 .log_err();
238 }
239
240 fn selected_index(&self) -> usize {
241 self.selected_index
242 }
243
244 fn set_selected_index(
245 &mut self,
246 ix: usize,
247 _: &mut Window,
248 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
249 ) {
250 self.selected_index = ix;
251 self.show_selected_theme(cx);
252 }
253
254 fn update_matches(
255 &mut self,
256 query: String,
257 window: &mut Window,
258 cx: &mut Context<Picker<ThemeSelectorDelegate>>,
259 ) -> gpui::Task<()> {
260 let background = cx.background_executor().clone();
261 let candidates = self
262 .themes
263 .iter()
264 .enumerate()
265 .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name))
266 .collect::<Vec<_>>();
267
268 cx.spawn_in(window, |this, mut cx| async move {
269 let matches = if query.is_empty() {
270 candidates
271 .into_iter()
272 .enumerate()
273 .map(|(index, candidate)| StringMatch {
274 candidate_id: index,
275 string: candidate.string,
276 positions: Vec::new(),
277 score: 0.0,
278 })
279 .collect()
280 } else {
281 match_strings(
282 &candidates,
283 &query,
284 false,
285 100,
286 &Default::default(),
287 background,
288 )
289 .await
290 };
291
292 this.update(&mut cx, |this, cx| {
293 this.delegate.matches = matches;
294 this.delegate.selected_index = this
295 .delegate
296 .selected_index
297 .min(this.delegate.matches.len().saturating_sub(1));
298 this.delegate.show_selected_theme(cx);
299 })
300 .log_err();
301 })
302 }
303
304 fn render_match(
305 &self,
306 ix: usize,
307 selected: bool,
308 _window: &mut Window,
309 _cx: &mut Context<Picker<Self>>,
310 ) -> Option<Self::ListItem> {
311 let theme_match = &self.matches[ix];
312
313 Some(
314 ListItem::new(ix)
315 .inset(true)
316 .spacing(ListItemSpacing::Sparse)
317 .toggle_state(selected)
318 .child(HighlightedLabel::new(
319 theme_match.string.clone(),
320 theme_match.positions.clone(),
321 )),
322 )
323 }
324}