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