1use editor::Editor;
2use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
3use gpui::{
4 actions, elements::*, keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext,
5 RenderContext, View, ViewContext, ViewHandle,
6};
7use settings::Settings;
8use std::{cmp, sync::Arc};
9use theme::{Theme, ThemeRegistry};
10use workspace::{
11 menu::{Confirm, SelectNext, SelectPrev},
12 Workspace,
13};
14
15pub struct ThemeSelector {
16 themes: Arc<ThemeRegistry>,
17 matches: Vec<StringMatch>,
18 query_editor: ViewHandle<Editor>,
19 list_state: UniformListState,
20 selected_index: usize,
21 original_theme: Arc<Theme>,
22 selection_completed: bool,
23}
24
25actions!(theme_selector, [Toggle, Reload]);
26
27pub fn init(cx: &mut MutableAppContext) {
28 cx.add_action(ThemeSelector::confirm);
29 cx.add_action(ThemeSelector::select_prev);
30 cx.add_action(ThemeSelector::select_next);
31 cx.add_action(ThemeSelector::toggle);
32 cx.add_action(ThemeSelector::reload);
33}
34
35pub enum Event {
36 Dismissed,
37}
38
39impl ThemeSelector {
40 fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
41 let query_editor = cx.add_view(|cx| {
42 Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
43 });
44
45 cx.subscribe(&query_editor, Self::on_query_editor_event)
46 .detach();
47
48 let original_theme = cx.global::<Settings>().theme.clone();
49
50 let mut this = Self {
51 themes: registry,
52 query_editor,
53 matches: Vec::new(),
54 list_state: Default::default(),
55 selected_index: 0, // Default index for now
56 original_theme: original_theme.clone(),
57 selection_completed: false,
58 };
59 this.update_matches(cx);
60
61 // Set selected index to current theme
62 this.select_if_matching(&original_theme.name);
63
64 this
65 }
66
67 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
68 let themes = workspace.themes();
69 workspace.toggle_modal(cx, |cx, _| {
70 let selector = cx.add_view(|cx| Self::new(themes, cx));
71 cx.subscribe(&selector, Self::on_event).detach();
72 selector
73 });
74 }
75
76 fn reload(workspace: &mut Workspace, _: &Reload, cx: &mut ViewContext<Workspace>) {
77 let current_theme_name = cx.global::<Settings>().theme.name.clone();
78 let themes = workspace.themes();
79 themes.clear();
80 match themes.get(¤t_theme_name) {
81 Ok(theme) => {
82 Self::set_theme(theme, cx);
83 log::info!("reloaded theme {}", current_theme_name);
84 }
85 Err(error) => {
86 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
87 }
88 }
89 }
90
91 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
92 self.selection_completed = true;
93 cx.emit(Event::Dismissed);
94 }
95
96 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
97 if self.selected_index > 0 {
98 self.selected_index -= 1;
99 }
100 self.list_state
101 .scroll_to(ScrollTarget::Show(self.selected_index));
102
103 self.show_selected_theme(cx);
104 cx.notify();
105 }
106
107 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
108 if self.selected_index + 1 < self.matches.len() {
109 self.selected_index += 1;
110 }
111 self.list_state
112 .scroll_to(ScrollTarget::Show(self.selected_index));
113
114 self.show_selected_theme(cx);
115 cx.notify();
116 }
117
118 fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
119 if let Some(mat) = self.matches.get(self.selected_index) {
120 match self.themes.get(&mat.string) {
121 Ok(theme) => Self::set_theme(theme, cx),
122 Err(error) => {
123 log::error!("error loading theme {}: {}", mat.string, error)
124 }
125 }
126 }
127 }
128
129 fn select_if_matching(&mut self, theme_name: &str) {
130 self.selected_index = self
131 .matches
132 .iter()
133 .position(|mat| mat.string == theme_name)
134 .unwrap_or(self.selected_index);
135 }
136
137 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
138 let background = cx.background().clone();
139 let candidates = self
140 .themes
141 .list()
142 .enumerate()
143 .map(|(id, name)| StringMatchCandidate {
144 id,
145 char_bag: name.as_str().into(),
146 string: name,
147 })
148 .collect::<Vec<_>>();
149 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
150
151 self.matches = if query.is_empty() {
152 candidates
153 .into_iter()
154 .enumerate()
155 .map(|(index, candidate)| StringMatch {
156 candidate_id: index,
157 string: candidate.string,
158 positions: Vec::new(),
159 score: 0.0,
160 })
161 .collect()
162 } else {
163 smol::block_on(match_strings(
164 &candidates,
165 &query,
166 false,
167 100,
168 &Default::default(),
169 background,
170 ))
171 };
172
173 self.selected_index = self
174 .selected_index
175 .min(self.matches.len().saturating_sub(1));
176
177 cx.notify();
178 }
179
180 fn on_event(
181 workspace: &mut Workspace,
182 _: ViewHandle<ThemeSelector>,
183 event: &Event,
184 cx: &mut ViewContext<Workspace>,
185 ) {
186 match event {
187 Event::Dismissed => {
188 workspace.dismiss_modal(cx);
189 }
190 }
191 }
192
193 fn on_query_editor_event(
194 &mut self,
195 _: ViewHandle<Editor>,
196 event: &editor::Event,
197 cx: &mut ViewContext<Self>,
198 ) {
199 match event {
200 editor::Event::BufferEdited { .. } => {
201 self.update_matches(cx);
202 self.select_if_matching(&cx.global::<Settings>().theme.name);
203 self.show_selected_theme(cx);
204 }
205 editor::Event::Blurred => cx.emit(Event::Dismissed),
206 _ => {}
207 }
208 }
209
210 fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
211 if self.matches.is_empty() {
212 let settings = cx.global::<Settings>();
213 return Container::new(
214 Label::new(
215 "No matches".into(),
216 settings.theme.selector.empty.label.clone(),
217 )
218 .boxed(),
219 )
220 .with_style(settings.theme.selector.empty.container)
221 .named("empty matches");
222 }
223
224 let handle = cx.handle();
225 let list =
226 UniformList::new(
227 self.list_state.clone(),
228 self.matches.len(),
229 move |mut range, items, cx| {
230 let cx = cx.as_ref();
231 let selector = handle.upgrade(cx).unwrap();
232 let selector = selector.read(cx);
233 let start = range.start;
234 range.end = cmp::min(range.end, selector.matches.len());
235 items.extend(selector.matches[range].iter().enumerate().map(
236 move |(i, path_match)| selector.render_match(path_match, start + i, cx),
237 ));
238 },
239 );
240
241 Container::new(list.boxed())
242 .with_margin_top(6.0)
243 .named("matches")
244 }
245
246 fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox {
247 let settings = cx.global::<Settings>();
248 let theme = &settings.theme;
249
250 let container = Container::new(
251 Label::new(
252 theme_match.string.clone(),
253 if index == self.selected_index {
254 theme.selector.active_item.label.clone()
255 } else {
256 theme.selector.item.label.clone()
257 },
258 )
259 .with_highlights(theme_match.positions.clone())
260 .boxed(),
261 )
262 .with_style(if index == self.selected_index {
263 theme.selector.active_item.container
264 } else {
265 theme.selector.item.container
266 });
267
268 container.boxed()
269 }
270
271 fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
272 cx.update_global::<Settings, _, _>(|settings, cx| {
273 settings.theme = theme;
274 cx.refresh_windows();
275 });
276 }
277}
278
279impl Entity for ThemeSelector {
280 type Event = Event;
281
282 fn release(&mut self, cx: &mut MutableAppContext) {
283 if !self.selection_completed {
284 Self::set_theme(self.original_theme.clone(), cx);
285 }
286 }
287}
288
289impl View for ThemeSelector {
290 fn ui_name() -> &'static str {
291 "ThemeSelector"
292 }
293
294 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
295 let theme = cx.global::<Settings>().theme.clone();
296 Align::new(
297 ConstrainedBox::new(
298 Container::new(
299 Flex::new(Axis::Vertical)
300 .with_child(
301 ChildView::new(&self.query_editor)
302 .contained()
303 .with_style(theme.selector.input_editor.container)
304 .boxed(),
305 )
306 .with_child(
307 FlexItem::new(self.render_matches(cx))
308 .flex(1., false)
309 .boxed(),
310 )
311 .boxed(),
312 )
313 .with_style(theme.selector.container)
314 .boxed(),
315 )
316 .with_max_width(600.0)
317 .with_max_height(400.0)
318 .boxed(),
319 )
320 .top()
321 .named("theme selector")
322 }
323
324 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
325 cx.focus(&self.query_editor);
326 }
327
328 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
329 let mut cx = Self::default_keymap_context();
330 cx.set.insert("menu".into());
331 cx
332 }
333}