theme.rs

   1mod theme_registry;
   2mod theme_settings;
   3pub mod ui;
   4
   5use gpui::{
   6    color::Color,
   7    elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle},
   8    fonts::{HighlightStyle, TextStyle},
   9    platform, AppContext, AssetSource, Border, MouseState,
  10};
  11use schemars::JsonSchema;
  12use serde::{de::DeserializeOwned, Deserialize};
  13use serde_json::Value;
  14use settings::SettingsStore;
  15use std::{collections::HashMap, sync::Arc};
  16use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle};
  17
  18pub use theme_registry::*;
  19pub use theme_settings::*;
  20
  21pub fn current(cx: &AppContext) -> Arc<Theme> {
  22    settings::get::<ThemeSettings>(cx).theme.clone()
  23}
  24
  25pub fn init(source: impl AssetSource, cx: &mut AppContext) {
  26    cx.set_global(ThemeRegistry::new(source, cx.font_cache().clone()));
  27    settings::register::<ThemeSettings>(cx);
  28
  29    let mut prev_buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
  30    cx.observe_global::<SettingsStore, _>(move |cx| {
  31        let buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
  32        if buffer_font_size != prev_buffer_font_size {
  33            prev_buffer_font_size = buffer_font_size;
  34            reset_font_size(cx);
  35        }
  36    })
  37    .detach();
  38}
  39
  40#[derive(Deserialize, Default, JsonSchema)]
  41pub struct Theme {
  42    #[serde(default)]
  43    pub meta: ThemeMeta,
  44    pub workspace: Workspace,
  45    pub context_menu: ContextMenu,
  46    pub contacts_popover: ContactsPopover,
  47    pub contact_list: ContactList,
  48    pub lsp_log_menu: LspLogMenu,
  49    pub copilot: Copilot,
  50    pub contact_finder: ContactFinder,
  51    pub project_panel: ProjectPanel,
  52    pub command_palette: CommandPalette,
  53    pub picker: Picker,
  54    pub editor: Editor,
  55    pub search: Search,
  56    pub project_diagnostics: ProjectDiagnostics,
  57    pub shared_screen: ContainerStyle,
  58    pub contact_notification: ContactNotification,
  59    pub update_notification: UpdateNotification,
  60    pub simple_message_notification: MessageNotification,
  61    pub project_shared_notification: ProjectSharedNotification,
  62    pub incoming_call_notification: IncomingCallNotification,
  63    pub tooltip: TooltipStyle,
  64    pub terminal: TerminalStyle,
  65    pub assistant: AssistantStyle,
  66    pub feedback: FeedbackStyle,
  67    pub welcome: WelcomeStyle,
  68    pub color_scheme: ColorScheme,
  69}
  70
  71#[derive(Deserialize, Default, Clone, JsonSchema)]
  72pub struct ThemeMeta {
  73    #[serde(skip_deserializing)]
  74    pub id: usize,
  75    pub name: String,
  76    pub is_light: bool,
  77}
  78
  79#[derive(Deserialize, Default, JsonSchema)]
  80pub struct Workspace {
  81    pub background: Color,
  82    pub blank_pane: BlankPaneStyle,
  83    pub titlebar: Titlebar,
  84    pub tab_bar: TabBar,
  85    pub pane_divider: Border,
  86    pub leader_border_opacity: f32,
  87    pub leader_border_width: f32,
  88    pub dock: Dock,
  89    pub status_bar: StatusBar,
  90    pub toolbar: Toolbar,
  91    pub breadcrumb_height: f32,
  92    pub breadcrumbs: Interactive<ContainedText>,
  93    pub disconnected_overlay: ContainedText,
  94    pub modal: ContainerStyle,
  95    pub zoomed_panel_foreground: ContainerStyle,
  96    pub zoomed_pane_foreground: ContainerStyle,
  97    pub zoomed_background: ContainerStyle,
  98    pub notification: ContainerStyle,
  99    pub notifications: Notifications,
 100    pub joining_project_avatar: ImageStyle,
 101    pub joining_project_message: ContainedText,
 102    pub external_location_message: ContainedText,
 103    pub drop_target_overlay_color: Color,
 104}
 105
 106#[derive(Clone, Deserialize, Default, JsonSchema)]
 107pub struct BlankPaneStyle {
 108    pub logo: SvgStyle,
 109    pub logo_shadow: SvgStyle,
 110    pub logo_container: ContainerStyle,
 111    pub keyboard_hints: ContainerStyle,
 112    pub keyboard_hint: Interactive<ContainedText>,
 113    pub keyboard_hint_width: f32,
 114}
 115
 116#[derive(Clone, Deserialize, Default, JsonSchema)]
 117pub struct Titlebar {
 118    #[serde(flatten)]
 119    pub container: ContainerStyle,
 120    pub height: f32,
 121    pub title: TextStyle,
 122    pub highlight_color: Color,
 123    pub item_spacing: f32,
 124    pub face_pile_spacing: f32,
 125    pub avatar_ribbon: AvatarRibbon,
 126    pub follower_avatar_overlap: f32,
 127    pub leader_selection: ContainerStyle,
 128    pub offline_icon: OfflineIcon,
 129    pub leader_avatar: AvatarStyle,
 130    pub follower_avatar: AvatarStyle,
 131    pub inactive_avatar_grayscale: bool,
 132    pub sign_in_prompt: Interactive<ContainedText>,
 133    pub outdated_warning: ContainedText,
 134    pub share_button: Interactive<ContainedText>,
 135    pub call_control: Interactive<IconButton>,
 136    pub toggle_contacts_button: Interactive<IconButton>,
 137    pub user_menu_button: Interactive<IconButton>,
 138    pub toggle_contacts_badge: ContainerStyle,
 139}
 140
 141#[derive(Copy, Clone, Deserialize, Default, JsonSchema)]
 142pub struct AvatarStyle {
 143    #[serde(flatten)]
 144    pub image: ImageStyle,
 145    pub outer_width: f32,
 146    pub outer_corner_radius: f32,
 147}
 148
 149#[derive(Deserialize, Default, Clone, JsonSchema)]
 150pub struct Copilot {
 151    pub out_link_icon: Interactive<IconStyle>,
 152    pub modal: ModalStyle,
 153    pub auth: CopilotAuth,
 154}
 155
 156#[derive(Deserialize, Default, Clone, JsonSchema)]
 157pub struct CopilotAuth {
 158    pub content_width: f32,
 159    pub prompting: CopilotAuthPrompting,
 160    pub not_authorized: CopilotAuthNotAuthorized,
 161    pub authorized: CopilotAuthAuthorized,
 162    pub cta_button: ButtonStyle,
 163    pub header: IconStyle,
 164}
 165
 166#[derive(Deserialize, Default, Clone, JsonSchema)]
 167pub struct CopilotAuthPrompting {
 168    pub subheading: ContainedText,
 169    pub hint: ContainedText,
 170    pub device_code: DeviceCode,
 171}
 172
 173#[derive(Deserialize, Default, Clone, JsonSchema)]
 174pub struct DeviceCode {
 175    pub text: TextStyle,
 176    pub cta: ButtonStyle,
 177    pub left: f32,
 178    pub left_container: ContainerStyle,
 179    pub right: f32,
 180    pub right_container: Interactive<ContainerStyle>,
 181}
 182
 183#[derive(Deserialize, Default, Clone, JsonSchema)]
 184pub struct CopilotAuthNotAuthorized {
 185    pub subheading: ContainedText,
 186    pub warning: ContainedText,
 187}
 188
 189#[derive(Deserialize, Default, Clone, JsonSchema)]
 190pub struct CopilotAuthAuthorized {
 191    pub subheading: ContainedText,
 192    pub hint: ContainedText,
 193}
 194
 195#[derive(Deserialize, Default, JsonSchema)]
 196pub struct ContactsPopover {
 197    #[serde(flatten)]
 198    pub container: ContainerStyle,
 199    pub height: f32,
 200    pub width: f32,
 201}
 202
 203#[derive(Deserialize, Default, JsonSchema)]
 204pub struct ContactList {
 205    pub user_query_editor: FieldEditor,
 206    pub user_query_editor_height: f32,
 207    pub add_contact_button: IconButton,
 208    pub header_row: Interactive<ContainedText>,
 209    pub leave_call: Interactive<ContainedText>,
 210    pub contact_row: Interactive<ContainerStyle>,
 211    pub row_height: f32,
 212    pub project_row: Interactive<ProjectRow>,
 213    pub tree_branch: Interactive<TreeBranch>,
 214    pub contact_avatar: ImageStyle,
 215    pub contact_status_free: ContainerStyle,
 216    pub contact_status_busy: ContainerStyle,
 217    pub contact_username: ContainedText,
 218    pub contact_button: Interactive<IconButton>,
 219    pub contact_button_spacing: f32,
 220    pub disabled_button: IconButton,
 221    pub section_icon_size: f32,
 222    pub calling_indicator: ContainedText,
 223}
 224
 225#[derive(Deserialize, Default, JsonSchema)]
 226pub struct ProjectRow {
 227    #[serde(flatten)]
 228    pub container: ContainerStyle,
 229    pub icon: Icon,
 230    pub name: ContainedText,
 231}
 232
 233#[derive(Deserialize, Default, Clone, Copy, JsonSchema)]
 234pub struct TreeBranch {
 235    pub width: f32,
 236    pub color: Color,
 237}
 238
 239#[derive(Deserialize, Default, JsonSchema)]
 240pub struct ContactFinder {
 241    pub picker: Picker,
 242    pub row_height: f32,
 243    pub contact_avatar: ImageStyle,
 244    pub contact_username: ContainerStyle,
 245    pub contact_button: IconButton,
 246    pub disabled_contact_button: IconButton,
 247}
 248
 249#[derive(Deserialize, Default, JsonSchema)]
 250pub struct LspLogMenu {
 251    #[serde(flatten)]
 252    pub container: ContainerStyle,
 253    pub header: Interactive<ContainedText>,
 254    pub server: ContainedText,
 255    pub item: Interactive<ContainedText>,
 256    pub row_height: f32,
 257}
 258
 259#[derive(Clone, Deserialize, Default, JsonSchema)]
 260pub struct TabBar {
 261    #[serde(flatten)]
 262    pub container: ContainerStyle,
 263    pub pane_button: Interactive<IconButton>,
 264    pub pane_button_container: ContainerStyle,
 265    pub active_pane: TabStyles,
 266    pub inactive_pane: TabStyles,
 267    pub dragged_tab: Tab,
 268    pub height: f32,
 269}
 270
 271impl TabBar {
 272    pub fn tab_style(&self, pane_active: bool, tab_active: bool) -> &Tab {
 273        let tabs = if pane_active {
 274            &self.active_pane
 275        } else {
 276            &self.inactive_pane
 277        };
 278
 279        if tab_active {
 280            &tabs.active_tab
 281        } else {
 282            &tabs.inactive_tab
 283        }
 284    }
 285}
 286
 287#[derive(Clone, Deserialize, Default, JsonSchema)]
 288pub struct TabStyles {
 289    pub active_tab: Tab,
 290    pub inactive_tab: Tab,
 291}
 292
 293#[derive(Clone, Deserialize, Default, JsonSchema)]
 294pub struct AvatarRibbon {
 295    #[serde(flatten)]
 296    pub container: ContainerStyle,
 297    pub width: f32,
 298    pub height: f32,
 299}
 300
 301#[derive(Clone, Deserialize, Default, JsonSchema)]
 302pub struct OfflineIcon {
 303    #[serde(flatten)]
 304    pub container: ContainerStyle,
 305    pub width: f32,
 306    pub color: Color,
 307}
 308
 309#[derive(Clone, Deserialize, Default, JsonSchema)]
 310pub struct Tab {
 311    pub height: f32,
 312    #[serde(flatten)]
 313    pub container: ContainerStyle,
 314    #[serde(flatten)]
 315    pub label: LabelStyle,
 316    pub description: ContainedText,
 317    pub spacing: f32,
 318    pub close_icon_width: f32,
 319    pub type_icon_width: f32,
 320    pub icon_close: Color,
 321    pub icon_close_active: Color,
 322    pub icon_dirty: Color,
 323    pub icon_conflict: Color,
 324}
 325
 326#[derive(Clone, Deserialize, Default, JsonSchema)]
 327pub struct Toolbar {
 328    #[serde(flatten)]
 329    pub container: ContainerStyle,
 330    pub height: f32,
 331    pub item_spacing: f32,
 332    pub nav_button: Interactive<IconButton>,
 333}
 334
 335#[derive(Clone, Deserialize, Default, JsonSchema)]
 336pub struct Notifications {
 337    #[serde(flatten)]
 338    pub container: ContainerStyle,
 339    pub width: f32,
 340}
 341
 342#[derive(Clone, Deserialize, Default, JsonSchema)]
 343pub struct Search {
 344    #[serde(flatten)]
 345    pub container: ContainerStyle,
 346    pub editor: FindEditor,
 347    pub invalid_editor: ContainerStyle,
 348    pub option_button_group: ContainerStyle,
 349    pub include_exclude_editor: FindEditor,
 350    pub invalid_include_exclude_editor: ContainerStyle,
 351    pub include_exclude_inputs: ContainedText,
 352    pub option_button: Interactive<ContainedText>,
 353    pub match_background: Color,
 354    pub match_index: ContainedText,
 355    pub results_status: TextStyle,
 356    pub dismiss_button: Interactive<IconButton>,
 357}
 358
 359#[derive(Clone, Deserialize, Default, JsonSchema)]
 360pub struct FindEditor {
 361    #[serde(flatten)]
 362    pub input: FieldEditor,
 363    pub min_width: f32,
 364    pub max_width: f32,
 365}
 366
 367#[derive(Deserialize, Default, JsonSchema)]
 368pub struct StatusBar {
 369    #[serde(flatten)]
 370    pub container: ContainerStyle,
 371    pub height: f32,
 372    pub item_spacing: f32,
 373    pub cursor_position: TextStyle,
 374    pub active_language: Interactive<ContainedText>,
 375    pub auto_update_progress_message: TextStyle,
 376    pub auto_update_done_message: TextStyle,
 377    pub lsp_status: Interactive<StatusBarLspStatus>,
 378    pub panel_buttons: StatusBarPanelButtons,
 379    pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
 380    pub diagnostic_message: Interactive<ContainedText>,
 381}
 382
 383#[derive(Deserialize, Default, JsonSchema)]
 384pub struct StatusBarPanelButtons {
 385    pub group_left: ContainerStyle,
 386    pub group_bottom: ContainerStyle,
 387    pub group_right: ContainerStyle,
 388    pub button: Interactive<PanelButton>,
 389}
 390
 391#[derive(Deserialize, Default, JsonSchema)]
 392pub struct StatusBarDiagnosticSummary {
 393    pub container_ok: ContainerStyle,
 394    pub container_warning: ContainerStyle,
 395    pub container_error: ContainerStyle,
 396    pub text: TextStyle,
 397    pub icon_color_ok: Color,
 398    pub icon_color_warning: Color,
 399    pub icon_color_error: Color,
 400    pub height: f32,
 401    pub icon_width: f32,
 402    pub icon_spacing: f32,
 403    pub summary_spacing: f32,
 404}
 405
 406#[derive(Deserialize, Default, JsonSchema)]
 407pub struct StatusBarLspStatus {
 408    #[serde(flatten)]
 409    pub container: ContainerStyle,
 410    pub height: f32,
 411    pub icon_spacing: f32,
 412    pub icon_color: Color,
 413    pub icon_width: f32,
 414    pub message: TextStyle,
 415}
 416
 417#[derive(Deserialize, Default, JsonSchema)]
 418pub struct Dock {
 419    pub left: ContainerStyle,
 420    pub bottom: ContainerStyle,
 421    pub right: ContainerStyle,
 422}
 423
 424#[derive(Clone, Deserialize, Default, JsonSchema)]
 425pub struct PanelButton {
 426    #[serde(flatten)]
 427    pub container: ContainerStyle,
 428    pub icon_color: Color,
 429    pub icon_size: f32,
 430    pub label: ContainedText,
 431}
 432
 433#[derive(Deserialize, Default, JsonSchema)]
 434pub struct ProjectPanel {
 435    #[serde(flatten)]
 436    pub container: ContainerStyle,
 437    pub entry: Interactive<ProjectPanelEntry>,
 438    pub dragged_entry: ProjectPanelEntry,
 439    pub ignored_entry: Interactive<ProjectPanelEntry>,
 440    pub cut_entry: Interactive<ProjectPanelEntry>,
 441    pub filename_editor: FieldEditor,
 442    pub indent_width: f32,
 443    pub open_project_button: Interactive<ContainedText>,
 444}
 445
 446#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 447pub struct ProjectPanelEntry {
 448    pub height: f32,
 449    #[serde(flatten)]
 450    pub container: ContainerStyle,
 451    pub text: TextStyle,
 452    pub icon_color: Color,
 453    pub icon_size: f32,
 454    pub icon_spacing: f32,
 455    pub status: EntryStatus,
 456}
 457
 458#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 459pub struct EntryStatus {
 460    pub git: GitProjectStatus,
 461}
 462
 463#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 464pub struct GitProjectStatus {
 465    pub modified: Color,
 466    pub inserted: Color,
 467    pub conflict: Color,
 468}
 469
 470#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 471pub struct ContextMenu {
 472    #[serde(flatten)]
 473    pub container: ContainerStyle,
 474    pub item: Interactive<ContextMenuItem>,
 475    pub keystroke_margin: f32,
 476    pub separator: ContainerStyle,
 477}
 478
 479#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 480pub struct ContextMenuItem {
 481    #[serde(flatten)]
 482    pub container: ContainerStyle,
 483    pub label: TextStyle,
 484    pub keystroke: ContainedText,
 485    pub icon_width: f32,
 486    pub icon_spacing: f32,
 487}
 488
 489#[derive(Debug, Deserialize, Default, JsonSchema)]
 490pub struct CommandPalette {
 491    pub key: Interactive<ContainedLabel>,
 492    pub keystroke_spacing: f32,
 493}
 494
 495#[derive(Deserialize, Default, JsonSchema)]
 496pub struct InviteLink {
 497    #[serde(flatten)]
 498    pub container: ContainerStyle,
 499    #[serde(flatten)]
 500    pub label: LabelStyle,
 501    pub icon: Icon,
 502}
 503
 504#[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
 505pub struct Icon {
 506    #[serde(flatten)]
 507    pub container: ContainerStyle,
 508    pub color: Color,
 509    pub width: f32,
 510}
 511
 512#[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
 513pub struct IconButton {
 514    #[serde(flatten)]
 515    pub container: ContainerStyle,
 516    pub color: Color,
 517    pub icon_width: f32,
 518    pub button_width: f32,
 519}
 520
 521#[derive(Deserialize, Default, JsonSchema)]
 522pub struct ChatMessage {
 523    #[serde(flatten)]
 524    pub container: ContainerStyle,
 525    pub body: TextStyle,
 526    pub sender: ContainedText,
 527    pub timestamp: ContainedText,
 528}
 529
 530#[derive(Deserialize, Default, JsonSchema)]
 531pub struct ChannelSelect {
 532    #[serde(flatten)]
 533    pub container: ContainerStyle,
 534    pub header: ChannelName,
 535    pub item: ChannelName,
 536    pub active_item: ChannelName,
 537    pub hovered_item: ChannelName,
 538    pub hovered_active_item: ChannelName,
 539    pub menu: ContainerStyle,
 540}
 541
 542#[derive(Deserialize, Default, JsonSchema)]
 543pub struct ChannelName {
 544    #[serde(flatten)]
 545    pub container: ContainerStyle,
 546    pub hash: ContainedText,
 547    pub name: TextStyle,
 548}
 549
 550#[derive(Clone, Deserialize, Default, JsonSchema)]
 551pub struct Picker {
 552    #[serde(flatten)]
 553    pub container: ContainerStyle,
 554    pub empty_container: ContainerStyle,
 555    pub input_editor: FieldEditor,
 556    pub empty_input_editor: FieldEditor,
 557    pub no_matches: ContainedLabel,
 558    pub item: Interactive<ContainedLabel>,
 559}
 560
 561#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 562pub struct ContainedText {
 563    #[serde(flatten)]
 564    pub container: ContainerStyle,
 565    #[serde(flatten)]
 566    pub text: TextStyle,
 567}
 568
 569#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 570pub struct ContainedLabel {
 571    #[serde(flatten)]
 572    pub container: ContainerStyle,
 573    #[serde(flatten)]
 574    pub label: LabelStyle,
 575}
 576
 577#[derive(Clone, Deserialize, Default, JsonSchema)]
 578pub struct ProjectDiagnostics {
 579    #[serde(flatten)]
 580    pub container: ContainerStyle,
 581    pub empty_message: TextStyle,
 582    pub tab_icon_width: f32,
 583    pub tab_icon_spacing: f32,
 584    pub tab_summary_spacing: f32,
 585}
 586
 587#[derive(Deserialize, Default, JsonSchema)]
 588pub struct ContactNotification {
 589    pub header_avatar: ImageStyle,
 590    pub header_message: ContainedText,
 591    pub header_height: f32,
 592    pub body_message: ContainedText,
 593    pub button: Interactive<ContainedText>,
 594    pub dismiss_button: Interactive<IconButton>,
 595}
 596
 597#[derive(Deserialize, Default, JsonSchema)]
 598pub struct UpdateNotification {
 599    pub message: ContainedText,
 600    pub action_message: Interactive<ContainedText>,
 601    pub dismiss_button: Interactive<IconButton>,
 602}
 603
 604#[derive(Deserialize, Default, JsonSchema)]
 605pub struct MessageNotification {
 606    pub message: ContainedText,
 607    pub action_message: Interactive<ContainedText>,
 608    pub dismiss_button: Interactive<IconButton>,
 609}
 610
 611#[derive(Deserialize, Default, JsonSchema)]
 612pub struct ProjectSharedNotification {
 613    pub window_height: f32,
 614    pub window_width: f32,
 615    #[serde(default)]
 616    pub background: Color,
 617    pub owner_container: ContainerStyle,
 618    pub owner_avatar: ImageStyle,
 619    pub owner_metadata: ContainerStyle,
 620    pub owner_username: ContainedText,
 621    pub message: ContainedText,
 622    pub worktree_roots: ContainedText,
 623    pub button_width: f32,
 624    pub open_button: ContainedText,
 625    pub dismiss_button: ContainedText,
 626}
 627
 628#[derive(Deserialize, Default, JsonSchema)]
 629pub struct IncomingCallNotification {
 630    pub window_height: f32,
 631    pub window_width: f32,
 632    #[serde(default)]
 633    pub background: Color,
 634    pub caller_container: ContainerStyle,
 635    pub caller_avatar: ImageStyle,
 636    pub caller_metadata: ContainerStyle,
 637    pub caller_username: ContainedText,
 638    pub caller_message: ContainedText,
 639    pub worktree_roots: ContainedText,
 640    pub button_width: f32,
 641    pub accept_button: ContainedText,
 642    pub decline_button: ContainedText,
 643}
 644
 645#[derive(Clone, Deserialize, Default, JsonSchema)]
 646pub struct Editor {
 647    pub text_color: Color,
 648    #[serde(default)]
 649    pub background: Color,
 650    pub selection: SelectionStyle,
 651    pub gutter_background: Color,
 652    pub gutter_padding_factor: f32,
 653    pub active_line_background: Color,
 654    pub highlighted_line_background: Color,
 655    pub rename_fade: f32,
 656    pub document_highlight_read_background: Color,
 657    pub document_highlight_write_background: Color,
 658    pub diff: DiffStyle,
 659    pub line_number: Color,
 660    pub line_number_active: Color,
 661    pub guest_selections: Vec<SelectionStyle>,
 662    pub syntax: Arc<SyntaxTheme>,
 663    pub suggestion: HighlightStyle,
 664    pub diagnostic_path_header: DiagnosticPathHeader,
 665    pub diagnostic_header: DiagnosticHeader,
 666    pub error_diagnostic: DiagnosticStyle,
 667    pub invalid_error_diagnostic: DiagnosticStyle,
 668    pub warning_diagnostic: DiagnosticStyle,
 669    pub invalid_warning_diagnostic: DiagnosticStyle,
 670    pub information_diagnostic: DiagnosticStyle,
 671    pub invalid_information_diagnostic: DiagnosticStyle,
 672    pub hint_diagnostic: DiagnosticStyle,
 673    pub invalid_hint_diagnostic: DiagnosticStyle,
 674    pub autocomplete: AutocompleteStyle,
 675    pub code_actions: CodeActions,
 676    pub folds: Folds,
 677    pub unnecessary_code_fade: f32,
 678    pub hover_popover: HoverPopover,
 679    pub link_definition: HighlightStyle,
 680    pub composition_mark: HighlightStyle,
 681    pub jump_icon: Interactive<IconButton>,
 682    pub scrollbar: Scrollbar,
 683    pub whitespace: Color,
 684}
 685
 686#[derive(Clone, Deserialize, Default, JsonSchema)]
 687pub struct Scrollbar {
 688    pub track: ContainerStyle,
 689    pub thumb: ContainerStyle,
 690    pub width: f32,
 691    pub min_height_factor: f32,
 692    pub git: GitDiffColors,
 693}
 694
 695#[derive(Clone, Deserialize, Default, JsonSchema)]
 696pub struct GitDiffColors {
 697    pub inserted: Color,
 698    pub modified: Color,
 699    pub deleted: Color,
 700}
 701
 702#[derive(Clone, Deserialize, Default, JsonSchema)]
 703pub struct DiagnosticPathHeader {
 704    #[serde(flatten)]
 705    pub container: ContainerStyle,
 706    pub filename: ContainedText,
 707    pub path: ContainedText,
 708    pub text_scale_factor: f32,
 709}
 710
 711#[derive(Clone, Deserialize, Default, JsonSchema)]
 712pub struct DiagnosticHeader {
 713    #[serde(flatten)]
 714    pub container: ContainerStyle,
 715    pub source: ContainedLabel,
 716    pub message: ContainedLabel,
 717    pub code: ContainedText,
 718    pub text_scale_factor: f32,
 719    pub icon_width_factor: f32,
 720}
 721
 722#[derive(Clone, Deserialize, Default, JsonSchema)]
 723pub struct DiagnosticStyle {
 724    pub message: LabelStyle,
 725    #[serde(default)]
 726    pub header: ContainerStyle,
 727    pub text_scale_factor: f32,
 728}
 729
 730#[derive(Clone, Deserialize, Default, JsonSchema)]
 731pub struct AutocompleteStyle {
 732    #[serde(flatten)]
 733    pub container: ContainerStyle,
 734    pub item: ContainerStyle,
 735    pub selected_item: ContainerStyle,
 736    pub hovered_item: ContainerStyle,
 737    pub match_highlight: HighlightStyle,
 738}
 739
 740#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
 741pub struct SelectionStyle {
 742    pub cursor: Color,
 743    pub selection: Color,
 744}
 745
 746#[derive(Clone, Deserialize, Default, JsonSchema)]
 747pub struct FieldEditor {
 748    #[serde(flatten)]
 749    pub container: ContainerStyle,
 750    pub text: TextStyle,
 751    #[serde(default)]
 752    pub placeholder_text: Option<TextStyle>,
 753    pub selection: SelectionStyle,
 754}
 755
 756#[derive(Clone, Deserialize, Default, JsonSchema)]
 757pub struct InteractiveColor {
 758    pub color: Color,
 759}
 760
 761#[derive(Clone, Deserialize, Default, JsonSchema)]
 762pub struct CodeActions {
 763    #[serde(default)]
 764    pub indicator: Interactive<InteractiveColor>,
 765    pub vertical_scale: f32,
 766}
 767
 768#[derive(Clone, Deserialize, Default, JsonSchema)]
 769pub struct Folds {
 770    pub indicator: Interactive<InteractiveColor>,
 771    pub ellipses: FoldEllipses,
 772    pub fold_background: Color,
 773    pub icon_margin_scale: f32,
 774    pub folded_icon: String,
 775    pub foldable_icon: String,
 776}
 777
 778#[derive(Clone, Deserialize, Default, JsonSchema)]
 779pub struct FoldEllipses {
 780    pub text_color: Color,
 781    pub background: Interactive<InteractiveColor>,
 782    pub corner_radius_factor: f32,
 783}
 784
 785#[derive(Clone, Deserialize, Default, JsonSchema)]
 786pub struct DiffStyle {
 787    pub inserted: Color,
 788    pub modified: Color,
 789    pub deleted: Color,
 790    pub removed_width_em: f32,
 791    pub width_em: f32,
 792    pub corner_radius: f32,
 793}
 794
 795#[derive(Debug, Default, Clone, Copy, JsonSchema)]
 796pub struct Interactive<T> {
 797    pub default: T,
 798    pub hover: Option<T>,
 799    pub hover_and_active: Option<T>,
 800    pub clicked: Option<T>,
 801    pub click_and_active: Option<T>,
 802    pub active: Option<T>,
 803    pub disabled: Option<T>,
 804}
 805
 806impl<T> Interactive<T> {
 807    pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T {
 808        if active {
 809            if state.hovered() {
 810                self.hover_and_active
 811                    .as_ref()
 812                    .unwrap_or(self.active.as_ref().unwrap_or(&self.default))
 813            } else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some()
 814            {
 815                self.click_and_active
 816                    .as_ref()
 817                    .unwrap_or(self.active.as_ref().unwrap_or(&self.default))
 818            } else {
 819                self.active.as_ref().unwrap_or(&self.default)
 820            }
 821        } else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
 822            self.clicked.as_ref().unwrap()
 823        } else if state.hovered() {
 824            self.hover.as_ref().unwrap_or(&self.default)
 825        } else {
 826            &self.default
 827        }
 828    }
 829
 830    pub fn disabled_style(&self) -> &T {
 831        self.disabled.as_ref().unwrap_or(&self.default)
 832    }
 833}
 834
 835impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
 836    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 837    where
 838        D: serde::Deserializer<'de>,
 839    {
 840        #[derive(Deserialize)]
 841        struct Helper {
 842            #[serde(flatten)]
 843            default: Value,
 844            hover: Option<Value>,
 845            hover_and_active: Option<Value>,
 846            clicked: Option<Value>,
 847            click_and_active: Option<Value>,
 848            active: Option<Value>,
 849            disabled: Option<Value>,
 850        }
 851
 852        let json = Helper::deserialize(deserializer)?;
 853
 854        let deserialize_state = |state_json: Option<Value>| -> Result<Option<T>, D::Error> {
 855            if let Some(mut state_json) = state_json {
 856                if let Value::Object(state_json) = &mut state_json {
 857                    if let Value::Object(default) = &json.default {
 858                        for (key, value) in default {
 859                            if !state_json.contains_key(key) {
 860                                state_json.insert(key.clone(), value.clone());
 861                            }
 862                        }
 863                    }
 864                }
 865                Ok(Some(
 866                    serde_json::from_value::<T>(state_json).map_err(serde::de::Error::custom)?,
 867                ))
 868            } else {
 869                Ok(None)
 870            }
 871        };
 872
 873        let hover = deserialize_state(json.hover)?;
 874        let hover_and_active = deserialize_state(json.hover_and_active)?;
 875        let clicked = deserialize_state(json.clicked)?;
 876        let click_and_active = deserialize_state(json.click_and_active)?;
 877        let active = deserialize_state(json.active)?;
 878        let disabled = deserialize_state(json.disabled)?;
 879        let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
 880
 881        Ok(Interactive {
 882            default,
 883            hover,
 884            hover_and_active,
 885            clicked,
 886            click_and_active,
 887            active,
 888            disabled,
 889        })
 890    }
 891}
 892
 893impl Editor {
 894    pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
 895        let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
 896        if style_ix == 0 {
 897            &self.selection
 898        } else {
 899            &self.guest_selections[style_ix - 1]
 900        }
 901    }
 902}
 903
 904#[derive(Default, JsonSchema)]
 905pub struct SyntaxTheme {
 906    pub highlights: Vec<(String, HighlightStyle)>,
 907}
 908
 909impl SyntaxTheme {
 910    pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
 911        Self { highlights }
 912    }
 913}
 914
 915impl<'de> Deserialize<'de> for SyntaxTheme {
 916    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 917    where
 918        D: serde::Deserializer<'de>,
 919    {
 920        let syntax_data: HashMap<String, HighlightStyle> = Deserialize::deserialize(deserializer)?;
 921
 922        let mut result = Self::new(Vec::new());
 923        for (key, style) in syntax_data {
 924            match result
 925                .highlights
 926                .binary_search_by(|(needle, _)| needle.cmp(&key))
 927            {
 928                Ok(i) | Err(i) => {
 929                    result.highlights.insert(i, (key, style));
 930                }
 931            }
 932        }
 933
 934        Ok(result)
 935    }
 936}
 937
 938#[derive(Clone, Deserialize, Default, JsonSchema)]
 939pub struct HoverPopover {
 940    pub container: ContainerStyle,
 941    pub info_container: ContainerStyle,
 942    pub warning_container: ContainerStyle,
 943    pub error_container: ContainerStyle,
 944    pub block_style: ContainerStyle,
 945    pub prose: TextStyle,
 946    pub diagnostic_source_highlight: HighlightStyle,
 947    pub highlight: Color,
 948}
 949
 950#[derive(Clone, Deserialize, Default, JsonSchema)]
 951pub struct TerminalStyle {
 952    pub black: Color,
 953    pub red: Color,
 954    pub green: Color,
 955    pub yellow: Color,
 956    pub blue: Color,
 957    pub magenta: Color,
 958    pub cyan: Color,
 959    pub white: Color,
 960    pub bright_black: Color,
 961    pub bright_red: Color,
 962    pub bright_green: Color,
 963    pub bright_yellow: Color,
 964    pub bright_blue: Color,
 965    pub bright_magenta: Color,
 966    pub bright_cyan: Color,
 967    pub bright_white: Color,
 968    pub foreground: Color,
 969    pub background: Color,
 970    pub modal_background: Color,
 971    pub cursor: Color,
 972    pub dim_black: Color,
 973    pub dim_red: Color,
 974    pub dim_green: Color,
 975    pub dim_yellow: Color,
 976    pub dim_blue: Color,
 977    pub dim_magenta: Color,
 978    pub dim_cyan: Color,
 979    pub dim_white: Color,
 980    pub bright_foreground: Color,
 981    pub dim_foreground: Color,
 982}
 983
 984#[derive(Clone, Deserialize, Default, JsonSchema)]
 985pub struct AssistantStyle {
 986    pub container: ContainerStyle,
 987    pub header: ContainerStyle,
 988    pub sent_at: ContainedText,
 989    pub user_sender: Interactive<ContainedText>,
 990    pub assistant_sender: Interactive<ContainedText>,
 991    pub system_sender: Interactive<ContainedText>,
 992    pub model_info_container: ContainerStyle,
 993    pub model: Interactive<ContainedText>,
 994    pub remaining_tokens: ContainedText,
 995    pub no_remaining_tokens: ContainedText,
 996    pub error_icon: Icon,
 997    pub api_key_editor: FieldEditor,
 998    pub api_key_prompt: ContainedText,
 999}
