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