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