theme_picker.rs

  1use std::sync::Arc;
  2
  3use fuzzy::{StringMatch, StringMatchCandidate};
  4use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window};
  5use picker::{Picker, PickerDelegate};
  6use theme::ThemeRegistry;
  7use ui::{ListItem, ListItemSpacing, prelude::*};
  8
  9type ThemePicker = Picker<ThemePickerDelegate>;
 10
 11pub struct ThemePickerDelegate {
 12    themes: Vec<SharedString>,
 13    filtered_themes: Vec<StringMatch>,
 14    selected_index: usize,
 15    current_theme: SharedString,
 16    on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
 17}
 18
 19impl ThemePickerDelegate {
 20    fn new(
 21        current_theme: SharedString,
 22        on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
 23        cx: &mut Context<ThemePicker>,
 24    ) -> Self {
 25        let theme_registry = ThemeRegistry::global(cx);
 26
 27        let themes = theme_registry.list_names();
 28        let selected_index = themes
 29            .iter()
 30            .position(|theme| *theme == current_theme)
 31            .unwrap_or(0);
 32
 33        let filtered_themes = themes
 34            .iter()
 35            .enumerate()
 36            .map(|(index, theme)| StringMatch {
 37                candidate_id: index,
 38                string: theme.to_string(),
 39                positions: Vec::new(),
 40                score: 0.0,
 41            })
 42            .collect();
 43
 44        Self {
 45            themes,
 46            filtered_themes,
 47            selected_index,
 48            current_theme,
 49            on_theme_changed: Arc::new(on_theme_changed),
 50        }
 51    }
 52}
 53
 54impl PickerDelegate for ThemePickerDelegate {
 55    type ListItem = AnyElement;
 56
 57    fn match_count(&self) -> usize {
 58        self.filtered_themes.len()
 59    }
 60
 61    fn selected_index(&self) -> usize {
 62        self.selected_index
 63    }
 64
 65    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<ThemePicker>) {
 66        self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1));
 67        cx.notify();
 68    }
 69
 70    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 71        "Search theme…".into()
 72    }
 73
 74    fn update_matches(
 75        &mut self,
 76        query: String,
 77        _window: &mut Window,
 78        cx: &mut Context<ThemePicker>,
 79    ) -> Task<()> {
 80        let themes = self.themes.clone();
 81        let current_theme = self.current_theme.clone();
 82
 83        let matches: Vec<StringMatch> = if query.is_empty() {
 84            themes
 85                .iter()
 86                .enumerate()
 87                .map(|(index, theme)| StringMatch {
 88                    candidate_id: index,
 89                    string: theme.to_string(),
 90                    positions: Vec::new(),
 91                    score: 0.0,
 92                })
 93                .collect()
 94        } else {
 95            let _candidates: Vec<StringMatchCandidate> = themes
 96                .iter()
 97                .enumerate()
 98                .map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref()))
 99                .collect();
100
101            themes
102                .iter()
103                .enumerate()
104                .filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase()))
105                .map(|(index, theme)| StringMatch {
106                    candidate_id: index,
107                    string: theme.to_string(),
108                    positions: Vec::new(),
109                    score: 0.0,
110                })
111                .collect()
112        };
113
114        let selected_index = if query.is_empty() {
115            themes
116                .iter()
117                .position(|theme| *theme == current_theme)
118                .unwrap_or(0)
119        } else {
120            matches
121                .iter()
122                .position(|m| themes[m.candidate_id] == current_theme)
123                .unwrap_or(0)
124        };
125
126        self.filtered_themes = matches;
127        self.selected_index = selected_index;
128        cx.notify();
129
130        Task::ready(())
131    }
132
133    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<ThemePicker>) {
134        if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
135            let theme = theme_match.string.clone();
136            (self.on_theme_changed)(theme.into(), cx);
137        }
138    }
139
140    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<ThemePicker>) {
141        cx.defer_in(window, |picker, window, cx| {
142            picker.set_query("", window, cx);
143        });
144        cx.emit(DismissEvent);
145    }
146
147    fn render_match(
148        &self,
149        ix: usize,
150        selected: bool,
151        _window: &mut Window,
152        _cx: &mut Context<ThemePicker>,
153    ) -> Option<Self::ListItem> {
154        let theme_match = self.filtered_themes.get(ix)?;
155
156        Some(
157            ListItem::new(ix)
158                .inset(true)
159                .spacing(ListItemSpacing::Sparse)
160                .toggle_state(selected)
161                .child(Label::new(theme_match.string.clone()))
162                .into_any_element(),
163        )
164    }
165}
166
167pub fn theme_picker(
168    current_theme: SharedString,
169    on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
170    window: &mut Window,
171    cx: &mut Context<ThemePicker>,
172) -> ThemePicker {
173    let delegate = ThemePickerDelegate::new(current_theme, on_theme_changed, cx);
174
175    Picker::uniform_list(delegate, window, cx)
176        .show_scrollbar(true)
177        .width(rems_from_px(210.))
178        .max_height(Some(rems(18.).into()))
179}