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