theme.rs

  1mod theme_registry;
  2
  3use gpui::{
  4    color::Color,
  5    elements::{ContainerStyle, ImageStyle, LabelStyle},
  6    fonts::{HighlightStyle, TextStyle},
  7    Border,
  8};
  9use serde::{de::DeserializeOwned, Deserialize};
 10use serde_json::Value;
 11use std::{collections::HashMap, sync::Arc};
 12
 13pub use theme_registry::*;
 14
 15pub const DEFAULT_THEME_NAME: &'static str = "dark";
 16
 17#[derive(Deserialize, Default)]
 18pub struct Theme {
 19    #[serde(default)]
 20    pub name: String,
 21    pub workspace: Workspace,
 22    pub chat_panel: ChatPanel,
 23    pub contacts_panel: ContactsPanel,
 24    pub project_panel: ProjectPanel,
 25    pub command_palette: CommandPalette,
 26    pub selector: Selector,
 27    pub editor: Editor,
 28    pub search: Search,
 29    pub project_diagnostics: ProjectDiagnostics,
 30    pub breadcrumbs: ContainedText,
 31}
 32
 33#[derive(Deserialize, Default)]
 34pub struct Workspace {
 35    pub background: Color,
 36    pub titlebar: Titlebar,
 37    pub tab: Tab,
 38    pub active_tab: Tab,
 39    pub pane_divider: Border,
 40    pub leader_border_opacity: f32,
 41    pub leader_border_width: f32,
 42    pub sidebar_resize_handle: ContainerStyle,
 43    pub status_bar: StatusBar,
 44    pub toolbar: Toolbar,
 45    pub disconnected_overlay: ContainedText,
 46    pub modal: ContainerStyle,
 47}
 48
 49#[derive(Clone, Deserialize, Default)]
 50pub struct Titlebar {
 51    #[serde(flatten)]
 52    pub container: ContainerStyle,
 53    pub height: f32,
 54    pub title: TextStyle,
 55    pub avatar_width: f32,
 56    pub avatar_ribbon: AvatarRibbon,
 57    pub offline_icon: OfflineIcon,
 58    pub share_icon: ShareIcon,
 59    pub hovered_share_icon: ShareIcon,
 60    pub active_share_icon: ShareIcon,
 61    pub hovered_active_share_icon: ShareIcon,
 62    pub avatar: ImageStyle,
 63    pub sign_in_prompt: ContainedText,
 64    pub hovered_sign_in_prompt: ContainedText,
 65    pub outdated_warning: ContainedText,
 66}
 67
 68#[derive(Clone, Deserialize, Default)]
 69pub struct AvatarRibbon {
 70    #[serde(flatten)]
 71    pub container: ContainerStyle,
 72    pub width: f32,
 73    pub height: f32,
 74}
 75
 76#[derive(Clone, Deserialize, Default)]
 77pub struct OfflineIcon {
 78    #[serde(flatten)]
 79    pub container: ContainerStyle,
 80    pub width: f32,
 81    pub color: Color,
 82}
 83
 84#[derive(Clone, Deserialize, Default)]
 85pub struct ShareIcon {
 86    #[serde(flatten)]
 87    pub container: ContainerStyle,
 88    pub color: Color,
 89}
 90
 91#[derive(Clone, Deserialize, Default)]
 92pub struct Tab {
 93    pub height: f32,
 94    #[serde(flatten)]
 95    pub container: ContainerStyle,
 96    #[serde(flatten)]
 97    pub label: LabelStyle,
 98    pub spacing: f32,
 99    pub icon_width: f32,
100    pub icon_close: Color,
101    pub icon_close_active: Color,
102    pub icon_dirty: Color,
103    pub icon_conflict: Color,
104}
105
106#[derive(Clone, Deserialize, Default)]
107pub struct Toolbar {
108    #[serde(flatten)]
109    pub container: ContainerStyle,
110    pub height: f32,
111    pub item_spacing: f32,
112}
113
114#[derive(Clone, Deserialize, Default)]
115pub struct Search {
116    #[serde(flatten)]
117    pub container: ContainerStyle,
118    pub editor: FindEditor,
119    pub invalid_editor: ContainerStyle,
120    pub option_button_group: ContainerStyle,
121    pub option_button: ContainedText,
122    pub active_option_button: ContainedText,
123    pub hovered_option_button: ContainedText,
124    pub active_hovered_option_button: ContainedText,
125    pub match_background: Color,
126    pub match_index: ContainedText,
127    pub results_status: TextStyle,
128    pub tab_icon_width: f32,
129    pub tab_icon_spacing: f32,
130}
131
132#[derive(Clone, Deserialize, Default)]
133pub struct FindEditor {
134    #[serde(flatten)]
135    pub input: FieldEditor,
136    pub min_width: f32,
137    pub max_width: f32,
138}
139
140#[derive(Deserialize, Default)]
141pub struct StatusBar {
142    #[serde(flatten)]
143    pub container: ContainerStyle,
144    pub height: f32,
145    pub item_spacing: f32,
146    pub cursor_position: TextStyle,
147    pub auto_update_progress_message: TextStyle,
148    pub auto_update_done_message: TextStyle,
149    pub lsp_status: Interactive<StatusBarLspStatus>,
150    pub sidebar_buttons: StatusBarSidebarButtons,
151    pub diagnostics: StatusBarDiagnostics,
152}
153
154#[derive(Deserialize, Default)]
155pub struct StatusBarSidebarButtons {
156    pub group_left: ContainerStyle,
157    pub group_right: ContainerStyle,
158    pub item: Interactive<SidebarItem>,
159}
160
161#[derive(Deserialize, Default)]
162pub struct StatusBarDiagnostics {
163    pub message: Interactive<ContainedText>,
164    pub summary_ok: Interactive<ContainedText>,
165    pub summary_warning: Interactive<ContainedText>,
166    pub summary_error: Interactive<ContainedText>,
167    pub icon_color_ok: Color,
168    pub icon_color_error: Color,
169    pub icon_color_warning: Color,
170    pub height: f32,
171    pub icon_width: f32,
172    pub icon_spacing: f32,
173    pub summary_spacing: f32,
174}
175
176#[derive(Deserialize, Default)]
177pub struct StatusBarLspStatus {
178    #[serde(flatten)]
179    pub container: ContainerStyle,
180    pub height: f32,
181    pub icon_spacing: f32,
182    pub icon_color: Color,
183    pub icon_width: f32,
184    pub message: TextStyle,
185}
186
187#[derive(Deserialize, Default)]
188pub struct Sidebar {
189    pub resize_handle: ContainerStyle,
190}
191
192#[derive(Clone, Copy, Deserialize, Default)]
193pub struct SidebarItem {
194    #[serde(flatten)]
195    pub container: ContainerStyle,
196    pub icon_color: Color,
197    pub icon_size: f32,
198}
199
200#[derive(Deserialize, Default)]
201pub struct ChatPanel {
202    #[serde(flatten)]
203    pub container: ContainerStyle,
204    pub message: ChatMessage,
205    pub pending_message: ChatMessage,
206    pub channel_select: ChannelSelect,
207    pub input_editor: FieldEditor,
208    pub sign_in_prompt: TextStyle,
209    pub hovered_sign_in_prompt: TextStyle,
210}
211
212#[derive(Debug, Deserialize, Default)]
213pub struct ProjectPanel {
214    #[serde(flatten)]
215    pub container: ContainerStyle,
216    pub entry: ProjectPanelEntry,
217    pub hovered_entry: ProjectPanelEntry,
218    pub selected_entry: ProjectPanelEntry,
219    pub hovered_selected_entry: ProjectPanelEntry,
220}
221
222#[derive(Debug, Deserialize, Default)]
223pub struct ProjectPanelEntry {
224    pub height: f32,
225    #[serde(flatten)]
226    pub container: ContainerStyle,
227    pub text: TextStyle,
228    pub icon_color: Color,
229    pub icon_size: f32,
230    pub icon_spacing: f32,
231}
232
233#[derive(Debug, Deserialize, Default)]
234pub struct CommandPalette {
235    pub key: ContainedLabel,
236    pub keystroke_spacing: f32,
237}
238
239#[derive(Deserialize, Default)]
240pub struct ContactsPanel {
241    #[serde(flatten)]
242    pub container: ContainerStyle,
243    pub host_row_height: f32,
244    pub host_avatar: ImageStyle,
245    pub host_username: ContainedText,
246    pub tree_branch_width: f32,
247    pub tree_branch_color: Color,
248    pub shared_project: WorktreeRow,
249    pub hovered_shared_project: WorktreeRow,
250    pub unshared_project: WorktreeRow,
251    pub hovered_unshared_project: WorktreeRow,
252}
253
254#[derive(Deserialize, Default)]
255pub struct WorktreeRow {
256    #[serde(flatten)]
257    pub container: ContainerStyle,
258    pub height: f32,
259    pub name: ContainedText,
260    pub guest_avatar: ImageStyle,
261    pub guest_avatar_spacing: f32,
262}
263
264#[derive(Deserialize, Default)]
265pub struct ChatMessage {
266    #[serde(flatten)]
267    pub container: ContainerStyle,
268    pub body: TextStyle,
269    pub sender: ContainedText,
270    pub timestamp: ContainedText,
271}
272
273#[derive(Deserialize, Default)]
274pub struct ChannelSelect {
275    #[serde(flatten)]
276    pub container: ContainerStyle,
277    pub header: ChannelName,
278    pub item: ChannelName,
279    pub active_item: ChannelName,
280    pub hovered_item: ChannelName,
281    pub hovered_active_item: ChannelName,
282    pub menu: ContainerStyle,
283}
284
285#[derive(Deserialize, Default)]
286pub struct ChannelName {
287    #[serde(flatten)]
288    pub container: ContainerStyle,
289    pub hash: ContainedText,
290    pub name: TextStyle,
291}
292
293#[derive(Deserialize, Default)]
294pub struct Selector {
295    #[serde(flatten)]
296    pub container: ContainerStyle,
297    pub empty: ContainedLabel,
298    pub input_editor: FieldEditor,
299    pub item: ContainedLabel,
300    pub active_item: ContainedLabel,
301}
302
303#[derive(Clone, Debug, Deserialize, Default)]
304pub struct ContainedText {
305    #[serde(flatten)]
306    pub container: ContainerStyle,
307    #[serde(flatten)]
308    pub text: TextStyle,
309}
310
311#[derive(Clone, Debug, Deserialize, Default)]
312pub struct ContainedLabel {
313    #[serde(flatten)]
314    pub container: ContainerStyle,
315    #[serde(flatten)]
316    pub label: LabelStyle,
317}
318
319#[derive(Clone, Deserialize, Default)]
320pub struct ProjectDiagnostics {
321    #[serde(flatten)]
322    pub container: ContainerStyle,
323    pub empty_message: TextStyle,
324    pub tab_icon_width: f32,
325    pub tab_icon_spacing: f32,
326    pub tab_summary_spacing: f32,
327}
328
329#[derive(Clone, Deserialize, Default)]
330pub struct Editor {
331    pub text_color: Color,
332    #[serde(default)]
333    pub background: Color,
334    pub selection: SelectionStyle,
335    pub gutter_background: Color,
336    pub gutter_padding_factor: f32,
337    pub active_line_background: Color,
338    pub highlighted_line_background: Color,
339    pub rename_fade: f32,
340    pub document_highlight_read_background: Color,
341    pub document_highlight_write_background: Color,
342    pub diff_background_deleted: Color,
343    pub diff_background_inserted: Color,
344    pub line_number: Color,
345    pub line_number_active: Color,
346    pub guest_selections: Vec<SelectionStyle>,
347    pub syntax: Arc<SyntaxTheme>,
348    pub diagnostic_path_header: DiagnosticPathHeader,
349    pub diagnostic_header: DiagnosticHeader,
350    pub error_diagnostic: DiagnosticStyle,
351    pub invalid_error_diagnostic: DiagnosticStyle,
352    pub warning_diagnostic: DiagnosticStyle,
353    pub invalid_warning_diagnostic: DiagnosticStyle,
354    pub information_diagnostic: DiagnosticStyle,
355    pub invalid_information_diagnostic: DiagnosticStyle,
356    pub hint_diagnostic: DiagnosticStyle,
357    pub invalid_hint_diagnostic: DiagnosticStyle,
358    pub autocomplete: AutocompleteStyle,
359    pub code_actions_indicator: Color,
360    pub unnecessary_code_fade: f32,
361}
362
363#[derive(Clone, Deserialize, Default)]
364pub struct DiagnosticPathHeader {
365    #[serde(flatten)]
366    pub container: ContainerStyle,
367    pub filename: ContainedText,
368    pub path: ContainedText,
369    pub text_scale_factor: f32,
370}
371
372#[derive(Clone, Deserialize, Default)]
373pub struct DiagnosticHeader {
374    #[serde(flatten)]
375    pub container: ContainerStyle,
376    pub message: ContainedLabel,
377    pub code: ContainedText,
378    pub text_scale_factor: f32,
379    pub icon_width_factor: f32,
380}
381
382#[derive(Clone, Deserialize, Default)]
383pub struct DiagnosticStyle {
384    pub message: LabelStyle,
385    #[serde(default)]
386    pub header: ContainerStyle,
387    pub text_scale_factor: f32,
388}
389
390#[derive(Clone, Deserialize, Default)]
391pub struct AutocompleteStyle {
392    #[serde(flatten)]
393    pub container: ContainerStyle,
394    pub item: ContainerStyle,
395    pub selected_item: ContainerStyle,
396    pub hovered_item: ContainerStyle,
397    pub match_highlight: HighlightStyle,
398}
399
400#[derive(Clone, Copy, Default, Deserialize)]
401pub struct SelectionStyle {
402    pub cursor: Color,
403    pub selection: Color,
404}
405
406#[derive(Clone, Deserialize, Default)]
407pub struct FieldEditor {
408    #[serde(flatten)]
409    pub container: ContainerStyle,
410    pub text: TextStyle,
411    #[serde(default)]
412    pub placeholder_text: Option<TextStyle>,
413    pub selection: SelectionStyle,
414}
415
416#[derive(Default, Clone, Copy)]
417pub struct Interactive<T> {
418    pub default: T,
419    pub hover: Option<T>,
420    pub active: Option<T>,
421}
422
423impl<T> Interactive<T> {
424    pub fn active(&self) -> &T {
425        self.active.as_ref().unwrap_or(&self.default)
426    }
427
428    pub fn hover(&self) -> &T {
429        self.hover.as_ref().unwrap_or(&self.default)
430    }
431}
432
433impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
434    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
435    where
436        D: serde::Deserializer<'de>,
437    {
438        #[derive(Deserialize)]
439        struct Helper {
440            #[serde(flatten)]
441            default: Value,
442            hover: Option<Value>,
443            active: Option<Value>,
444        }
445
446        let json = Helper::deserialize(deserializer)?;
447
448        let hover = if let Some(mut hovered) = json.hover {
449            if let Value::Object(hovered) = &mut hovered {
450                if let Value::Object(default) = &json.default {
451                    for (key, value) in default {
452                        if !hovered.contains_key(key) {
453                            hovered.insert(key.clone(), value.clone());
454                        }
455                    }
456                }
457            }
458            Some(serde_json::from_value::<T>(hovered).map_err(serde::de::Error::custom)?)
459        } else {
460            None
461        };
462
463        let active = if let Some(mut active) = json.active {
464            if let Value::Object(active) = &mut active {
465                if let Value::Object(default) = &json.default {
466                    for (key, value) in default {
467                        if !active.contains_key(key) {
468                            active.insert(key.clone(), value.clone());
469                        }
470                    }
471                }
472            }
473            Some(serde_json::from_value::<T>(active).map_err(serde::de::Error::custom)?)
474        } else {
475            None
476        };
477
478        let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
479
480        Ok(Interactive {
481            default,
482            hover,
483            active,
484        })
485    }
486}
487
488impl Editor {
489    pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
490        let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
491        if style_ix == 0 {
492            &self.selection
493        } else {
494            &self.guest_selections[style_ix - 1]
495        }
496    }
497}
498
499#[derive(Default)]
500pub struct SyntaxTheme {
501    pub highlights: Vec<(String, HighlightStyle)>,
502}
503
504impl SyntaxTheme {
505    pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
506        Self { highlights }
507    }
508}
509
510impl<'de> Deserialize<'de> for SyntaxTheme {
511    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
512    where
513        D: serde::Deserializer<'de>,
514    {
515        let syntax_data: HashMap<String, HighlightStyle> = Deserialize::deserialize(deserializer)?;
516
517        let mut result = Self::new(Vec::new());
518        for (key, style) in syntax_data {
519            match result
520                .highlights
521                .binary_search_by(|(needle, _)| needle.cmp(&key))
522            {
523                Ok(i) | Err(i) => {
524                    result.highlights.insert(i, (key, style));
525                }
526            }
527        }
528
529        Ok(result)
530    }
531}