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, _: &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 log::info!("reloaded theme {}", current_theme_name);
120 }
121 Err(error) => {
122 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
123 }
124 }
125 }
126
127 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
128 self.selection_completed = true;
129 cx.emit(Event::Dismissed);
130 }
131
132 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
133 if self.selected_index > 0 {
134 self.selected_index -= 1;
135 }
136 self.list_state
137 .scroll_to(ScrollTarget::Show(self.selected_index));
138
139 self.show_selected_theme();
140 cx.notify();
141 }
142
143 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
144 if self.selected_index + 1 < self.matches.len() {
145 self.selected_index += 1;
146 }
147 self.list_state
148 .scroll_to(ScrollTarget::Show(self.selected_index));
149
150 self.show_selected_theme();
151 cx.notify();
152 }
153
154 fn show_selected_theme(&mut self) {
155 if let Some(mat) = self.matches.get(self.selected_index) {
156 match self.themes.get(&mat.string) {
157 Ok(theme) => self.set_theme(theme),
158 Err(error) => {
159 log::error!("error loading theme {}: {}", mat.string, error)
160 }
161 }
162 }
163 }
164
165 fn select_if_matching(&mut self, theme_name: &str) {
166 self.selected_index = self
167 .matches
168 .iter()
169 .position(|mat| mat.string == theme_name)
170 .unwrap_or(self.selected_index);
171 }
172
173 fn current_theme(&self) -> Arc<Theme> {
174 self.settings_tx.lock().borrow().theme.clone()
175 }
176
177 fn set_theme(&self, theme: Arc<Theme>) {
178 self.settings_tx.lock().borrow_mut().theme = theme;
179 }
180
181 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
182 let background = cx.background().clone();
183 let candidates = self
184 .themes
185 .list()
186 .enumerate()
187 .map(|(id, name)| StringMatchCandidate {
188 id,
189 char_bag: name.as_str().into(),
190 string: name,
191 })
192 .collect::<Vec<_>>();
193 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
194
195 self.matches = if query.is_empty() {
196 candidates
197 .into_iter()
198 .enumerate()
199 .map(|(index, candidate)| StringMatch {
200 candidate_id: index,
201 string: candidate.string,
202 positions: Vec::new(),
203 score: 0.0,
204 })
205 .collect()
206 } else {
207 smol::block_on(match_strings(
208 &candidates,
209 &query,
210 false,
211 100,
212 &Default::default(),
213 background,
214 ))
215 };
216
217 self.selected_index = self
218 .selected_index
219 .min(self.matches.len().saturating_sub(1));
220
221 cx.notify();
222 }
223
224 fn on_event(
225 workspace: &mut Workspace,
226 _: ViewHandle<ThemeSelector>,
227 event: &Event,
228 cx: &mut ViewContext<Workspace>,
229 ) {
230 match event {
231 Event::Dismissed => {
232 workspace.dismiss_modal(cx);
233 }
234 }
235 }
236
237 fn on_query_editor_event(
238 &mut self,
239 _: ViewHandle<Editor>,
240 event: &editor::Event,
241 cx: &mut ViewContext<Self>,
242 ) {
243 match event {
244 editor::Event::Edited => {
245 self.update_matches(cx);
246 self.select_if_matching(&self.current_theme().name);
247 self.show_selected_theme();
248 }
249 editor::Event::Blurred => cx.emit(Event::Dismissed),
250 _ => {}
251 }
252 }
253
254 fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
255 if self.matches.is_empty() {
256 let settings = self.settings.borrow();
257 return Container::new(
258 Label::new(
259 "No matches".into(),
260 settings.theme.selector.empty.label.clone(),
261 )
262 .boxed(),
263 )
264 .with_style(settings.theme.selector.empty.container)
265 .named("empty matches");
266 }
267
268 let handle = cx.handle();
269 let list = UniformList::new(
270 self.list_state.clone(),
271 self.matches.len(),
272 move |mut range, items, cx| {
273 let cx = cx.as_ref();
274 let selector = handle.upgrade(cx).unwrap();
275 let selector = selector.read(cx);
276 let start = range.start;
277 range.end = cmp::min(range.end, selector.matches.len());
278 items.extend(
279 selector.matches[range]
280 .iter()
281 .enumerate()
282 .map(move |(i, path_match)| selector.render_match(path_match, start + i)),
283 );
284 },
285 );
286
287 Container::new(list.boxed())
288 .with_margin_top(6.0)
289 .named("matches")
290 }
291
292 fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
293 let settings = self.settings.borrow();
294 let theme = &settings.theme;
295
296 let container = Container::new(
297 Label::new(
298 theme_match.string.clone(),
299 if index == self.selected_index {
300 theme.selector.active_item.label.clone()
301 } else {
302 theme.selector.item.label.clone()
303 },
304 )
305 .with_highlights(theme_match.positions.clone())
306 .boxed(),
307 )
308 .with_style(if index == self.selected_index {
309 theme.selector.active_item.container
310 } else {
311 theme.selector.item.container
312 });
313
314 container.boxed()
315 }
316}
317
318impl Entity for ThemeSelector {
319 type Event = Event;
320
321 fn release(&mut self, _: &mut MutableAppContext) {
322 if !self.selection_completed {
323 self.set_theme(self.original_theme.clone());
324 }
325 }
326}
327
328impl View for ThemeSelector {
329 fn ui_name() -> &'static str {
330 "ThemeSelector"
331 }
332
333 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
334 let settings = self.settings.borrow();
335
336 Align::new(
337 ConstrainedBox::new(
338 Container::new(
339 Flex::new(Axis::Vertical)
340 .with_child(
341 ChildView::new(&self.query_editor)
342 .contained()
343 .with_style(settings.theme.selector.input_editor.container)
344 .boxed(),
345 )
346 .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
347 .boxed(),
348 )
349 .with_style(settings.theme.selector.container)
350 .boxed(),
351 )
352 .with_max_width(600.0)
353 .with_max_height(400.0)
354 .boxed(),
355 )
356 .top()
357 .named("theme selector")
358 }
359
360 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
361 cx.focus(&self.query_editor);
362 }
363
364 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
365 let mut cx = Self::default_keymap_context();
366 cx.set.insert("menu".into());
367 cx
368 }
369}
370
371impl<'a> From<&'a AppState> for ThemeSelectorParams {
372 fn from(state: &'a AppState) -> Self {
373 Self {
374 settings_tx: state.settings_tx.clone(),
375 settings: state.settings.clone(),
376 themes: state.themes.clone(),
377 }
378 }
379}