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