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