Skip to main content

twrite_gpui/
theme.rs

1use std::collections::HashMap;
2
3use gpui::{Hsla, hsla, rgb};
4use twrite_core::{CalloutKind, 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    /// Background tint color for `==mark==` highlight spans.
40    pub highlight_bg: Hsla,
41    /// Accent color for `> [!NOTE]` callout blocks.
42    pub callout_note: Hsla,
43    /// Accent color for `> [!TIP]` callout blocks.
44    pub callout_tip: Hsla,
45    /// Accent color for `> [!WARNING]` callout blocks.
46    pub callout_warning: Hsla,
47    /// Accent color for `> [!CAUTION]` callout blocks.
48    pub callout_caution: Hsla,
49    /// Accent color for `> [!IMPORTANT]` callout blocks.
50    pub callout_important: Hsla,
51    /// Color for hyperlink text.
52    pub link: Hsla,
53    /// Registered colors for `HighlightTag::Custom` names. Unregistered names
54    /// fall back to the editor foreground.
55    pub custom: HashMap<&'static str, Hsla>,
56    /// Color for diagnostic error underlines and squiggles.
57    pub error: Hsla,
58    /// Color for diagnostic warning underlines and squiggles.
59    pub warning: Hsla,
60}
61
62impl Default for SyntaxTheme {
63    fn default() -> Self {
64        Self {
65            keyword: rgb(0xcba6f7).into(),
66            function: rgb(0x89b4fa).into(),
67            type_name: rgb(0xf9e2af).into(),
68            string: rgb(0xa6e3a1).into(),
69            number: rgb(0xfab387).into(),
70            comment: rgb(0x6c7086).into(),
71            operator: rgb(0x89dceb).into(),
72            punctuation: rgb(0x9399b2).into(),
73            heading1: rgb(0xffb959).into(),
74            heading2: rgb(0xffb959).into(),
75            heading3: rgb(0xffb959).into(),
76            bold: rgb(0xcdd6f4).into(),
77            italic: rgb(0xb4befe).into(),
78            code: rgb(0xf5c2e7).into(),
79            code_bg: hsla(0.65, 0.4, 0.6, 0.15),
80            highlight_bg: hsla(0.11, 0.85, 0.6, 0.35),
81            callout_note: rgb(0x89b4fa).into(),
82            callout_tip: rgb(0xa6e3a1).into(),
83            callout_warning: rgb(0xf9e2af).into(),
84            callout_caution: rgb(0xfab387).into(),
85            callout_important: rgb(0xf38ba8).into(),
86            link: rgb(0x89b4fa).into(),
87            custom: HashMap::new(),
88            error: rgb(0xf38ba8).into(),
89            warning: rgb(0xf9e2af).into(),
90        }
91    }
92}
93
94impl SyntaxTheme {
95    /// Registers a color for a `HighlightTag::Custom` name (e.g. `"speaker"`).
96    pub fn set_custom_tag_color(&mut self, name: &'static str, color: Hsla) {
97        self.custom.insert(name, color);
98    }
99
100    /// Resolves a callout kind to its accent color. Unrecognized kinds render
101    /// with the default note accent instead of failing.
102    pub fn callout_accent(&self, kind: CalloutKind) -> Hsla {
103        match kind {
104            CalloutKind::Note => self.callout_note,
105            CalloutKind::Tip => self.callout_tip,
106            CalloutKind::Warning => self.callout_warning,
107            CalloutKind::Caution => self.callout_caution,
108            CalloutKind::Important => self.callout_important,
109            CalloutKind::Other => self.callout_note,
110        }
111    }
112}
113
114/// Complete theme configuration for the editor.
115#[derive(Clone, Debug)]
116pub struct EditorTheme {
117    /// Background color of the text canvas.
118    pub background: Hsla,
119    /// Default text color.
120    pub foreground: Hsla,
121    /// Text insertion cursor color.
122    pub cursor: Hsla,
123    /// Background highlight color for selected text ranges.
124    pub selection: Hsla,
125    /// Background wash color for highlight-all search matches.
126    pub search_match: Hsla,
127    /// Gutter line number color for inactive lines.
128    pub line_number: Hsla,
129    /// Gutter line number color for the line containing the cursor.
130    pub line_number_active: Hsla,
131    /// Background fill for the context menu popup.
132    pub menu_bg: Hsla,
133    /// Border color for the context menu popup.
134    pub menu_border: Hsla,
135    /// Hover fill for context menu rows.
136    pub menu_hover: Hsla,
137    /// Primary text color for context menu rows.
138    pub menu_fg: Hsla,
139    /// Hint (keybinding) text color for context menu rows.
140    pub menu_hint: Hsla,
141    /// Palette for syntax highlighting tokens.
142    pub syntax: SyntaxTheme,
143}
144
145impl Default for EditorTheme {
146    fn default() -> Self {
147        Self {
148            background: rgb(0x181825).into(),
149            foreground: rgb(0xcdd6f4).into(),
150            cursor: rgb(0xf5e0dc).into(),
151            selection: hsla(0.65, 0.4, 0.6, 0.25),
152            search_match: hsla(0.12, 0.8, 0.65, 0.25),
153            line_number: rgb(0x6c7086).into(),
154            line_number_active: rgb(0xcdd6f4).into(),
155            menu_bg: rgb(0x1e1e2e).into(),
156            menu_border: rgb(0x45475a).into(),
157            menu_hover: rgb(0x313244).into(),
158            menu_fg: rgb(0xcdd6f4).into(),
159            menu_hint: rgb(0x6c7086).into(),
160            syntax: SyntaxTheme::default(),
161        }
162    }
163}
164
165/// Fully resolved style ready for canvas text run construction.
166#[derive(Clone, Debug, PartialEq)]
167pub struct ResolvedTokenStyle {
168    /// Foreground text color.
169    pub color: Hsla,
170    /// Optional background highlight or pill fill color.
171    pub background: Option<Hsla>,
172    /// Whether the text is rendered with bold font weight.
173    pub bold: bool,
174    /// Whether the text is rendered with italic font style.
175    pub italic: bool,
176    /// Optional underline decoration (solid or wavy).
177    pub underline: Option<UnderlineDecoration>,
178    /// Whether the text is rendered with a strikethrough line.
179    pub strikethrough: bool,
180}
181
182impl EditorTheme {
183    /// Converts a headless Rgba color to GPUI Hsla.
184    pub fn rgba_to_hsla(rgba: Rgba) -> Hsla {
185        let r = rgba.r as f32 / 255.0;
186        let g = rgba.g as f32 / 255.0;
187        let b = rgba.b as f32 / 255.0;
188        let a = rgba.a as f32 / 255.0;
189        gpui::Rgba { r, g, b, a }.into()
190    }
191
192    /// Resolves a semantic HighlightTag to its foreground color.
193    pub fn tag_color(&self, tag: HighlightTag) -> Hsla {
194        match tag {
195            HighlightTag::Keyword => self.syntax.keyword,
196            HighlightTag::Function => self.syntax.function,
197            HighlightTag::Type => self.syntax.type_name,
198            HighlightTag::String => self.syntax.string,
199            HighlightTag::Number => self.syntax.number,
200            HighlightTag::Comment => self.syntax.comment,
201            HighlightTag::Operator => self.syntax.operator,
202            HighlightTag::Punctuation => self.syntax.punctuation,
203            HighlightTag::Heading(1) => self.syntax.heading1,
204            HighlightTag::Heading(2) => self.syntax.heading2,
205            HighlightTag::Heading(_) => self.syntax.heading3,
206            HighlightTag::Bold => self.syntax.bold,
207            HighlightTag::Italic => self.syntax.italic,
208            HighlightTag::Code => self.syntax.code,
209            HighlightTag::Highlight => self.foreground,
210            HighlightTag::Link => self.syntax.link,
211            HighlightTag::Custom(name) => self
212                .syntax
213                .custom
214                .get(name)
215                .copied()
216                .unwrap_or(self.foreground),
217            HighlightTag::Dimmed => {
218                let mut c = self.syntax.comment;
219                c.a = 0.25;
220                c
221            }
222            HighlightTag::Hidden => {
223                let mut c = self.syntax.comment;
224                c.a = 0.0;
225                c
226            }
227            HighlightTag::Blockquote => self.syntax.comment,
228            HighlightTag::Callout(_) => self.foreground,
229            HighlightTag::HorizontalRule => self.syntax.punctuation,
230            HighlightTag::TaskUnchecked => self.syntax.comment,
231            HighlightTag::TaskChecked => self.syntax.string,
232        }
233    }
234
235    /// Resolves any StyleValue into concrete rendering attributes.
236    pub fn resolve_style(&self, style_value: &StyleValue) -> ResolvedTokenStyle {
237        match style_value {
238            StyleValue::Tag(tag) => {
239                let color = self.tag_color(*tag);
240                let bold = matches!(tag, HighlightTag::Heading(_) | HighlightTag::Bold);
241                let italic = matches!(tag, HighlightTag::Italic | HighlightTag::Comment);
242                let background = if matches!(tag, HighlightTag::Code) {
243                    Some(self.syntax.code_bg)
244                } else if matches!(tag, HighlightTag::Highlight) {
245                    Some(self.syntax.highlight_bg)
246                } else {
247                    None
248                };
249                let underline = if matches!(tag, HighlightTag::Link) {
250                    Some(UnderlineDecoration::Solid)
251                } else {
252                    None
253                };
254
255                ResolvedTokenStyle {
256                    color,
257                    background,
258                    bold,
259                    italic,
260                    underline,
261                    strikethrough: false,
262                }
263            }
264            StyleValue::Direct(direct) => ResolvedTokenStyle {
265                color: direct
266                    .color
267                    .map(Self::rgba_to_hsla)
268                    .unwrap_or(self.foreground),
269                background: direct.background.map(Self::rgba_to_hsla),
270                bold: direct.bold,
271                italic: direct.italic,
272                underline: direct.underline,
273                strikethrough: direct.strikethrough,
274            },
275        }
276    }
277}