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