Skip to main content

twrite_gpui/
theme.rs

1use std::collections::HashMap;
2
3use gpui::{Hsla, hsla, rgb};
4use twrite_core::{HighlightTag, Rgba, StyleValue, UnderlineDecoration};
5
6/// Color configuration for syntax elements.
7#[derive(Clone, Debug)]
8pub struct SyntaxTheme {
9    /// Color for language keywords.
10    pub keyword: Hsla,
11    /// Color for function and method names.
12    pub function: Hsla,
13    /// Color for type and struct names.
14    pub type_name: Hsla,
15    /// Color for string literals.
16    pub string: Hsla,
17    /// Color for numeric literals.
18    pub number: Hsla,
19    /// Color for comments.
20    pub comment: Hsla,
21    /// Color for mathematical and logical operators.
22    pub operator: Hsla,
23    /// Color for punctuation and delimiter characters.
24    pub punctuation: Hsla,
25    /// Color for top-level headers (# Header).
26    pub heading1: Hsla,
27    /// Color for second-level headers (## Header).
28    pub heading2: Hsla,
29    /// Color for third-level headers (### Header).
30    pub heading3: Hsla,
31    /// Color for bold text.
32    pub bold: Hsla,
33    /// Color for italic text.
34    pub italic: Hsla,
35    /// Color for inline code spans.
36    pub code: Hsla,
37    /// Background pill fill color for inline code spans.
38    pub code_bg: Hsla,
39    /// Color for hyperlink text.
40    pub link: Hsla,
41    /// Registered colors for `HighlightTag::Custom` names. Unregistered names
42    /// fall back to the editor foreground.
43    pub custom: HashMap<&'static str, Hsla>,
44    /// Color for diagnostic error underlines and squiggles.
45    pub error: Hsla,
46    /// Color for diagnostic warning underlines and squiggles.
47    pub warning: Hsla,
48}
49
50impl Default for SyntaxTheme {
51    fn default() -> Self {
52        Self {
53            keyword: rgb(0xcba6f7).into(),
54            function: rgb(0x89b4fa).into(),
55            type_name: rgb(0xf9e2af).into(),
56            string: rgb(0xa6e3a1).into(),
57            number: rgb(0xfab387).into(),
58            comment: rgb(0x6c7086).into(),
59            operator: rgb(0x89dceb).into(),
60            punctuation: rgb(0x9399b2).into(),
61            heading1: rgb(0xffb959).into(),
62            heading2: rgb(0xffb959).into(),
63            heading3: rgb(0xffb959).into(),
64            bold: rgb(0xcdd6f4).into(),
65            italic: rgb(0xb4befe).into(),
66            code: rgb(0xf5c2e7).into(),
67            code_bg: hsla(0.65, 0.4, 0.6, 0.15),
68            link: rgb(0x89b4fa).into(),
69            custom: HashMap::new(),
70            error: rgb(0xf38ba8).into(),
71            warning: rgb(0xf9e2af).into(),
72        }
73    }
74}
75
76impl SyntaxTheme {
77    /// Registers a color for a `HighlightTag::Custom` name (e.g. `"speaker"`).
78    pub fn set_custom_tag_color(&mut self, name: &'static str, color: Hsla) {
79        self.custom.insert(name, color);
80    }
81}
82
83/// Complete theme configuration for the editor.
84#[derive(Clone, Debug)]
85pub struct EditorTheme {
86    /// Background color of the text canvas.
87    pub background: Hsla,
88    /// Default text color.
89    pub foreground: Hsla,
90    /// Text insertion cursor color.
91    pub cursor: Hsla,
92    /// Background highlight color for selected text ranges.
93    pub selection: Hsla,
94    /// Background wash color for highlight-all search matches.
95    pub search_match: Hsla,
96    /// Gutter line number color for inactive lines.
97    pub line_number: Hsla,
98    /// Gutter line number color for the line containing the cursor.
99    pub line_number_active: Hsla,
100    /// Background fill for the context menu popup.
101    pub menu_bg: Hsla,
102    /// Border color for the context menu popup.
103    pub menu_border: Hsla,
104    /// Hover fill for context menu rows.
105    pub menu_hover: Hsla,
106    /// Primary text color for context menu rows.
107    pub menu_fg: Hsla,
108    /// Hint (keybinding) text color for context menu rows.
109    pub menu_hint: Hsla,
110    /// Palette for syntax highlighting tokens.
111    pub syntax: SyntaxTheme,
112}
113
114impl Default for EditorTheme {
115    fn default() -> Self {
116        Self {
117            background: rgb(0x181825).into(),
118            foreground: rgb(0xcdd6f4).into(),
119            cursor: rgb(0xf5e0dc).into(),
120            selection: hsla(0.65, 0.4, 0.6, 0.25),
121            search_match: hsla(0.12, 0.8, 0.65, 0.25),
122            line_number: rgb(0x6c7086).into(),
123            line_number_active: rgb(0xcdd6f4).into(),
124            menu_bg: rgb(0x1e1e2e).into(),
125            menu_border: rgb(0x45475a).into(),
126            menu_hover: rgb(0x313244).into(),
127            menu_fg: rgb(0xcdd6f4).into(),
128            menu_hint: rgb(0x6c7086).into(),
129            syntax: SyntaxTheme::default(),
130        }
131    }
132}
133
134/// Fully resolved style ready for canvas text run construction.
135#[derive(Clone, Debug, PartialEq)]
136pub struct ResolvedTokenStyle {
137    /// Foreground text color.
138    pub color: Hsla,
139    /// Optional background highlight or pill fill color.
140    pub background: Option<Hsla>,
141    /// Whether the text is rendered with bold font weight.
142    pub bold: bool,
143    /// Whether the text is rendered with italic font style.
144    pub italic: bool,
145    /// Optional underline decoration (solid or wavy).
146    pub underline: Option<UnderlineDecoration>,
147    /// Whether the text is rendered with a strikethrough line.
148    pub strikethrough: bool,
149}
150
151impl EditorTheme {
152    /// Converts a headless Rgba color to GPUI Hsla.
153    pub fn rgba_to_hsla(rgba: Rgba) -> Hsla {
154        let r = rgba.r as f32 / 255.0;
155        let g = rgba.g as f32 / 255.0;
156        let b = rgba.b as f32 / 255.0;
157        let a = rgba.a as f32 / 255.0;
158        gpui::Rgba { r, g, b, a }.into()
159    }
160
161    /// Resolves a semantic HighlightTag to its foreground color.
162    pub fn tag_color(&self, tag: HighlightTag) -> Hsla {
163        match tag {
164            HighlightTag::Keyword => self.syntax.keyword,
165            HighlightTag::Function => self.syntax.function,
166            HighlightTag::Type => self.syntax.type_name,
167            HighlightTag::String => self.syntax.string,
168            HighlightTag::Number => self.syntax.number,
169            HighlightTag::Comment => self.syntax.comment,
170            HighlightTag::Operator => self.syntax.operator,
171            HighlightTag::Punctuation => self.syntax.punctuation,
172            HighlightTag::Heading(1) => self.syntax.heading1,
173            HighlightTag::Heading(2) => self.syntax.heading2,
174            HighlightTag::Heading(_) => self.syntax.heading3,
175            HighlightTag::Bold => self.syntax.bold,
176            HighlightTag::Italic => self.syntax.italic,
177            HighlightTag::Code => self.syntax.code,
178            HighlightTag::Link => self.syntax.link,
179            HighlightTag::Custom(name) => self
180                .syntax
181                .custom
182                .get(name)
183                .copied()
184                .unwrap_or(self.foreground),
185            HighlightTag::Dimmed => {
186                let mut c = self.syntax.comment;
187                c.a = 0.25;
188                c
189            }
190            HighlightTag::Hidden => {
191                let mut c = self.syntax.comment;
192                c.a = 0.0;
193                c
194            }
195            HighlightTag::Blockquote => self.syntax.comment,
196            HighlightTag::HorizontalRule => self.syntax.punctuation,
197            HighlightTag::TaskUnchecked => self.syntax.comment,
198            HighlightTag::TaskChecked => self.syntax.string,
199        }
200    }
201
202    /// Resolves any StyleValue into concrete rendering attributes.
203    pub fn resolve_style(&self, style_value: &StyleValue) -> ResolvedTokenStyle {
204        match style_value {
205            StyleValue::Tag(tag) => {
206                let color = self.tag_color(*tag);
207                let bold = matches!(tag, HighlightTag::Heading(_) | HighlightTag::Bold);
208                let italic = matches!(tag, HighlightTag::Italic | HighlightTag::Comment);
209                let background = if matches!(tag, HighlightTag::Code) {
210                    Some(self.syntax.code_bg)
211                } else {
212                    None
213                };
214                let underline = if matches!(tag, HighlightTag::Link) {
215                    Some(UnderlineDecoration::Solid)
216                } else {
217                    None
218                };
219
220                ResolvedTokenStyle {
221                    color,
222                    background,
223                    bold,
224                    italic,
225                    underline,
226                    strikethrough: false,
227                }
228            }
229            StyleValue::Direct(direct) => ResolvedTokenStyle {
230                color: direct
231                    .color
232                    .map(Self::rgba_to_hsla)
233                    .unwrap_or(self.foreground),
234                background: direct.background.map(Self::rgba_to_hsla),
235                bold: direct.bold,
236                italic: direct.italic,
237                underline: direct.underline,
238                strikethrough: direct.strikethrough,
239            },
240        }
241    }
242}