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