1use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
2use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext};
3use picker::{Picker, PickerDelegate, PickerEvent};
4use settings::{settings_file::SettingsFile, Settings};
5use staff_mode::StaffMode;
6use std::sync::Arc;
7use theme::{Theme, ThemeMeta, ThemeRegistry};
8use util::ResultExt;
9use workspace::Workspace;
10
11actions!(theme_selector, [Toggle, Reload]);
12
13pub fn init(cx: &mut AppContext) {
14 cx.add_action(toggle);
15 ThemeSelector::init(cx);
16}
17
18pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
19 workspace.toggle_modal(cx, |workspace, cx| {
20 let themes = workspace.app_state().themes.clone();
21 cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx))
22 });
23}
24
25#[cfg(debug_assertions)]
26pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
27 let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
28 themes.clear();
29 match themes.get(¤t_theme_name) {
30 Ok(theme) => {
31 ThemeSelectorDelegate::set_theme(theme, cx);
32 log::info!("reloaded theme {}", current_theme_name);
33 }
34 Err(error) => {
35 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
36 }
37 }
38}
39
40pub type ThemeSelector = Picker<ThemeSelectorDelegate>;
41
42pub struct ThemeSelectorDelegate {
43 registry: Arc<ThemeRegistry>,
44 theme_data: Vec<ThemeMeta>,
45 matches: Vec<StringMatch>,
46 original_theme: Arc<Theme>,
47 selection_completed: bool,
48 selected_index: usize,
49}
50
51impl ThemeSelectorDelegate {
52 fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<ThemeSelector>) -> Self {
53 let settings = cx.global::<Settings>();
54
55 let original_theme = settings.theme.clone();
56
57 let mut theme_names = registry
58 .list(**cx.default_global::<StaffMode>())
59 .collect::<Vec<_>>();
60 theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
61 let matches = theme_names
62 .iter()
63 .map(|meta| StringMatch {
64 candidate_id: 0,
65 score: 0.0,
66 positions: Default::default(),
67 string: meta.name.clone(),
68 })
69 .collect();
70 let mut this = Self {
71 registry,
72 theme_data: theme_names,
73 matches,
74 original_theme: original_theme.clone(),
75 selected_index: 0,
76 selection_completed: false,
77 };
78 this.select_if_matching(&original_theme.meta.name);
79 this
80 }
81
82 fn show_selected_theme(&mut self, cx: &mut ViewContext<ThemeSelector>) {
83 if let Some(mat) = self.matches.get(self.selected_index) {
84 match self.registry.get(&mat.string) {
85 Ok(theme) => {
86 Self::set_theme(theme, cx);
87 }
88 Err(error) => {
89 log::error!("error loading theme {}: {}", mat.string, error)
90 }
91 }
92 }
93 }
94
95 fn select_if_matching(&mut self, theme_name: &str) {
96 self.selected_index = self
97 .matches
98 .iter()
99 .position(|mat| mat.string == theme_name)
100 .unwrap_or(self.selected_index);
101 }
102
103 fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
104 cx.update_global::<Settings, _, _>(|settings, cx| {
105 settings.theme = theme;
106 cx.refresh_windows();
107 });
108 }
109}
110
111impl PickerDelegate for ThemeSelectorDelegate {
112 fn placeholder_text(&self) -> Arc<str> {
113 "Select Theme...".into()
114 }
115
116 fn match_count(&self) -> usize {
117 self.matches.len()
118 }
119
120 fn confirm(&mut self, cx: &mut ViewContext<ThemeSelector>) {
121 self.selection_completed = true;
122
123 let theme_name = cx.global::<Settings>().theme.meta.name.clone();
124 SettingsFile::update(cx, |settings_content| {
125 settings_content.theme = Some(theme_name);
126 });
127
128 cx.emit(PickerEvent::Dismiss);
129 }
130
131 fn dismissed(&mut self, cx: &mut ViewContext<ThemeSelector>) {
132 if !self.selection_completed {
133 Self::set_theme(self.original_theme.clone(), cx);
134 self.selection_completed = true;
135 }
136 }
137
138 fn selected_index(&self) -> usize {
139 self.selected_index
140 }
141
142 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<ThemeSelector>) {
143 self.selected_index = ix;
144 self.show_selected_theme(cx);
145 }
146
147 fn update_matches(
148 &mut self,
149 query: String,
150 cx: &mut ViewContext<ThemeSelector>,
151 ) -> gpui::Task<()> {
152 let background = cx.background().clone();
153 let candidates = self
154 .theme_data
155 .iter()
156 .enumerate()
157 .map(|(id, meta)| StringMatchCandidate {
158 id,
159 char_bag: meta.name.as_str().into(),
160 string: meta.name.clone(),
161 })
162 .collect::<Vec<_>>();
163
164 cx.spawn(|this, mut cx| async move {
165 let matches = if query.is_empty() {
166 candidates
167 .into_iter()
168 .enumerate()
169 .map(|(index, candidate)| StringMatch {
170 candidate_id: index,
171 string: candidate.string,
172 positions: Vec::new(),
173 score: 0.0,
174 })
175 .collect()
176 } else {
177 match_strings(
178 &candidates,
179 &query,
180 false,
181 100,
182 &Default::default(),
183 background,
184 )
185 .await
186 };
187
188 this.update(&mut cx, |this, cx| {
189 let delegate = this.delegate_mut();
190 delegate.matches = matches;
191 delegate.selected_index = delegate
192 .selected_index
193 .min(delegate.matches.len().saturating_sub(1));
194 delegate.show_selected_theme(cx);
195 })
196 .log_err();
197 })
198 }
199
200 fn render_match(
201 &self,
202 ix: usize,
203 mouse_state: &mut MouseState,
204 selected: bool,
205 cx: &AppContext,
206 ) -> AnyElement<Picker<Self>> {
207 let settings = cx.global::<Settings>();
208 let theme = &settings.theme;
209 let theme_match = &self.matches[ix];
210 let style = theme.picker.item.style_for(mouse_state, selected);
211
212 Label::new(theme_match.string.clone(), style.label.clone())
213 .with_highlights(theme_match.positions.clone())
214 .contained()
215 .with_style(style.container)
216 .into_any()
217 }
218}