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 Window, &mut App) + 'static>,
17}
18
19impl IconThemePickerDelegate {
20 fn new(
21 current_theme: SharedString,
22 on_theme_changed: impl Fn(SharedString, &mut Window, &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_theme| *icon_theme == current_theme)
36 .unwrap_or(0);
37
38 let filtered_themes = icon_themes
39 .iter()
40 .enumerate()
41 .map(|(index, theme)| StringMatch {
42 candidate_id: index,
43 string: theme.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(
71 &mut self,
72 index: usize,
73 _window: &mut Window,
74 cx: &mut Context<IconThemePicker>,
75 ) {
76 self.selected_index = index.min(self.filtered_themes.len().saturating_sub(1));
77 cx.notify();
78 }
79
80 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
81 "Search icon themes…".into()
82 }
83
84 fn update_matches(
85 &mut self,
86 query: String,
87 _window: &mut Window,
88 cx: &mut Context<IconThemePicker>,
89 ) -> Task<()> {
90 let icon_themes = self.icon_themes.clone();
91 let current_theme = self.current_theme.clone();
92
93 let matches: Vec<StringMatch> = if query.is_empty() {
94 icon_themes
95 .iter()
96 .enumerate()
97 .map(|(index, theme)| StringMatch {
98 candidate_id: index,
99 string: theme.to_string(),
100 positions: Vec::new(),
101 score: 0.0,
102 })
103 .collect()
104 } else {
105 let _candidates: Vec<StringMatchCandidate> = icon_themes
106 .iter()
107 .enumerate()
108 .map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref()))
109 .collect();
110
111 icon_themes
112 .iter()
113 .enumerate()
114 .filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase()))
115 .map(|(index, theme)| StringMatch {
116 candidate_id: index,
117 string: theme.to_string(),
118 positions: Vec::new(),
119 score: 0.0,
120 })
121 .collect()
122 };
123
124 let selected_index = if query.is_empty() {
125 icon_themes
126 .iter()
127 .position(|theme| *theme == current_theme)
128 .unwrap_or(0)
129 } else {
130 matches
131 .iter()
132 .position(|m| icon_themes[m.candidate_id] == current_theme)
133 .unwrap_or(0)
134 };
135
136 self.filtered_themes = matches;
137 self.selected_index = selected_index;
138 cx.notify();
139
140 Task::ready(())
141 }
142
143 fn confirm(
144 &mut self,
145 _secondary: bool,
146 window: &mut Window,
147 cx: &mut Context<IconThemePicker>,
148 ) {
149 if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
150 let theme = theme_match.string.clone();
151 (self.on_theme_changed)(theme.into(), window, cx);
152 }
153 }
154
155 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<IconThemePicker>) {
156 cx.defer_in(window, |picker, window, cx| {
157 picker.set_query("", window, cx);
158 });
159 cx.emit(DismissEvent);
160 }
161
162 fn render_match(
163 &self,
164 index: usize,
165 selected: bool,
166 _window: &mut Window,
167 _cx: &mut Context<IconThemePicker>,
168 ) -> Option<Self::ListItem> {
169 let theme_match = self.filtered_themes.get(index)?;
170
171 Some(
172 ListItem::new(index)
173 .inset(true)
174 .spacing(ListItemSpacing::Sparse)
175 .toggle_state(selected)
176 .child(Label::new(theme_match.string.clone()))
177 .into_any_element(),
178 )
179 }
180}
181
182pub fn icon_theme_picker(
183 current_theme: SharedString,
184 on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
185 window: &mut Window,
186 cx: &mut Context<IconThemePicker>,
187) -> IconThemePicker {
188 let delegate = IconThemePickerDelegate::new(current_theme, on_theme_changed, cx);
189
190 Picker::uniform_list(delegate, window, cx)
191 .show_scrollbar(true)
192 .width(rems_from_px(210.))
193 .max_height(Some(rems(18.).into()))
194}