theme.rs

  1mod theme_registry;
  2
  3use gpui::{
  4    color::Color,
  5    elements::{ContainerStyle, ImageStyle, LabelStyle, TooltipStyle},
  6    fonts::{HighlightStyle, TextStyle},
  7    Border, MouseState,
  8};
  9use serde::{de::DeserializeOwned, Deserialize};
 10use serde_json::Value;
 11use std::{collections::HashMap, sync::Arc};
 12
 13pub use theme_registry::*;
 14
 15#[derive(Deserialize, Default)]
 16pub struct Theme {
 17    #[serde(default)]
 18    pub name: String,
 19    pub workspace: Workspace,
 20    pub context_menu: ContextMenu,
 21    pub chat_panel: ChatPanel,
 22    pub contacts_panel: ContactsPanel,
 23    pub contact_finder: ContactFinder,
 24    pub project_panel: ProjectPanel,
 25    pub command_palette: CommandPalette,
 26    pub picker: Picker,
 27    pub editor: Editor,
 28    pub search: Search,
 29    pub project_diagnostics: ProjectDiagnostics,
 30    pub breadcrumbs: ContainedText,
 31    pub contact_notification: ContactNotification,
 32    pub update_notification: UpdateNotification,
 33    pub tooltip: TooltipStyle,
 34    pub terminal: TerminalStyle,
 35}
 36
 37#[derive(Deserialize, Default)]
 38pub struct Workspace {
 39    pub background: Color,
 40    pub titlebar: Titlebar,
 41    pub active_pane_active_tab: Tab,
 42    pub active_pane_inactive_tab: Tab,
 43    pub inactive_pane_active_tab: Tab,
 44    pub inactive_pane_inactive_tab: Tab,
 45    pub pane_button: Interactive<IconButton>,
 46    pub pane_divider: Border,
 47    pub leader_border_opacity: f32,
 48    pub leader_border_width: f32,
 49    pub sidebar_resize_handle: ContainerStyle,
 50    pub status_bar: StatusBar,
 51    pub toolbar: Toolbar,
 52    pub disconnected_overlay: ContainedText,
 53    pub modal: ContainerStyle,
 54    pub notification: ContainerStyle,
 55    pub notifications: Notifications,
 56    pub joining_project_avatar: ImageStyle,
 57    pub joining_project_message: ContainedText,
 58}
 59
 60#[derive(Clone, Deserialize, Default)]
 61pub struct Titlebar {
 62    #[serde(flatten)]
 63    pub container: ContainerStyle,
 64    pub height: f32,
 65    pub title: TextStyle,
 66    pub avatar_width: f32,
 67    pub avatar_margin: f32,
 68    pub avatar_ribbon: AvatarRibbon,
 69    pub offline_icon: OfflineIcon,
 70    pub avatar: ImageStyle,
 71    pub sign_in_prompt: Interactive<ContainedText>,
 72    pub outdated_warning: ContainedText,
 73}
 74
 75#[derive(Clone, Deserialize, Default)]
 76pub struct AvatarRibbon {
 77    #[serde(flatten)]
 78    pub container: ContainerStyle,
 79    pub width: f32,
 80    pub height: f32,
 81}
 82
 83#[derive(Clone, Deserialize, Default)]
 84pub struct OfflineIcon {
 85    #[serde(flatten)]
 86    pub container: ContainerStyle,
 87    pub width: f32,
 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 description: ContainedText,
 99    pub spacing: f32,
100    pub icon_width: f32,
101    pub icon_close: Color,
102    pub icon_close_active: Color,
103    pub icon_dirty: Color,
104    pub icon_conflict: Color,
105}
106
107#[derive(Clone, Deserialize, Default)]
108pub struct Toolbar {
109    #[serde(flatten)]
110    pub container: ContainerStyle,
111    pub height: f32,
112    pub item_spacing: f32,
113    pub nav_button: Interactive<IconButton>,
114}
115
116#[derive(Clone, Deserialize, Default)]
117pub struct Notifications {
118    #[serde(flatten)]
119    pub container: ContainerStyle,
120    pub width: f32,
121}
122
123#[derive(Clone, Deserialize, Default)]
124pub struct Search {
125    #[serde(flatten)]
126    pub container: ContainerStyle,
127    pub editor: FindEditor,
128    pub invalid_editor: ContainerStyle,
129    pub option_button_group: ContainerStyle,
130    pub option_button: Interactive<ContainedText>,
131    pub match_background: Color,
132    pub match_index: ContainedText,
133    pub results_status: TextStyle,
134    pub tab_icon_width: f32,
135    pub tab_icon_spacing: f32,
136}
137
138#[derive(Clone, Deserialize, Default)]
139pub struct FindEditor {
140    #[serde(flatten)]
141    pub input: FieldEditor,
142    pub min_width: f32,
143    pub max_width: f32,
144}
145
146#[derive(Deserialize, Default)]
147pub struct StatusBar {
148    #[serde(flatten)]
149    pub container: ContainerStyle,
150    pub height: f32,
151    pub item_spacing: f32,
152    pub cursor_position: TextStyle,
153    pub auto_update_progress_message: TextStyle,
154    pub auto_update_done_message: TextStyle,
155    pub lsp_status: Interactive<StatusBarLspStatus>,
156    pub feedback: Interactive<TextStyle>,
157    pub sidebar_buttons: StatusBarSidebarButtons,
158    pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
159    pub diagnostic_message: Interactive<ContainedText>,
160}
161
162#[derive(Deserialize, Default)]
163pub struct StatusBarSidebarButtons {
164    pub group_left: ContainerStyle,
165    pub group_right: ContainerStyle,
166    pub item: Interactive<SidebarItem>,
167    pub badge: ContainerStyle,
168}
169
170#[derive(Deserialize, Default)]
171pub struct StatusBarDiagnosticSummary {
172    pub container_ok: ContainerStyle,
173    pub container_warning: ContainerStyle,
174    pub container_error: ContainerStyle,
175    pub text: TextStyle,
176    pub icon_color_ok: Color,
177    pub icon_color_warning: Color,
178    pub icon_color_error: Color,
179    pub height: f32,
180    pub icon_width: f32,
181    pub icon_spacing: f32,
182    pub summary_spacing: f32,
183}
184
185#[derive(Deserialize, Default)]
186pub struct StatusBarLspStatus {
187    #[serde(flatten)]
188    pub container: ContainerStyle,
189    pub height: f32,
190    pub icon_spacing: f32,
191    pub icon_color: Color,
192    pub icon_width: f32,
193    pub message: TextStyle,
194}
195
196#[derive(Deserialize, Default)]
197pub struct Sidebar {
198    pub resize_handle: ContainerStyle,
199}
200
201#[derive(Clone, Copy, Deserialize, Default)]
202pub struct SidebarItem {
203    #[serde(flatten)]
204    pub container: ContainerStyle,
205    pub icon_color: Color,
206    pub icon_size: f32,
207}
208
209#[derive(Deserialize, Default)]
210pub struct ChatPanel {
211    #[serde(flatten)]
212    pub container: ContainerStyle,
213    pub message: ChatMessage,
214    pub pending_message: ChatMessage,
215    pub channel_select: ChannelSelect,
216    pub input_editor: FieldEditor,
217    pub sign_in_prompt: TextStyle,
218    pub hovered_sign_in_prompt: TextStyle,
219}
220
221#[derive(Deserialize, Default)]
222pub struct ProjectPanel {
223    #[serde(flatten)]
224    pub container: ContainerStyle,
225    pub entry: Interactive<ProjectPanelEntry>,
226    pub cut_entry_fade: f32,
227    pub ignored_entry_fade: f32,
228    pub filename_editor: FieldEditor,
229    pub indent_width: f32,
230}
231
232#[derive(Clone, Debug, Deserialize, Default)]
233pub struct ProjectPanelEntry {
234    pub height: f32,
235    #[serde(flatten)]
236    pub container: ContainerStyle,
237    pub text: TextStyle,
238    pub icon_color: Color,
239    pub icon_size: f32,
240    pub icon_spacing: f32,
241}
242
243#[derive(Clone, Debug, Deserialize, Default)]
244pub struct ContextMenu {
245    #[serde(flatten)]
246    pub container: ContainerStyle,
247    pub item: Interactive<ContextMenuItem>,
248    pub keystroke_margin: f32,
249    pub separator: ContainerStyle,
250}
251
252#[derive(Clone, Debug, Deserialize, Default)]
253pub struct ContextMenuItem {
254    #[serde(flatten)]
255    pub container: ContainerStyle,
256    pub label: TextStyle,
257    pub keystroke: ContainedText,
258}
259
260#[derive(Debug, Deserialize, Default)]
261pub struct CommandPalette {
262    pub key: Interactive<ContainedLabel>,
263    pub keystroke_spacing: f32,
264}
265
266#[derive(Deserialize, Default)]
267pub struct ContactsPanel {
268    #[serde(flatten)]
269    pub container: ContainerStyle,
270    pub user_query_editor: FieldEditor,
271    pub user_query_editor_height: f32,
272    pub add_contact_button: IconButton,
273    pub header_row: Interactive<ContainedText>,
274    pub contact_row: Interactive<ContainerStyle>,
275    pub project_row: Interactive<ProjectRow>,
276    pub row_height: f32,
277    pub contact_avatar: ImageStyle,
278    pub contact_username: ContainedText,
279    pub contact_button: Interactive<IconButton>,
280    pub contact_button_spacing: f32,
281    pub disabled_button: IconButton,
282    pub tree_branch: Interactive<TreeBranch>,
283    pub private_button: Interactive<IconButton>,
284    pub section_icon_size: f32,
285    pub invite_row: Interactive<ContainedLabel>,
286}
287
288#[derive(Deserialize, Default)]
289pub struct InviteLink {
290    #[serde(flatten)]
291    pub container: ContainerStyle,
292    #[serde(flatten)]
293    pub label: LabelStyle,
294    pub icon: Icon,
295}
296
297#[derive(Deserialize, Default, Clone, Copy)]
298pub struct TreeBranch {
299    pub width: f32,
300    pub color: Color,
301}
302
303#[derive(Deserialize, Default)]
304pub struct ContactFinder {
305    pub row_height: f32,
306    pub contact_avatar: ImageStyle,
307    pub contact_username: ContainerStyle,
308    pub contact_button: IconButton,
309    pub disabled_contact_button: IconButton,
310}
311
312#[derive(Deserialize, Default)]
313pub struct Icon {
314    #[serde(flatten)]
315    pub container: ContainerStyle,
316    pub color: Color,
317    pub width: f32,
318    pub path: String,
319}
320
321#[derive(Deserialize, Clone, Copy, Default)]
322pub struct IconButton {
323    #[serde(flatten)]
324    pub container: ContainerStyle,
325    pub color: Color,
326    pub icon_width: f32,
327    pub button_width: f32,
328}
329
330#[derive(Deserialize, Default)]
331pub struct ProjectRow {
332    #[serde(flatten)]
333    pub container: ContainerStyle,
334    pub name: ContainedText,
335    pub guests: ContainerStyle,
336    pub guest_avatar: ImageStyle,
337    pub guest_avatar_spacing: f32,
338}
339
340#[derive(Deserialize, Default)]
341pub struct ChatMessage {
342    #[serde(flatten)]
343    pub container: ContainerStyle,
344    pub body: TextStyle,
345    pub sender: ContainedText,
346    pub timestamp: ContainedText,
347}
348
349#[derive(Deserialize, Default)]
350pub struct ChannelSelect {
351    #[serde(flatten)]
352    pub container: ContainerStyle,
353    pub header: ChannelName,
354    pub item: ChannelName,
355    pub active_item: ChannelName,
356    pub hovered_item: ChannelName,
357    pub hovered_active_item: ChannelName,
358    pub menu: ContainerStyle,
359}
360
361#[derive(Deserialize, Default)]
362pub struct ChannelName {
363    #[serde(flatten)]
364    pub container: ContainerStyle,
365    pub hash: ContainedText,
366    pub name: TextStyle,
367}
368
369#[derive(Deserialize, Default)]
370pub struct Picker {
371    #[serde(flatten)]
372    pub container: ContainerStyle,
373    pub empty: ContainedLabel,
374    pub input_editor: FieldEditor,
375    pub item: Interactive<ContainedLabel>,
376}
377
378#[derive(Clone, Debug, Deserialize, Default)]
379pub struct ContainedText {
380    #[serde(flatten)]
381    pub container: ContainerStyle,
382    #[serde(flatten)]
383    pub text: TextStyle,
384}
385
386#[derive(Clone, Debug, Deserialize, Default)]
387pub struct ContainedLabel {
388    #[serde(flatten)]
389    pub container: ContainerStyle,
390    #[serde(flatten)]
391    pub label: LabelStyle,
392}
393
394#[derive(Clone, Deserialize, Default)]
395pub struct ProjectDiagnostics {
396    #[serde(flatten)]
397    pub container: ContainerStyle,
398    pub empty_message: TextStyle,
399    pub tab_icon_width: f32,
400    pub tab_icon_spacing: f32,
401    pub tab_summary_spacing: f32,
402}
403
404#[derive(Deserialize, Default)]
405pub struct ContactNotification {
406    pub header_avatar: ImageStyle,
407    pub header_message: ContainedText,
408    pub header_height: f32,
409    pub body_message: ContainedText,
410    pub button: Interactive<ContainedText>,
411    pub dismiss_button: Interactive<IconButton>,
412}
413
414#[derive(Deserialize, Default)]
415pub struct UpdateNotification {
416    pub message: ContainedText,
417    pub action_message: Interactive<ContainedText>,
418    pub dismiss_button: Interactive<IconButton>,
419}
420
421#[derive(Clone, Deserialize, Default)]
422pub struct Editor {
423    pub text_color: Color,
424    #[serde(default)]
425    pub background: Color,
426    pub selection: SelectionStyle,
427    pub gutter_background: Color,
428    pub gutter_padding_factor: f32,
429    pub active_line_background: Color,
430    pub highlighted_line_background: Color,
431    pub rename_fade: f32,
432    pub document_highlight_read_background: Color,
433    pub document_highlight_write_background: Color,
434    pub diff_background_deleted: Color,
435    pub diff_background_inserted: Color,
436    pub line_number: Color,
437    pub line_number_active: Color,
438    pub guest_selections: Vec<SelectionStyle>,
439    pub syntax: Arc<SyntaxTheme>,
440    pub diagnostic_path_header: DiagnosticPathHeader,
441    pub diagnostic_header: DiagnosticHeader,
442    pub error_diagnostic: DiagnosticStyle,
443    pub invalid_error_diagnostic: DiagnosticStyle,
444    pub warning_diagnostic: DiagnosticStyle,
445    pub invalid_warning_diagnostic: DiagnosticStyle,
446    pub information_diagnostic: DiagnosticStyle,
447    pub invalid_information_diagnostic: DiagnosticStyle,
448    pub hint_diagnostic: DiagnosticStyle,
449    pub invalid_hint_diagnostic: DiagnosticStyle,
450    pub autocomplete: AutocompleteStyle,
451    pub code_actions_indicator: Color,
452    pub unnecessary_code_fade: f32,
453    pub hover_popover: HoverPopover,
454    pub link_definition: HighlightStyle,
455    pub composition_mark: HighlightStyle,
456    pub jump_icon: Interactive<IconButton>,
457}
458
459#[derive(Clone, Deserialize, Default)]
460pub struct DiagnosticPathHeader {
461    #[serde(flatten)]
462    pub container: ContainerStyle,
463    pub filename: ContainedText,
464    pub path: ContainedText,
465    pub text_scale_factor: f32,
466}
467
468#[derive(Clone, Deserialize, Default)]
469pub struct DiagnosticHeader {
470    #[serde(flatten)]
471    pub container: ContainerStyle,
472    pub message: ContainedLabel,
473    pub code: ContainedText,
474    pub text_scale_factor: f32,
475    pub icon_width_factor: f32,
476}
477
478#[derive(Clone, Deserialize, Default)]
479pub struct DiagnosticStyle {
480    pub message: LabelStyle,
481    #[serde(default)]
482    pub header: ContainerStyle,
483    pub text_scale_factor: f32,
484}
485
486#[derive(Clone, Deserialize, Default)]
487pub struct AutocompleteStyle {
488    #[serde(flatten)]
489    pub container: ContainerStyle,
490    pub item: ContainerStyle,
491    pub selected_item: ContainerStyle,
492    pub hovered_item: ContainerStyle,
493    pub match_highlight: HighlightStyle,
494}
495
496#[derive(Clone, Copy, Default, Deserialize)]
497pub struct SelectionStyle {
498    pub cursor: Color,
499    pub selection: Color,
500}
501
502#[derive(Clone, Deserialize, Default)]
503pub struct FieldEditor {
504    #[serde(flatten)]
505    pub container: ContainerStyle,
506    pub text: TextStyle,
507    #[serde(default)]
508    pub placeholder_text: Option<TextStyle>,
509    pub selection: SelectionStyle,
510}
511
512#[derive(Debug, Default, Clone, Copy)]
513pub struct Interactive<T> {
514    pub default: T,
515    pub hover: Option<T>,
516    pub active: Option<T>,
517    pub disabled: Option<T>,
518}
519
520impl<T> Interactive<T> {
521    pub fn style_for(&self, state: MouseState, active: bool) -> &T {
522        if active {
523            self.active.as_ref().unwrap_or(&self.default)
524        } else if state.hovered {
525            self.hover.as_ref().unwrap_or(&self.default)
526        } else {
527            &self.default
528        }
529    }
530
531    pub fn disabled_style(&self) -> &T {
532        self.disabled.as_ref().unwrap_or(&self.default)
533    }
534}
535
536impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
537    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
538    where
539        D: serde::Deserializer<'de>,
540    {
541        #[derive(Deserialize)]
542        struct Helper {
543            #[serde(flatten)]
544            default: Value,
545            hover: Option<Value>,
546            active: Option<Value>,
547            disabled: Option<Value>,
548        }
549
550        let json = Helper::deserialize(deserializer)?;
551
552        let deserialize_state = |state_json: Option<Value>| -> Result<Option<T>, D::Error> {
553            if let Some(mut state_json) = state_json {
554                if let Value::Object(state_json) = &mut state_json {
555                    if let Value::Object(default) = &json.default {
556                        for (key, value) in default {
557                            if !state_json.contains_key(key) {
558                                state_json.insert(key.clone(), value.clone());
559                            }
560                        }
561                    }
562                }
563                Ok(Some(
564                    serde_json::from_value::<T>(state_json).map_err(serde::de::Error::custom)?,
565                ))
566            } else {
567                Ok(None)
568            }
569        };
570
571        let hover = deserialize_state(json.hover)?;
572        let active = deserialize_state(json.active)?;
573        let disabled = deserialize_state(json.disabled)?;
574        let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
575
576        Ok(Interactive {
577            default,
578            hover,
579            active,
580            disabled,
581        })
582    }
583}
584
585impl Editor {
586    pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
587        let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
588        if style_ix == 0 {
589            &self.selection
590        } else {
591            &self.guest_selections[style_ix - 1]
592        }
593    }
594}
595
596#[derive(Default)]
597pub struct SyntaxTheme {
598    pub highlights: Vec<(String, HighlightStyle)>,
599}
600
601impl SyntaxTheme {
602    pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
603        Self { highlights }
604    }
605}
606
607impl<'de> Deserialize<'de> for SyntaxTheme {
608    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
609    where
610        D: serde::Deserializer<'de>,
611    {
612        let syntax_data: HashMap<String, HighlightStyle> = Deserialize::deserialize(deserializer)?;
613
614        let mut result = Self::new(Vec::new());
615        for (key, style) in syntax_data {
616            match result
617                .highlights
618                .binary_search_by(|(needle, _)| needle.cmp(&key))
619            {
620                Ok(i) | Err(i) => {
621                    result.highlights.insert(i, (key, style));
622                }
623            }
624        }
625
626        Ok(result)
627    }
628}
629
630#[derive(Clone, Deserialize, Default)]
631pub struct HoverPopover {
632    pub container: ContainerStyle,
633    pub block_style: ContainerStyle,
634    pub prose: TextStyle,
635    pub highlight: Color,
636}
637
638#[derive(Clone, Deserialize, Default)]
639pub struct TerminalStyle {
640    pub colors: TerminalColors,
641    pub modal_container: ContainerStyle,
642}
643
644#[derive(Clone, Deserialize, Default)]
645pub struct TerminalColors {
646    pub black: Color,
647    pub red: Color,
648    pub green: Color,
649    pub yellow: Color,
650    pub blue: Color,
651    pub magenta: Color,
652    pub cyan: Color,
653    pub white: Color,
654    pub bright_black: Color,
655    pub bright_red: Color,
656    pub bright_green: Color,
657    pub bright_yellow: Color,
658    pub bright_blue: Color,
659    pub bright_magenta: Color,
660    pub bright_cyan: Color,
661    pub bright_white: Color,
662    pub foreground: Color,
663    pub background: Color,
664    pub modal_background: Color,
665    pub cursor: Color,
666    pub dim_black: Color,
667    pub dim_red: Color,
668    pub dim_green: Color,
669    pub dim_yellow: Color,
670    pub dim_blue: Color,
671    pub dim_magenta: Color,
672    pub dim_cyan: Color,
673    pub dim_white: Color,
674    pub bright_foreground: Color,
675    pub dim_foreground: Color,
676}