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_file::SettingsFile, Settings};
8use std::sync::Arc;
9use theme::{Theme, ThemeMeta, ThemeRegistry};
10use util::StaffMode;
11use workspace::{AppState, Workspace};
12
13pub struct ThemeSelector {
14 registry: Arc<ThemeRegistry>,
15 theme_data: Vec<ThemeMeta>,
16 matches: Vec<StringMatch>,
17 original_theme: Arc<Theme>,
18 picker: ViewHandle<Picker<Self>>,
19 selection_completed: bool,
20 selected_index: usize,
21}
22
23actions!(theme_selector, [Toggle, Reload]);
24
25pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
26 Picker::<ThemeSelector>::init(cx);
27 cx.add_action({
28 let theme_registry = app_state.themes.clone();
29 move |workspace, _: &Toggle, cx| {
30 ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
31 }
32 });
33}
34
35pub enum Event {
36 Dismissed,
37}
38
39impl ThemeSelector {
40 fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
41 let handle = cx.weak_handle();
42 let picker = cx.add_view(|cx| Picker::new("Select Theme...", handle, cx));
43 let settings = cx.global::<Settings>();
44
45 let original_theme = settings.theme.clone();
46
47 let mut theme_names = registry
48 .list(**cx.default_global::<StaffMode>())
49 .collect::<Vec<_>>();
50 theme_names.sort_unstable_by(|a, b| {
51 a.is_light
52 .cmp(&b.is_light)
53 .reverse()
54 .then(a.name.cmp(&b.name))
55 });
56 let matches = theme_names
57 .iter()
58 .map(|meta| StringMatch {
59 candidate_id: 0,
60 score: 0.0,
61 positions: Default::default(),
62 string: meta.name.clone(),
63 })
64 .collect();
65 let mut this = Self {
66 registry,
67 theme_data: theme_names,
68 matches,
69 picker,
70 original_theme: original_theme.clone(),
71 selected_index: 0,
72 selection_completed: false,
73 };
74 this.select_if_matching(&original_theme.meta.name);
75 this
76 }
77
78 fn toggle(
79 workspace: &mut Workspace,
80 themes: Arc<ThemeRegistry>,
81 cx: &mut ViewContext<Workspace>,
82 ) {
83 workspace.toggle_modal(cx, |_, cx| {
84 let this = cx.add_view(|cx| Self::new(themes, cx));
85 cx.subscribe(&this, Self::on_event).detach();
86 this
87 });
88 }
89
90 #[cfg(debug_assertions)]
91 pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
92 let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
93 themes.clear();
94 match themes.get(¤t_theme_name) {
95 Ok(theme) => {
96 Self::set_theme(theme, cx);
97 log::info!("reloaded theme {}", current_theme_name);
98 }
99 Err(error) => {
100 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
101 }
102 }
103 }
104
105 fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
106 if let Some(mat) = self.matches.get(self.selected_index) {
107 match self.registry.get(&mat.string) {
108 Ok(theme) => {
109 Self::set_theme(theme, cx);
110 }
111 Err(error) => {
112 log::error!("error loading theme {}: {}", mat.string, error)
113 }
114 }
115 }
116 }
117
118 fn select_if_matching(&mut self, theme_name: &str) {
119 self.selected_index = self
120 .matches
121 .iter()
122 .position(|mat| mat.string == theme_name)
123 .unwrap_or(self.selected_index);
124 }
125
126 fn on_event(
127 workspace: &mut Workspace,
128 _: ViewHandle<ThemeSelector>,
129 event: &Event,
130 cx: &mut ViewContext<Workspace>,
131 ) {
132 match event {
133 Event::Dismissed => {
134 workspace.dismiss_modal(cx);
135 }
136 }
137 }
138
139 fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
140 cx.update_global::<Settings, _, _>(|settings, cx| {
141 settings.theme = theme;
142 cx.refresh_windows();
143 });
144 }
145}
146
147impl PickerDelegate for ThemeSelector {
148 fn match_count(&self) -> usize {
149 self.matches.len()
150 }
151
152 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
153 self.selection_completed = true;
154
155 let theme_name = cx.global::<Settings>().theme.meta.name.clone();
156 SettingsFile::update(cx, |settings_content| {
157 settings_content.theme = Some(theme_name);
158 });
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: &mut 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, cx: &mut RenderContext<Self>) -> ElementBox {
264 ChildView::new(self.picker.clone(), cx).boxed()
265 }
266
267 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
268 if cx.is_self_focused() {
269 cx.focus(&self.picker);
270 }
271 }
272}