Skip to main content

rgpui_component/theme/
highlight.rs

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/// 语法高亮主题中的语法颜色集合。
55#[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/// 主题文件中的字体样式。
118#[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/// 主题文件中的字体粗细。
137#[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/// 单个语法作用域的显示样式。
168#[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    /// 按语法作用域名称查找高亮样式。
188    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    /// 按查询捕获索引查找高亮样式。
249    #[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/// 诊断和状态类颜色。
256#[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/// 主题文件中的编辑器和语法高亮配置。
378#[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/// 可被编辑器和 Markdown 代码块共用的高亮主题。
401#[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    /// 返回默认暗色高亮主题。
419    pub fn default_dark() -> Arc<Self> {
420        DEFAULT_THEME_COLORS[&ThemeMode::Dark].1.clone()
421    }
422
423    /// 返回默认亮色高亮主题。
424    pub fn default_light() -> Arc<Self> {
425        DEFAULT_THEME_COLORS[&ThemeMode::Light].1.clone()
426    }
427}