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