1use std::{ops::Deref, sync::Arc};
2
3use rgpui::{App, FontWeight, HighlightStyle, Hsla};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_repr::{Deserialize_repr, Serialize_repr};
7
8use crate::{ActiveTheme, DEFAULT_THEME_COLORS, ThemeMode};
9
10const HIGHLIGHT_NAMES: [&str; 41] = [
11 "attribute",
12 "boolean",
13 "comment",
14 "comment.doc",
15 "constant",
16 "constructor",
17 "embedded",
18 "emphasis",
19 "emphasis.strong",
20 "enum",
21 "function",
22 "hint",
23 "keyword",
24 "label",
25 "link_text",
26 "link_uri",
27 "number",
28 "operator",
29 "predictive",
30 "preproc",
31 "primary",
32 "property",
33 "punctuation",
34 "punctuation.bracket",
35 "punctuation.delimiter",
36 "punctuation.list_marker",
37 "punctuation.special",
38 "string",
39 "string.escape",
40 "string.regex",
41 "string.special",
42 "string.special.symbol",
43 "tag",
44 "tag.doctype",
45 "text.code.span",
46 "text.literal",
47 "title",
48 "type",
49 "variable",
50 "variable.special",
51 "variant",
52];
53
54#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
56pub struct SyntaxColors {
57 pub attribute: Option<ThemeStyle>,
58 pub boolean: Option<ThemeStyle>,
59 pub comment: Option<ThemeStyle>,
60 pub comment_doc: Option<ThemeStyle>,
61 pub constant: Option<ThemeStyle>,
62 pub constructor: Option<ThemeStyle>,
63 pub embedded: Option<ThemeStyle>,
64 pub emphasis: Option<ThemeStyle>,
65 #[serde(rename = "emphasis.strong")]
66 pub emphasis_strong: Option<ThemeStyle>,
67 #[serde(rename = "enum")]
68 pub enum_: Option<ThemeStyle>,
69 pub function: Option<ThemeStyle>,
70 pub hint: Option<ThemeStyle>,
71 pub keyword: Option<ThemeStyle>,
72 pub label: Option<ThemeStyle>,
73 #[serde(rename = "link_text")]
74 pub link_text: Option<ThemeStyle>,
75 #[serde(rename = "link_uri")]
76 pub link_uri: Option<ThemeStyle>,
77 pub number: Option<ThemeStyle>,
78 pub operator: Option<ThemeStyle>,
79 pub predictive: Option<ThemeStyle>,
80 pub preproc: Option<ThemeStyle>,
81 pub primary: Option<ThemeStyle>,
82 pub property: Option<ThemeStyle>,
83 pub punctuation: Option<ThemeStyle>,
84 #[serde(rename = "punctuation.bracket")]
85 pub punctuation_bracket: Option<ThemeStyle>,
86 #[serde(rename = "punctuation.delimiter")]
87 pub punctuation_delimiter: Option<ThemeStyle>,
88 #[serde(rename = "punctuation.list_marker")]
89 pub punctuation_list_marker: Option<ThemeStyle>,
90 #[serde(rename = "punctuation.special")]
91 pub punctuation_special: Option<ThemeStyle>,
92 pub string: Option<ThemeStyle>,
93 #[serde(rename = "string.escape")]
94 pub string_escape: Option<ThemeStyle>,
95 #[serde(rename = "string.regex")]
96 pub string_regex: Option<ThemeStyle>,
97 #[serde(rename = "string.special")]
98 pub string_special: Option<ThemeStyle>,
99 #[serde(rename = "string.special.symbol")]
100 pub string_special_symbol: Option<ThemeStyle>,
101 pub tag: Option<ThemeStyle>,
102 #[serde(rename = "tag.doctype")]
103 pub tag_doctype: Option<ThemeStyle>,
104 #[serde(rename = "text.code.span")]
105 pub text_code_span: Option<ThemeStyle>,
106 #[serde(rename = "text.literal")]
107 pub text_literal: Option<ThemeStyle>,
108 pub title: Option<ThemeStyle>,
109 #[serde(rename = "type")]
110 pub type_: Option<ThemeStyle>,
111 pub variable: Option<ThemeStyle>,
112 #[serde(rename = "variable.special")]
113 pub variable_special: Option<ThemeStyle>,
114 pub variant: Option<ThemeStyle>,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum FontStyle {
121 Normal,
122 Italic,
123 Underline,
124}
125
126impl From<FontStyle> for rgpui::FontStyle {
127 fn from(style: FontStyle) -> Self {
128 match style {
129 FontStyle::Normal => rgpui::FontStyle::Normal,
130 FontStyle::Italic => rgpui::FontStyle::Italic,
131 FontStyle::Underline => rgpui::FontStyle::Normal,
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr, JsonSchema)]
138#[repr(u16)]
139pub enum FontWeightContent {
140 Thin = 100,
141 ExtraLight = 200,
142 Light = 300,
143 Normal = 400,
144 Medium = 500,
145 Semibold = 600,
146 Bold = 700,
147 ExtraBold = 800,
148 Black = 900,
149}
150
151impl From<FontWeightContent> for FontWeight {
152 fn from(value: FontWeightContent) -> Self {
153 match value {
154 FontWeightContent::Thin => FontWeight::THIN,
155 FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT,
156 FontWeightContent::Light => FontWeight::LIGHT,
157 FontWeightContent::Normal => FontWeight::NORMAL,
158 FontWeightContent::Medium => FontWeight::MEDIUM,
159 FontWeightContent::Semibold => FontWeight::SEMIBOLD,
160 FontWeightContent::Bold => FontWeight::BOLD,
161 FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD,
162 FontWeightContent::Black => FontWeight::BLACK,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
169pub struct ThemeStyle {
170 color: Option<Hsla>,
171 font_style: Option<FontStyle>,
172 font_weight: Option<FontWeightContent>,
173}
174
175impl From<ThemeStyle> for HighlightStyle {
176 fn from(style: ThemeStyle) -> Self {
177 HighlightStyle {
178 color: style.color,
179 font_weight: style.font_weight.map(Into::into),
180 font_style: style.font_style.map(Into::into),
181 ..Default::default()
182 }
183 }
184}
185
186impl SyntaxColors {
187 pub fn style(&self, name: &str) -> Option<HighlightStyle> {
189 if name.is_empty() {
190 return None;
191 }
192
193 let style = match name {
194 "attribute" => self.attribute,
195 "boolean" => self.boolean,
196 "comment" => self.comment,
197 "comment.doc" => self.comment_doc,
198 "constant" => self.constant,
199 "constructor" => self.constructor,
200 "embedded" => self.embedded,
201 "emphasis" => self.emphasis,
202 "emphasis.strong" => self.emphasis_strong,
203 "enum" => self.enum_,
204 "function" => self.function,
205 "hint" => self.hint,
206 "keyword" => self.keyword,
207 "label" => self.label,
208 "link_text" => self.link_text,
209 "link_uri" => self.link_uri,
210 "number" => self.number,
211 "operator" => self.operator,
212 "predictive" => self.predictive,
213 "preproc" => self.preproc,
214 "primary" => self.primary,
215 "property" => self.property,
216 "punctuation" => self.punctuation,
217 "punctuation.bracket" => self.punctuation_bracket,
218 "punctuation.delimiter" => self.punctuation_delimiter,
219 "punctuation.list_marker" => self.punctuation_list_marker,
220 "punctuation.special" => self.punctuation_special,
221 "string" => self.string,
222 "string.escape" => self.string_escape,
223 "string.regex" => self.string_regex,
224 "string.special" => self.string_special,
225 "string.special.symbol" => self.string_special_symbol,
226 "tag" => self.tag,
227 "tag.doctype" => self.tag_doctype,
228 "text.code.span" => self.text_code_span,
229 "text.literal" => self.text_literal,
230 "title" => self.title,
231 "type" => self.type_,
232 "variable" => self.variable,
233 "variable.special" => self.variable_special,
234 "variant" => self.variant,
235 _ => None,
236 }
237 .map(|s| s.into());
238
239 if style.is_some() {
240 style
241 } else if name.contains('.') {
242 name.split('.').next().and_then(|prefix| self.style(prefix))
243 } else {
244 None
245 }
246 }
247
248 #[inline]
250 pub fn style_for_index(&self, index: usize) -> Option<HighlightStyle> {
251 HIGHLIGHT_NAMES.get(index).and_then(|name| self.style(name))
252 }
253}
254
255#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
257pub struct StatusColors {
258 #[serde(rename = "error")]
259 error: Option<Hsla>,
260 #[serde(rename = "error.background")]
261 error_background: Option<Hsla>,
262 #[serde(rename = "error.border")]
263 error_border: Option<Hsla>,
264 #[serde(rename = "warning")]
265 warning: Option<Hsla>,
266 #[serde(rename = "warning.background")]
267 warning_background: Option<Hsla>,
268 #[serde(rename = "warning.border")]
269 warning_border: Option<Hsla>,
270 #[serde(rename = "info")]
271 info: Option<Hsla>,
272 #[serde(rename = "info.background")]
273 info_background: Option<Hsla>,
274 #[serde(rename = "info.border")]
275 info_border: Option<Hsla>,
276 #[serde(rename = "success")]
277 success: Option<Hsla>,
278 #[serde(rename = "success.background")]
279 success_background: Option<Hsla>,
280 #[serde(rename = "success.border")]
281 success_border: Option<Hsla>,
282 #[serde(rename = "hint")]
283 hint: Option<Hsla>,
284 #[serde(rename = "hint.background")]
285 hint_background: Option<Hsla>,
286 #[serde(rename = "hint.border")]
287 hint_border: Option<Hsla>,
288}
289
290impl StatusColors {
291 #[inline]
292 pub fn error(&self, cx: &App) -> Hsla {
293 self.error.unwrap_or(cx.theme().red)
294 }
295
296 #[inline]
297 pub fn error_background(&self, cx: &App) -> Hsla {
298 let bg = cx.theme().background;
299 self.error_background
300 .unwrap_or(bg.blend(self.error(cx).alpha(0.2)))
301 }
302
303 #[inline]
304 pub fn error_border(&self, cx: &App) -> Hsla {
305 self.error_border.unwrap_or(self.error(cx))
306 }
307
308 #[inline]
309 pub fn warning(&self, cx: &App) -> Hsla {
310 self.warning.unwrap_or(cx.theme().yellow)
311 }
312
313 #[inline]
314 pub fn warning_background(&self, cx: &App) -> Hsla {
315 let bg = cx.theme().background;
316 self.warning_background
317 .unwrap_or(bg.blend(self.warning(cx).alpha(0.2)))
318 }
319
320 #[inline]
321 pub fn warning_border(&self, cx: &App) -> Hsla {
322 self.warning_border.unwrap_or(self.warning(cx))
323 }
324
325 #[inline]
326 pub fn info(&self, cx: &App) -> Hsla {
327 self.info.unwrap_or(cx.theme().blue)
328 }
329
330 #[inline]
331 pub fn info_background(&self, cx: &App) -> Hsla {
332 let bg = cx.theme().background;
333 self.info_background
334 .unwrap_or(bg.blend(self.info(cx).alpha(0.2)))
335 }
336
337 #[inline]
338 pub fn info_border(&self, cx: &App) -> Hsla {
339 self.info_border.unwrap_or(self.info(cx))
340 }
341
342 #[inline]
343 pub fn success(&self, cx: &App) -> Hsla {
344 self.success.unwrap_or(cx.theme().green)
345 }
346
347 #[inline]
348 pub fn success_background(&self, cx: &App) -> Hsla {
349 let bg = cx.theme().background;
350 self.success_background
351 .unwrap_or(bg.blend(self.success(cx).alpha(0.2)))
352 }
353
354 #[inline]
355 pub fn success_border(&self, cx: &App) -> Hsla {
356 self.success_border.unwrap_or(self.success(cx))
357 }
358
359 #[inline]
360 pub fn hint(&self, cx: &App) -> Hsla {
361 self.hint.unwrap_or(cx.theme().cyan)
362 }
363
364 #[inline]
365 pub fn hint_background(&self, cx: &App) -> Hsla {
366 let bg = cx.theme().background;
367 self.hint_background
368 .unwrap_or(bg.blend(self.hint(cx).alpha(0.2)))
369 }
370
371 #[inline]
372 pub fn hint_border(&self, cx: &App) -> Hsla {
373 self.hint_border.unwrap_or(self.hint(cx))
374 }
375}
376
377#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
379pub struct HighlightThemeStyle {
380 #[serde(rename = "editor.background")]
381 pub editor_background: Option<Hsla>,
382 #[serde(rename = "editor.foreground")]
383 pub editor_foreground: Option<Hsla>,
384 #[serde(rename = "editor.active_line.background")]
385 pub editor_active_line: Option<Hsla>,
386 #[serde(rename = "editor.line_number")]
387 pub editor_line_number: Option<Hsla>,
388 #[serde(rename = "editor.active_line_number")]
389 pub editor_active_line_number: Option<Hsla>,
390 #[serde(rename = "editor.invisible")]
391 pub editor_invisible: Option<Hsla>,
392 #[serde(rename = "editor.gutter.background")]
393 pub editor_gutter_background: Option<Hsla>,
394 #[serde(flatten)]
395 pub status: StatusColors,
396 #[serde(rename = "syntax")]
397 pub syntax: SyntaxColors,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
402pub struct HighlightTheme {
403 pub name: String,
404 #[serde(default)]
405 pub appearance: ThemeMode,
406 pub style: HighlightThemeStyle,
407}
408
409impl Deref for HighlightTheme {
410 type Target = SyntaxColors;
411
412 fn deref(&self) -> &Self::Target {
413 &self.style.syntax
414 }
415}
416
417impl HighlightTheme {
418 pub fn default_dark() -> Arc<Self> {
420 DEFAULT_THEME_COLORS[&ThemeMode::Dark].1.clone()
421 }
422
423 pub fn default_light() -> Arc<Self> {
425 DEFAULT_THEME_COLORS[&ThemeMode::Light].1.clone()
426 }
427}