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