1000
1001#[derive(Clone, Deserialize, Default, JsonSchema)]
1002pub struct FeedbackStyle {
1003    pub submit_button: Interactive<ContainedText>,
1004    pub button_margin: f32,
1005    pub info_text_default: ContainedText,
1006    pub link_text_default: ContainedText,
1007    pub link_text_hover: ContainedText,
1008}
1009
1010#[derive(Clone, Deserialize, Default, JsonSchema)]
1011pub struct WelcomeStyle {
1012    pub page_width: f32,
1013    pub logo: SvgStyle,
1014    pub logo_subheading: ContainedText,
1015    pub usage_note: ContainedText,
1016    pub checkbox: CheckboxStyle,
1017    pub checkbox_container: ContainerStyle,
1018    pub button: Interactive<ContainedText>,
1019    pub button_group: ContainerStyle,
1020    pub heading_group: ContainerStyle,
1021    pub checkbox_group: ContainerStyle,
1022}
1023
1024#[derive(Clone, Deserialize, Default, JsonSchema)]
1025pub struct ColorScheme {
1026    pub name: String,
1027    pub is_light: bool,
1028    pub ramps: RampSet,
1029    pub lowest: Layer,
1030    pub middle: Layer,
1031    pub highest: Layer,
1032
1033    pub popover_shadow: Shadow,
1034    pub modal_shadow: Shadow,
1035
1036    pub players: Vec<Player>,
1037}
1038
1039#[derive(Clone, Deserialize, Default, JsonSchema)]
1040pub struct Player {
1041    pub cursor: Color,
1042    pub selection: Color,
1043}
1044
1045#[derive(Clone, Deserialize, Default, JsonSchema)]
1046pub struct RampSet {
1047    pub neutral: Vec<Color>,
1048    pub red: Vec<Color>,
1049    pub orange: Vec<Color>,
1050    pub yellow: Vec<Color>,
1051    pub green: Vec<Color>,
1052    pub cyan: Vec<Color>,
1053    pub blue: Vec<Color>,
1054    pub violet: Vec<Color>,
1055    pub magenta: Vec<Color>,
1056}
1057
1058#[derive(Clone, Deserialize, Default, JsonSchema)]
1059pub struct Layer {
1060    pub base: StyleSet,
1061    pub variant: StyleSet,
1062    pub on: StyleSet,
1063    pub accent: StyleSet,
1064    pub positive: StyleSet,
1065    pub warning: StyleSet,
1066    pub negative: StyleSet,
1067}
1068
1069#[derive(Clone, Deserialize, Default, JsonSchema)]
1070pub struct StyleSet {
1071    pub default: Style,
1072    pub active: Style,
1073    pub disabled: Style,
1074    pub hovered: Style,
1075    pub pressed: Style,
1076    pub inverted: Style,
1077}
1078
1079#[derive(Clone, Deserialize, Default, JsonSchema)]
1080pub struct Style {
1081    pub background: Color,
1082    pub border: Color,
1083    pub foreground: Color,
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088    use super::*;
1089    use schemars::schema_for;
1090
1091    #[test]
1092    fn export_schema() {
1093        let theme = schema_for!(Theme);
1094        let output = serde_json::to_string_pretty(&theme).unwrap();
1095        std::fs::create_dir("schemas").ok();
1096        std::fs::write("schemas/theme.json", output).ok();
1097    }
1098}