theme_selector.rs

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