theme_selector.rs

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