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