1use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
2use gpui::{
3 actions, anyhow::Result, elements::*, AnyViewHandle, AppContext, Element, ElementBox, Entity,
4 MouseState, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
5};
6use paths::SETTINGS;
7use picker::{Picker, PickerDelegate};
8use settings::{write_theme, Settings};
9use smol::{fs::read_to_string, io::AsyncWriteExt};
10use std::sync::Arc;
11use theme::{Theme, ThemeMeta, ThemeRegistry};
12use workspace::{AppState, Workspace};
13
14pub struct ThemeSelector {
15 registry: Arc<ThemeRegistry>,
16 theme_data: Vec<ThemeMeta>,
17 matches: Vec<StringMatch>,
18 original_theme: Arc<Theme>,
19 picker: ViewHandle<Picker<Self>>,
20 selection_completed: bool,
21 selected_index: usize,
22}
23
24actions!(theme_selector, [Toggle, Reload]);
25
26pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
27 Picker::<ThemeSelector>::init(cx);
28 cx.add_action({
29 let theme_registry = app_state.themes.clone();
30 move |workspace, _: &Toggle, cx| {
31 ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
32 }
33 });
34}
35
36pub enum Event {
37 Dismissed,
38}
39
40impl ThemeSelector {
41 fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
42 let handle = cx.weak_handle();
43 let picker = cx.add_view(|cx| Picker::new(handle, cx));
44 let settings = cx.global::<Settings>();
45
46 let original_theme = settings.theme.clone();
47
48 let mut theme_names = registry
49 .list(
50 settings.staff_mode,
51 settings.experiments.experimental_themes,
52 )
53 .collect::<Vec<_>>();
54 theme_names.sort_unstable_by(|a, b| {
55 a.is_light
56 .cmp(&b.is_light)
57 .reverse()
58 .then(a.name.cmp(&b.name))
59 });
60 let matches = theme_names
61 .iter()
62 .map(|meta| StringMatch {
63 candidate_id: 0,
64 score: 0.0,
65 positions: Default::default(),
66 string: meta.name.clone(),
67 })
68 .collect();
69 let mut this = Self {
70 registry,
71 theme_data: theme_names,
72 matches,
73 picker,
74 original_theme: original_theme.clone(),
75 selected_index: 0,
76 selection_completed: false,
77 };
78 this.select_if_matching(&original_theme.meta.name);
79 this
80 }
81
82 fn toggle(
83 workspace: &mut Workspace,
84 themes: Arc<ThemeRegistry>,
85 cx: &mut ViewContext<Workspace>,
86 ) {
87 workspace.toggle_modal(cx, |_, cx| {
88 let this = cx.add_view(|cx| Self::new(themes, cx));
89 cx.subscribe(&this, Self::on_event).detach();
90 this
91 });
92 }
93
94 #[cfg(debug_assertions)]
95 pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
96 let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
97 themes.clear();
98 match themes.get(¤t_theme_name) {
99 Ok(theme) => {
100 Self::set_theme(theme, cx);
101 log::info!("reloaded theme {}", current_theme_name);
102 }
103 Err(error) => {
104 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
105 }
106 }
107 }
108
109 fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
110 if let Some(mat) = self.matches.get(self.selected_index) {
111 match self.registry.get(&mat.string) {
112 Ok(theme) => {
113 Self::set_theme(theme, cx);
114
115 let theme_name = mat.string.clone();
116
117 cx.background()
118 .spawn(async move {
119 match write_theme_name(theme_name).await {
120 Ok(_) => {}
121 Err(_) => return, //TODO Pop toast
122 }
123 })
124 .detach()
125 }
126 Err(error) => {
127 log::error!("error loading theme {}: {}", mat.string, error)
128 }
129 }
130 }
131 }
132
133 fn select_if_matching(&mut self, theme_name: &str) {
134 self.selected_index = self
135 .matches
136 .iter()
137 .position(|mat| mat.string == theme_name)
138 .unwrap_or(self.selected_index);
139 }
140
141 fn on_event(
142 workspace: &mut Workspace,
143 _: ViewHandle<ThemeSelector>,
144 event: &Event,
145 cx: &mut ViewContext<Workspace>,
146 ) {
147 match event {
148 Event::Dismissed => {
149 workspace.dismiss_modal(cx);
150 }
151 }
152 }
153
154 fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
155 cx.update_global::<Settings, _, _>(|settings, cx| {
156 settings.theme = theme;
157 cx.refresh_windows();
158 });
159 }
160}
161
162impl PickerDelegate for ThemeSelector {
163 fn match_count(&self) -> usize {
164 self.matches.len()
165 }
166
167 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
168 self.selection_completed = true;
169 cx.emit(Event::Dismissed);
170 }
171
172 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
173 if !self.selection_completed {
174 Self::set_theme(self.original_theme.clone(), cx);
175 self.selection_completed = true;
176 }
177 cx.emit(Event::Dismissed);
178 }
179
180 fn selected_index(&self) -> usize {
181 self.selected_index
182 }
183
184 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
185 self.selected_index = ix;
186 self.show_selected_theme(cx);
187 }
188
189 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
190 let background = cx.background().clone();
191 let candidates = self
192 .theme_data
193 .iter()
194 .enumerate()
195 .map(|(id, meta)| StringMatchCandidate {
196 id,
197 char_bag: meta.name.as_str().into(),
198 string: meta.name.clone(),
199 })
200 .collect::<Vec<_>>();
201
202 cx.spawn(|this, mut cx| async move {
203 let matches = if query.is_empty() {
204 candidates
205 .into_iter()
206 .enumerate()
207 .map(|(index, candidate)| StringMatch {
208 candidate_id: index,
209 string: candidate.string,
210 positions: Vec::new(),
211 score: 0.0,
212 })
213 .collect()
214 } else {
215 match_strings(
216 &candidates,
217 &query,
218 false,
219 100,
220 &Default::default(),
221 background,
222 )
223 .await
224 };
225
226 this.update(&mut cx, |this, cx| {
227 this.matches = matches;
228 this.selected_index = this
229 .selected_index
230 .min(this.matches.len().saturating_sub(1));
231 this.show_selected_theme(cx);
232 cx.notify();
233 });
234 })
235 }
236
237 fn render_match(
238 &self,
239 ix: usize,
240 mouse_state: MouseState,
241 selected: bool,
242 cx: &AppContext,
243 ) -> ElementBox {
244 let settings = cx.global::<Settings>();
245 let theme = &settings.theme;
246 let theme_match = &self.matches[ix];
247 let style = theme.picker.item.style_for(mouse_state, selected);
248
249 Label::new(theme_match.string.clone(), style.label.clone())
250 .with_highlights(theme_match.positions.clone())
251 .contained()
252 .with_style(style.container)
253 .boxed()
254 }
255}
256
257impl Entity for ThemeSelector {
258 type Event = Event;
259
260 fn release(&mut self, cx: &mut MutableAppContext) {
261 if !self.selection_completed {
262 Self::set_theme(self.original_theme.clone(), cx);
263 }
264 }
265}
266
267impl View for ThemeSelector {
268 fn ui_name() -> &'static str {
269 "ThemeSelector"
270 }
271
272 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
273 ChildView::new(self.picker.clone()).boxed()
274 }
275
276 fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
277 if cx.is_self_focused() {
278 cx.focus(&self.picker);
279 }
280 }
281}
282
283async fn write_theme_name(theme_name: String) -> Result<()> {
284 let mut settings = read_to_string(SETTINGS.as_path()).await?;
285 settings = write_theme(settings, &theme_name);
286
287 let mut file = smol::fs::OpenOptions::new()
288 .truncate(true)
289 .write(true)
290 .open(SETTINGS.as_path())
291 .await?;
292
293 file.write_all(settings.as_bytes()).await?;
294
295 Ok(())
296}