theme_selector.rs

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