theme_selector.rs

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