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