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