1use editor::Editor;
2use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
3use gpui::{
4 action,
5 elements::*,
6 keymap::{self, Binding},
7 AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
8 ViewContext, ViewHandle,
9};
10use parking_lot::Mutex;
11use postage::watch;
12use std::{cmp, sync::Arc};
13use theme::{Theme, ThemeRegistry};
14use workspace::{
15 menu::{Confirm, SelectNext, SelectPrev},
16 AppState, Settings, Workspace,
17};
18
19#[derive(Clone)]
20pub struct ThemeSelectorParams {
21 pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
22 pub settings: watch::Receiver<Settings>,
23 pub themes: Arc<ThemeRegistry>,
24}
25
26pub struct ThemeSelector {
27 settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
28 settings: watch::Receiver<Settings>,
29 themes: Arc<ThemeRegistry>,
30 matches: Vec<StringMatch>,
31 query_editor: ViewHandle<Editor>,
32 list_state: UniformListState,
33 selected_index: usize,
34 original_theme: Arc<Theme>,
35 selection_completed: bool,
36}
37
38action!(Toggle, ThemeSelectorParams);
39action!(Reload, ThemeSelectorParams);
40
41pub fn init(params: ThemeSelectorParams, cx: &mut MutableAppContext) {
42 cx.add_action(ThemeSelector::confirm);
43 cx.add_action(ThemeSelector::select_prev);
44 cx.add_action(ThemeSelector::select_next);
45 cx.add_action(ThemeSelector::toggle);
46 cx.add_action(ThemeSelector::reload);
47
48 cx.add_bindings(vec![
49 Binding::new("cmd-k cmd-t", Toggle(params.clone()), None),
50 Binding::new("cmd-k t", Reload(params.clone()), None),
51 Binding::new("escape", Toggle(params.clone()), Some("ThemeSelector")),
52 ]);
53}
54
55pub enum Event {
56 Dismissed,
57}
58
59impl ThemeSelector {
60 fn new(
61 settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
62 settings: watch::Receiver<Settings>,
63 registry: Arc<ThemeRegistry>,
64 cx: &mut ViewContext<Self>,
65 ) -> Self {
66 let query_editor = cx.add_view(|cx| {
67 Editor::single_line(
68 settings.clone(),
69 Some(|theme| theme.selector.input_editor.clone()),
70 cx,
71 )
72 });
73
74 cx.subscribe(&query_editor, Self::on_query_editor_event)
75 .detach();
76
77 let original_theme = settings.borrow().theme.clone();
78
79 let mut this = Self {
80 settings,
81 settings_tx,
82 themes: registry,
83 query_editor,
84 matches: Vec::new(),
85 list_state: Default::default(),
86 selected_index: 0, // Default index for now
87 original_theme: original_theme.clone(),
88 selection_completed: false,
89 };
90 this.update_matches(cx);
91
92 // Set selected index to current theme
93 this.select_if_matching(&original_theme.name);
94
95 this
96 }
97
98 fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
99 workspace.toggle_modal(cx, |cx, _| {
100 let selector = cx.add_view(|cx| {
101 Self::new(
102 action.0.settings_tx.clone(),
103 action.0.settings.clone(),
104 action.0.themes.clone(),
105 cx,
106 )
107 });
108 cx.subscribe(&selector, Self::on_event).detach();
109 selector
110 });
111 }
112
113 fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
114 let current_theme_name = action.0.settings.borrow().theme.name.clone();
115 action.0.themes.clear();
116 match action.0.themes.get(¤t_theme_name) {
117 Ok(theme) => {
118 action.0.settings_tx.lock().borrow_mut().theme = theme;
119 cx.refresh_windows();
120 log::info!("reloaded theme {}", current_theme_name);
121 }
122 Err(error) => {
123 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
124 }
125 }
126 }
127
128 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
129 self.selection_completed = true;
130 cx.emit(Event::Dismissed);
131 }
132
133 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
134 if self.selected_index > 0 {
135 self.selected_index -= 1;
136 }
137 self.list_state
138 .scroll_to(ScrollTarget::Show(self.selected_index));
139
140 self.show_selected_theme(cx);
141 cx.notify();
142 }
143
144 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
145 if self.selected_index + 1 < self.matches.len() {
146 self.selected_index += 1;
147 }
148 self.list_state
149 .scroll_to(ScrollTarget::Show(self.selected_index));
150
151 self.show_selected_theme(cx);
152 cx.notify();
153 }
154
155 fn show_selected_theme(&mut self, cx: &mut MutableAppContext) {
156 if let Some(mat) = self.matches.get(self.selected_index) {
157 match self.themes.get(&mat.string) {
158 Ok(theme) => {
159 self.set_theme(theme, cx);
160 }
161 Err(error) => {
162 log::error!("error loading theme {}: {}", mat.string, error)
163 }
164 }
165 }
166 }
167
168 fn select_if_matching(&mut self, theme_name: &str) {
169 self.selected_index = self
170 .matches
171 .iter()
172 .position(|mat| mat.string == theme_name)
173 .unwrap_or(self.selected_index);
174 }
175
176 fn current_theme(&self) -> Arc<Theme> {
177 self.settings_tx.lock().borrow().theme.clone()
178 }
179
180 fn set_theme(&self, theme: Arc<Theme>, cx: &mut MutableAppContext) {
181 self.settings_tx.lock().borrow_mut().theme = theme;
182 cx.refresh_windows();
183 }
184
185 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
186 let background = cx.background().clone();
187 let candidates = self
188 .themes
189 .list()
190 .enumerate()
191 .map(|(id, name)| StringMatchCandidate {
192 id,
193 char_bag: name.as_str().into(),
194 string: name,
195 })
196 .collect::<Vec<_>>();
197 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
198
199 self.matches = if query.is_empty() {
200 candidates
201 .into_iter()
202 .enumerate()
203 .map(|(index, candidate)| StringMatch {
204 candidate_id: index,
205 string: candidate.string,
206 positions: Vec::new(),
207 score: 0.0,
208 })
209 .collect()
210 } else {
211 smol::block_on(match_strings(
212 &candidates,
213 &query,
214 false,
215 100,
216 &Default::default(),
217 background,
218 ))
219 };
220
221 self.selected_index = self.selected_index
222 .min(self.matches.len().saturating_sub(1));
223
224 cx.notify();
225 }
226
227 fn on_event(
228 workspace: &mut Workspace,
229 _: ViewHandle<ThemeSelector>,
230 event: &Event,
231 cx: &mut ViewContext<Workspace>,
232 ) {
233 match event {
234 Event::Dismissed => {
235 workspace.dismiss_modal(cx);
236 }
237 }
238 }
239
240 fn on_query_editor_event(
241 &mut self,
242 _: ViewHandle<Editor>,
243 event: &editor::Event,
244 cx: &mut ViewContext<Self>,
245 ) {
246 match event {
247 editor::Event::Edited => {
248 self.update_matches(cx);
249 self.select_if_matching(&self.current_theme().name);
250 self.show_selected_theme(cx);
251 }
252 editor::Event::Blurred => cx.emit(Event::Dismissed),
253 _ => {}
254 }
255 }
256
257 fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
258 if self.matches.is_empty() {
259 let settings = self.settings.borrow();
260 return Container::new(
261 Label::new(
262 "No matches".into(),
263 settings.theme.selector.empty.label.clone(),
264 )
265 .boxed(),
266 )
267 .with_style(settings.theme.selector.empty.container)
268 .named("empty matches");
269 }
270
271 let handle = cx.handle();
272 let list = UniformList::new(
273 self.list_state.clone(),
274 self.matches.len(),
275 move |mut range, items, cx| {
276 let cx = cx.as_ref();
277 let selector = handle.upgrade(cx).unwrap();
278 let selector = selector.read(cx);
279 let start = range.start;
280 range.end = cmp::min(range.end, selector.matches.len());
281 items.extend(
282 selector.matches[range]
283 .iter()
284 .enumerate()
285 .map(move |(i, path_match)| selector.render_match(path_match, start + i)),
286 );
287 },
288 );
289
290 Container::new(list.boxed())
291 .with_margin_top(6.0)
292 .named("matches")
293 }
294
295 fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
296 let settings = self.settings.borrow();
297 let theme = &settings.theme;
298
299 let container = Container::new(
300 Label::new(
301 theme_match.string.clone(),
302 if index == self.selected_index {
303 theme.selector.active_item.label.clone()
304 } else {
305 theme.selector.item.label.clone()
306 },
307 )
308 .with_highlights(theme_match.positions.clone())
309 .boxed(),
310 )
311 .with_style(if index == self.selected_index {
312 theme.selector.active_item.container
313 } else {
314 theme.selector.item.container
315 });
316
317 container.boxed()
318 }
319}
320
321impl Entity for ThemeSelector {
322 type Event = Event;
323
324 fn release(&mut self, cx: &mut MutableAppContext) {
325 if !self.selection_completed {
326 self.set_theme(self.original_theme.clone(), cx);
327 }
328 }
329}
330
331impl View for ThemeSelector {
332 fn ui_name() -> &'static str {
333 "ThemeSelector"
334 }
335
336 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
337 let settings = self.settings.borrow();
338
339 Align::new(
340 ConstrainedBox::new(
341 Container::new(
342 Flex::new(Axis::Vertical)
343 .with_child(
344 ChildView::new(&self.query_editor).contained()
345 .with_style(settings.theme.selector.input_editor.container)
346 .boxed(),
347 )
348 .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
349 .boxed(),
350 )
351 .with_style(settings.theme.selector.container)
352 .boxed(),
353 )
354 .with_max_width(600.0)
355 .with_max_height(400.0)
356 .boxed(),
357 )
358 .top()
359 .named("theme selector")
360 }
361
362 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
363 cx.focus(&self.query_editor);
364 }
365
366 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
367 let mut cx = Self::default_keymap_context();
368 cx.set.insert("menu".into());
369 cx
370 }
371}
372
373impl<'a> From<&'a AppState> for ThemeSelectorParams {
374 fn from(state: &'a AppState) -> Self {
375 Self {
376 settings_tx: state.settings_tx.clone(),
377 settings: state.settings.clone(),
378 themes: state.themes.clone(),
379 }
380 }
381}