1use std::sync::Arc;
2
3use fuzzy::{StringMatch, StringMatchCandidate};
4use gpui::{AnyElement, App, Context, 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
144 fn render_match(
145 &self,
146 ix: usize,
147 selected: bool,
148 _window: &mut Window,
149 _cx: &mut Context<FontPicker>,
150 ) -> Option<Self::ListItem> {
151 let font_match = self.filtered_fonts.get(ix)?;
152
153 Some(
154 ListItem::new(ix)
155 .inset(true)
156 .spacing(ListItemSpacing::Sparse)
157 .toggle_state(selected)
158 .child(Label::new(font_match.string.clone()))
159 .into_any_element(),
160 )
161 }
162}
163
164pub fn font_picker(
165 current_font: SharedString,
166 on_font_changed: impl Fn(SharedString, &mut App) + 'static,
167 window: &mut Window,
168 cx: &mut Context<FontPicker>,
169) -> FontPicker {
170 let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx);
171
172 Picker::uniform_list(delegate, window, cx)
173 .show_scrollbar(true)
174 .width(rems_from_px(210.))
175 .max_height(Some(rems(20.).into()))
176}