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(settings.clone(), cx).with_style({
62 let settings = settings.clone();
63 move |_| settings.borrow().theme.selector.input_editor.as_editor()
64 })
65 });
66
67 cx.subscribe(&query_editor, Self::on_query_editor_event)
68 .detach();
69
70 let mut this = Self {
71 settings,
72 settings_tx,
73 registry,
74 query_editor,
75 matches: Vec::new(),
76 list_state: Default::default(),
77 selected_index: 0,
78 };
79 this.update_matches(cx);
80 this
81 }
82
83 fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
84 workspace.toggle_modal(cx, |cx, _| {
85 let selector = cx.add_view(|cx| {
86 Self::new(
87 action.0.settings_tx.clone(),
88 action.0.settings.clone(),
89 action.0.themes.clone(),
90 cx,
91 )
92 });
93 cx.subscribe(&selector, Self::on_event).detach();
94 selector
95 });
96 }
97
98 fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
99 let current_theme_name = action.0.settings.borrow().theme.name.clone();
100 action.0.themes.clear();
101 match action.0.themes.get(¤t_theme_name) {
102 Ok(theme) => {
103 cx.refresh_windows();
104 action.0.settings_tx.lock().borrow_mut().theme = theme;
105 log::info!("reloaded theme {}", current_theme_name);
106 }
107 Err(error) => {
108 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
109 }
110 }
111 }
112
113 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
114 if let Some(mat) = self.matches.get(self.selected_index) {
115 match self.registry.get(&mat.string) {
116 Ok(theme) => {
117 self.settings_tx.lock().borrow_mut().theme = theme;
118 cx.refresh_windows();
119 cx.emit(Event::Dismissed);
120 }
121 Err(error) => log::error!("error loading theme {}: {}", mat.string, error),
122 }
123 }
124 }
125
126 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
127 if self.selected_index > 0 {
128 self.selected_index -= 1;
129 }
130 self.list_state.scroll_to(self.selected_index);
131 cx.notify();
132 }
133
134 fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
135 if self.selected_index + 1 < self.matches.len() {
136 self.selected_index += 1;
137 }
138 self.list_state.scroll_to(self.selected_index);
139 cx.notify();
140 }
141
142 // fn select(&mut self, selected_index: &usize, cx: &mut ViewContext<Self>) {
143 // self.selected_index = *selected_index;
144 // self.confirm(&(), cx);
145 // }
146
147 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
148 let background = cx.background().clone();
149 let candidates = self
150 .registry
151 .list()
152 .map(|name| StringMatchCandidate {
153 char_bag: name.as_str().into(),
154 string: name,
155 })
156 .collect::<Vec<_>>();
157 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
158
159 self.matches = if query.is_empty() {
160 candidates
161 .into_iter()
162 .map(|candidate| StringMatch {
163 string: candidate.string,
164 positions: Vec::new(),
165 score: 0.0,
166 })
167 .collect()
168 } else {
169 smol::block_on(match_strings(
170 &candidates,
171 &query,
172 false,
173 100,
174 &Default::default(),
175 background,
176 ))
177 };
178 cx.notify();
179 }
180
181 fn on_event(
182 workspace: &mut Workspace,
183 _: ViewHandle<ThemeSelector>,
184 event: &Event,
185 cx: &mut ViewContext<Workspace>,
186 ) {
187 match event {
188 Event::Dismissed => {
189 workspace.dismiss_modal(cx);
190 }
191 }
192 }
193
194 fn on_query_editor_event(
195 &mut self,
196 _: ViewHandle<Editor>,
197 event: &editor::Event,
198 cx: &mut ViewContext<Self>,
199 ) {
200 match event {
201 editor::Event::Edited => self.update_matches(cx),
202 editor::Event::Blurred => cx.emit(Event::Dismissed),
203 _ => {}
204 }
205 }
206
207 fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
208 if self.matches.is_empty() {
209 let settings = self.settings.borrow();
210 return Container::new(
211 Label::new("No matches".into(), settings.theme.selector.label.clone()).boxed(),
212 )
213 .with_margin_top(6.0)
214 .named("empty matches");
215 }
216
217 let handle = cx.handle();
218 let list = UniformList::new(
219 self.list_state.clone(),
220 self.matches.len(),
221 move |mut range, items, cx| {
222 let cx = cx.as_ref();
223 let selector = handle.upgrade(cx).unwrap();
224 let selector = selector.read(cx);
225 let start = range.start;
226 range.end = cmp::min(range.end, selector.matches.len());
227 items.extend(
228 selector.matches[range]
229 .iter()
230 .enumerate()
231 .map(move |(i, path_match)| selector.render_match(path_match, start + i)),
232 );
233 },
234 );
235
236 Container::new(list.boxed())
237 .with_margin_top(6.0)
238 .named("matches")
239 }
240
241 fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
242 let settings = self.settings.borrow();
243 let theme = &settings.theme;
244
245 let container = Container::new(
246 Label::new(
247 theme_match.string.clone(),
248 if index == self.selected_index {
249 theme.selector.active_item.label.clone()
250 } else {
251 theme.selector.item.label.clone()
252 },
253 )
254 .with_highlights(theme_match.positions.clone())
255 .boxed(),
256 )
257 .with_style(if index == self.selected_index {
258 &theme.selector.active_item.container
259 } else {
260 &theme.selector.item.container
261 });
262
263 container.boxed()
264 }
265}
266
267impl Entity for ThemeSelector {
268 type Event = Event;
269}
270
271impl View for ThemeSelector {
272 fn ui_name() -> &'static str {
273 "ThemeSelector"
274 }
275
276 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
277 let settings = self.settings.borrow();
278
279 Align::new(
280 ConstrainedBox::new(
281 Container::new(
282 Flex::new(Axis::Vertical)
283 .with_child(ChildView::new(self.query_editor.id()).boxed())
284 .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
285 .boxed(),
286 )
287 .with_style(&settings.theme.selector.container)
288 .boxed(),
289 )
290 .with_max_width(600.0)
291 .with_max_height(400.0)
292 .boxed(),
293 )
294 .top()
295 .named("theme selector")
296 }
297
298 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
299 cx.focus(&self.query_editor);
300 }
301
302 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
303 let mut cx = Self::default_keymap_context();
304 cx.set.insert("menu".into());
305 cx
306 }
307}