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