iced_code_editor/
theme.rs

1use iced::Color;
2
3/// The appearance of a code editor.
4#[derive(Debug, Clone, Copy)]
5pub struct Style {
6    /// Main editor background color
7    pub background: Color,
8    /// Text content color
9    pub text_color: Color,
10    /// Line numbers gutter background color
11    pub gutter_background: Color,
12    /// Border color for the gutter
13    pub gutter_border: Color,
14    /// Color for line numbers text
15    pub line_number_color: Color,
16    /// Scrollbar background color
17    pub scrollbar_background: Color,
18    /// Scrollbar scroller (thumb) color
19    pub scroller_color: Color,
20    /// Highlight color for the current line where cursor is located
21    pub current_line_highlight: Color,
22}
23
24/// The theme catalog of a code editor.
25pub trait Catalog {
26    /// The item class of the [`Catalog`].
27    type Class<'a>;
28
29    /// The default class produced by the [`Catalog`].
30    fn default<'a>() -> Self::Class<'a>;
31
32    /// The [`Style`] of a class with the given status.
33    fn style(&self, class: &Self::Class<'_>) -> Style;
34}
35
36/// A styling function for a code editor.
37///
38/// This is a shorthand for a function that takes a reference to a
39/// [`Theme`](iced::Theme) and returns a [`Style`].
40pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
41
42impl Catalog for iced::Theme {
43    type Class<'a> = StyleFn<'a, Self>;
44
45    fn default<'a>() -> Self::Class<'a> {
46        Box::new(dark)
47    }
48
49    fn style(&self, class: &Self::Class<'_>) -> Style {
50        class(self)
51    }
52}
53
54/// Creates a dark theme with VSCode-like colors.
55///
56/// This is the default styling function. This theme mimics the default VSCode Dark theme with:
57/// - Very dark background (#0D0D12)
58/// - Light text for contrast
59/// - Slightly lighter gutter (#141419)
60/// - Medium gray line numbers (#808080)
61/// - Subtle scrollbar colors
62/// - Dark bluish current line highlight
63pub fn dark(_theme: &iced::Theme) -> Style {
64    Style {
65        background: Color::from_rgb(0.05, 0.05, 0.07), // #0D0D12
66        text_color: Color::from_rgb(0.9, 0.9, 0.9),    // #E6E6E6
67        gutter_background: Color::from_rgb(0.08, 0.08, 0.10), // #141419
68        gutter_border: Color::from_rgb(0.15, 0.15, 0.15), // #262626
69        line_number_color: Color::from_rgb(0.5, 0.5, 0.5), // #808080
70        scrollbar_background: Color::from_rgb(0.1, 0.1, 0.12), // #1A1A1F
71        scroller_color: Color::from_rgb(0.3, 0.3, 0.35), // #4D4D59
72        current_line_highlight: Color::from_rgb(0.15, 0.15, 0.2), // #262633
73    }
74}
75
76/// Creates a light theme with VSCode-like colors.
77///
78/// This theme mimics the default VSCode Light theme with:
79/// - White background (#FFFFFF)
80/// - Dark text for contrast
81/// - Light gray gutter (#F3F3F3)
82/// - Medium gray line numbers (#858585)
83/// - Subtle scrollbar colors
84/// - Light grayish-blue current line highlight for cursor visibility
85pub fn light(_theme: &iced::Theme) -> Style {
86    Style {
87        background: Color::from_rgb(1.0, 1.0, 1.0), // #FFFFFF
88        text_color: Color::from_rgb(0.0, 0.0, 0.0), // #000000
89        gutter_background: Color::from_rgb(0.953, 0.953, 0.953), // #F3F3F3
90        gutter_border: Color::from_rgb(0.9, 0.9, 0.9), // #E5E5E5
91        line_number_color: Color::from_rgb(0.522, 0.522, 0.522), // #858585
92        scrollbar_background: Color::from_rgb(0.961, 0.961, 0.961), // #F5F5F5
93        scroller_color: Color::from_rgb(0.784, 0.784, 0.784), // #C8C8C8
94        current_line_highlight: Color::from_rgb(0.95, 0.95, 0.97), // #F2F2F7
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_dark_theme_creation() {
104        let theme = iced::Theme::Dark;
105        let style = dark(&theme);
106
107        // Verify very dark background (almost black)
108        assert!(style.background.r < 0.1);
109        assert!(style.background.g < 0.1);
110        assert!(style.background.b < 0.1);
111
112        // Verify bright text for contrast
113        assert!(style.text_color.r > 0.8);
114        assert!(style.text_color.g > 0.8);
115        assert!(style.text_color.b > 0.8);
116    }
117
118    #[test]
119    fn test_dark_theme_gutter_colors() {
120        let theme = iced::Theme::Dark;
121        let style = dark(&theme);
122
123        // Gutter should be slightly lighter than background
124        assert!(style.gutter_background.r > style.background.r);
125        assert!(style.gutter_background.g > style.background.g);
126        assert!(style.gutter_background.b > style.background.b);
127
128        // Line numbers should be medium gray (readable but not bright)
129        let line_num_r = style.line_number_color.r;
130        assert!(line_num_r > 0.4 && line_num_r < 0.6);
131    }
132
133    #[test]
134    fn test_dark_theme_scrollbar_colors() {
135        let theme = iced::Theme::Dark;
136        let style = dark(&theme);
137
138        // Scrollbar background should be darker than scroller
139        assert!(style.scrollbar_background.r < style.scroller_color.r);
140        assert!(style.scrollbar_background.g < style.scroller_color.g);
141        assert!(style.scrollbar_background.b < style.scroller_color.b);
142
143        // Scroller should be visible (medium gray)
144        assert!(style.scroller_color.r > 0.2);
145        assert!(style.scroller_color.g > 0.2);
146        assert!(style.scroller_color.b > 0.2);
147    }
148
149    #[test]
150    fn test_style_copy() {
151        let theme = iced::Theme::Dark;
152        let style1 = dark(&theme);
153        let style2 = style1;
154
155        // Verify colors are approximately equal (using epsilon for float comparison)
156        assert!(
157            (style1.background.r - style2.background.r).abs() < f32::EPSILON
158        );
159        assert!(
160            (style1.text_color.r - style2.text_color.r).abs() < f32::EPSILON
161        );
162        assert!(
163            (style1.gutter_background.r - style2.gutter_background.r).abs()
164                < f32::EPSILON
165        );
166    }
167
168    #[test]
169    fn test_catalog_default() {
170        let theme = iced::Theme::Dark;
171        let class = <iced::Theme as Catalog>::default();
172        let style = theme.style(&class);
173
174        // Should produce the dark theme by default
175        assert!(style.background.r < 0.1);
176        assert!(style.text_color.r > 0.8);
177    }
178
179    #[test]
180    fn test_light_theme_creation() {
181        let theme = iced::Theme::Light;
182        let style = light(&theme);
183
184        // Verify bright background (white)
185        assert!(style.background.r > 0.9);
186        assert!(style.background.g > 0.9);
187        assert!(style.background.b > 0.9);
188
189        // Verify dark text for contrast
190        assert!(style.text_color.r < 0.2);
191        assert!(style.text_color.g < 0.2);
192        assert!(style.text_color.b < 0.2);
193    }
194
195    #[test]
196    fn test_light_theme_gutter_colors() {
197        let theme = iced::Theme::Light;
198        let style = light(&theme);
199
200        // Gutter should be slightly darker than background
201        assert!(style.gutter_background.r < style.background.r);
202        assert!(style.gutter_background.g < style.background.g);
203        assert!(style.gutter_background.b < style.background.b);
204
205        // Gutter should still be very light (light gray)
206        assert!(style.gutter_background.r > 0.9);
207
208        // Line numbers should be medium gray (readable)
209        let line_num_r = style.line_number_color.r;
210        assert!(line_num_r > 0.4 && line_num_r < 0.6);
211    }
212
213    #[test]
214    fn test_light_theme_scrollbar_colors() {
215        let theme = iced::Theme::Light;
216        let style = light(&theme);
217
218        // Scrollbar background should be lighter than scroller
219        assert!(style.scrollbar_background.r > style.scroller_color.r);
220        assert!(style.scrollbar_background.g > style.scroller_color.g);
221        assert!(style.scrollbar_background.b > style.scroller_color.b);
222
223        // Scroller should be visible (medium-light gray)
224        assert!(style.scroller_color.r > 0.7);
225        assert!(style.scroller_color.g > 0.7);
226        assert!(style.scroller_color.b > 0.7);
227    }
228
229    #[test]
230    fn test_light_vs_dark_contrast() {
231        let light_style = light(&iced::Theme::Light);
232        let dark_style = dark(&iced::Theme::Dark);
233
234        // Light theme should have brighter background than dark theme
235        assert!(light_style.background.r > dark_style.background.r);
236
237        // Light theme should have darker text than dark theme
238        assert!(light_style.text_color.r < dark_style.text_color.r);
239    }
240}