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