theme_selector.rs

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