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 cx.emit(Event::Dismissed);
157 }
158
159 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
160 if !self.selection_completed {
161 Self::set_theme(self.original_theme.clone(), cx);
162 self.selection_completed = true;
163 }
164 cx.emit(Event::Dismissed);
165 }
166
167 fn selected_index(&self) -> usize {
168 self.selected_index
169 }
170
171 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
172 self.selected_index = ix;
173 self.show_selected_theme(cx);
174 }
175
176 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
177 let background = cx.background().clone();
178 let candidates = self
179 .theme_data
180 .iter()
181 .enumerate()
182 .map(|(id, meta)| StringMatchCandidate {
183 id,
184 char_bag: meta.name.as_str().into(),
185 string: meta.name.clone(),
186 })
187 .collect::<Vec<_>>();
188
189 cx.spawn(|this, mut cx| async move {
190 let matches = if query.is_empty() {
191 candidates
192 .into_iter()
193 .enumerate()
194 .map(|(index, candidate)| StringMatch {
195 candidate_id: index,
196 string: candidate.string,
197 positions: Vec::new(),
198 score: 0.0,
199 })
200 .collect()
201 } else {
202 match_strings(
203 &candidates,
204 &query,
205 false,
206 100,
207 &Default::default(),
208 background,
209 )
210 .await
211 };
212
213 this.update(&mut cx, |this, cx| {
214 this.matches = matches;
215 this.selected_index = this
216 .selected_index
217 .min(this.matches.len().saturating_sub(1));
218 this.show_selected_theme(cx);
219 cx.notify();
220 });
221 })
222 }
223
224 fn render_match(
225 &self,
226 ix: usize,
227 mouse_state: MouseState,
228 selected: bool,
229 cx: &AppContext,
230 ) -> ElementBox {
231 let settings = cx.global::<Settings>();
232 let theme = &settings.theme;
233 let theme_match = &self.matches[ix];
234 let style = theme.picker.item.style_for(mouse_state, selected);
235
236 Label::new(theme_match.string.clone(), style.label.clone())
237 .with_highlights(theme_match.positions.clone())
238 .contained()
239 .with_style(style.container)
240 .boxed()
241 }
242}
243
244impl Entity for ThemeSelector {
245 type Event = Event;
246
247 fn release(&mut self, cx: &mut MutableAppContext) {
248 if !self.selection_completed {
249 Self::set_theme(self.original_theme.clone(), cx);
250 }
251 }
252}
253
254impl View for ThemeSelector {
255 fn ui_name() -> &'static str {
256 "ThemeSelector"
257 }
258
259 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
260 ChildView::new(self.picker.clone()).boxed()
261 }
262
263 fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
264 if cx.is_self_focused() {
265 cx.focus(&self.picker);
266 }
267 }
268}