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