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