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