use std::ops::{Deref, DerefMut}; use gpui::{ hsla, point, px, App, BoxShadow, Global, Hsla, Pixels, SharedString, Window, WindowAppearance, }; use serde::{Deserialize, Serialize}; use crate::{scroll::ScrollbarShow, Colorize as _}; pub fn init(cx: &mut App) { Theme::sync_system_appearance(None, cx); Theme::sync_scrollbar_appearance(cx); } pub trait ActiveTheme { fn theme(&self) -> &Theme; } impl ActiveTheme for App { #[inline] fn theme(&self) -> &Theme { Theme::global(self) } } /// Make a [gpui::Hsla] color. /// /// - h: 0..360.0 /// - s: 0.0..100.0 /// - l: 0.0..100.0 #[inline] pub fn hsl(h: f32, s: f32, l: f32) -> Hsla { hsla(h / 360., s / 100.0, l / 100.0, 1.0) } /// Make a BoxShadow like CSS /// /// e.g: /// /// If CSS is `box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);` /// /// Then the equivalent in Rust is `box_shadow(0., 0., 10., 0., hsla(0., 0., 0., 0.1))` #[inline] pub fn box_shadow( x: impl Into, y: impl Into, blur: impl Into, spread: impl Into, color: Hsla, ) -> BoxShadow { BoxShadow { offset: point(x.into(), y.into()), blur_radius: blur.into(), spread_radius: spread.into(), color, } } #[derive(Debug, Clone, Copy, Default)] pub struct ThemeColor { /// Used for accents such as hover background on MenuItem, ListItem, etc. pub accent: Hsla, /// Used for accent text color. pub accent_foreground: Hsla, /// Accordion background color. pub accordion: Hsla, /// Accordion active background color. pub accordion_active: Hsla, /// Accordion hover background color. pub accordion_hover: Hsla, /// Default background color. pub background: Hsla, /// Default border color pub border: Hsla, /// Background color for Card. pub card: Hsla, /// Text color for Card. pub card_foreground: Hsla, /// Input caret color (Blinking cursor). pub caret: Hsla, /// Danger background color. pub danger: Hsla, /// Danger active background color. pub danger_active: Hsla, /// Danger text color. pub danger_foreground: Hsla, /// Danger hover background color. pub danger_hover: Hsla, /// Description List label background color. pub description_list_label: Hsla, /// Description List label foreground color. pub description_list_label_foreground: Hsla, /// Drag border color. pub drag_border: Hsla, /// Drop target background color. pub drop_target: Hsla, /// Default text color. pub foreground: Hsla, /// Info background color. pub info: Hsla, /// Info active background color. pub info_active: Hsla, /// Info text color. pub info_foreground: Hsla, /// Info hover background color. pub info_hover: Hsla, /// Border color for inputs such as Input, Dropdown, etc. pub input: Hsla, /// Link text color. pub link: Hsla, /// Active link text color. pub link_active: Hsla, /// Hover link text color. pub link_hover: Hsla, /// Background color for List and ListItem. pub list: Hsla, /// Background color for active ListItem. pub list_active: Hsla, /// Border color for active ListItem. pub list_active_border: Hsla, /// Stripe background color for even ListItem. pub list_even: Hsla, /// Background color for List header. pub list_head: Hsla, /// Hover background color for ListItem. pub list_hover: Hsla, /// Muted backgrounds such as Skeleton and Switch. pub muted: Hsla, /// Muted text color, as used in disabled text. pub muted_foreground: Hsla, /// Background color for Popover. pub popover: Hsla, /// Text color for Popover. pub popover_foreground: Hsla, /// Primary background color. pub primary: Hsla, /// Active primary background color. pub primary_active: Hsla, /// Primary text color. pub primary_foreground: Hsla, /// Hover primary background color. pub primary_hover: Hsla, /// Progress bar background color. pub progress_bar: Hsla, /// Used for focus ring. pub ring: Hsla, /// Scrollbar background color. pub scrollbar: Hsla, /// Scrollbar thumb background color. pub scrollbar_thumb: Hsla, /// Scrollbar thumb hover background color. pub scrollbar_thumb_hover: Hsla, /// Secondary background color. pub secondary: Hsla, /// Active secondary background color. pub secondary_active: Hsla, /// Secondary text color, used for secondary Button text color or secondary text. pub secondary_foreground: Hsla, /// Hover secondary background color. pub secondary_hover: Hsla, /// Input selection background color. pub selection: Hsla, /// Sidebar background color. pub sidebar: Hsla, /// Sidebar accent background color. pub sidebar_accent: Hsla, /// Sidebar accent text color. pub sidebar_accent_foreground: Hsla, /// Sidebar border color. pub sidebar_border: Hsla, /// Sidebar text color. pub sidebar_foreground: Hsla, /// Sidebar primary background color. pub sidebar_primary: Hsla, /// Sidebar primary text color. pub sidebar_primary_foreground: Hsla, /// Skeleton background color. pub skeleton: Hsla, /// Slider bar background color. pub slider_bar: Hsla, /// Slider thumb background color. pub slider_thumb: Hsla, /// Success background color. pub success: Hsla, /// Success text color. pub success_foreground: Hsla, /// Success hover background color. pub success_hover: Hsla, /// Success active background color. pub success_active: Hsla, /// Switch background color. pub switch: Hsla, /// Tab background color. pub tab: Hsla, /// Tab active background color. pub tab_active: Hsla, /// Tab active text color. pub tab_active_foreground: Hsla, /// TabBar background color. pub tab_bar: Hsla, /// TabBar segmented background color. pub tab_bar_segmented: Hsla, /// Tab text color. pub tab_foreground: Hsla, /// Table background color. pub table: Hsla, /// Table active item background color. pub table_active: Hsla, /// Table active item border color. pub table_active_border: Hsla, /// Stripe background color for even TableRow. pub table_even: Hsla, /// Table head background color. pub table_head: Hsla, /// Table head text color. pub table_head_foreground: Hsla, /// Table item hover background color. pub table_hover: Hsla, /// Table row border color. pub table_row_border: Hsla, /// TitleBar background color, use for Window title bar. pub title_bar: Hsla, /// TitleBar border color. pub title_bar_border: Hsla, /// Background color for Tiles. pub tiles: Hsla, /// Warning background color. pub warning: Hsla, /// Warning active background color. pub warning_active: Hsla, /// Warning hover background color. pub warning_hover: Hsla, /// Warning foreground color. pub warning_foreground: Hsla, /// Window border color. /// /// # Platform specific: /// /// This is only works on Linux, other platforms we can't change the window border color. pub window_border: Hsla, } impl ThemeColor { pub fn light() -> Self { Self { accent: hsl(240.0, 4.8, 93.9), accent_foreground: hsl(240.0, 5.9, 10.0), accordion: hsl(0.0, 0.0, 100.0), accordion_active: hsl(240.0, 5.9, 90.0), accordion_hover: hsl(240.0, 4.8, 95.9).opacity(0.7), background: hsl(0.0, 0.0, 100.), border: hsl(240.0, 5.9, 90.0), card: hsl(0.0, 0.0, 100.0), card_foreground: hsl(240.0, 10.0, 3.9), caret: hsl(240.0, 10., 3.9), danger: crate::red_500(), danger_active: crate::red_600(), danger_foreground: crate::red_50(), danger_hover: crate::red_500().opacity(0.9), description_list_label: hsl(240.0, 5.9, 96.9), description_list_label_foreground: hsl(240.0, 5.9, 10.0), drag_border: crate::blue_500(), drop_target: hsl(235.0, 30., 44.0).opacity(0.25), foreground: hsl(240.0, 10., 3.9), info: crate::sky_500(), info_active: crate::sky_600(), info_hover: crate::sky_500().opacity(0.9), info_foreground: crate::sky_50(), input: hsl(240.0, 5.9, 90.0), link: hsl(221.0, 83.0, 53.0), link_active: hsl(221.0, 83.0, 53.0).darken(0.2), link_hover: hsl(221.0, 83.0, 53.0).lighten(0.2), list: hsl(0.0, 0.0, 100.), list_active: hsl(211.0, 97.0, 85.0).opacity(0.2), list_active_border: hsl(211.0, 97.0, 85.0), list_even: hsl(240.0, 5.0, 96.0), list_head: hsl(0.0, 0.0, 100.), list_hover: hsl(240.0, 4.8, 95.0), muted: hsl(240.0, 4.8, 95.9), muted_foreground: hsl(240.0, 3.8, 46.1), popover: hsl(0.0, 0.0, 100.0), popover_foreground: hsl(240.0, 10.0, 3.9), primary: hsl(223.0, 5.9, 10.0), primary_active: hsl(223.0, 1.9, 25.0), primary_foreground: hsl(223.0, 0.0, 98.0), primary_hover: hsl(223.0, 5.9, 15.0), progress_bar: hsl(223.0, 5.9, 10.0), ring: hsl(240.0, 5.9, 65.0), scrollbar: hsl(0., 0., 92.).opacity(0.75), scrollbar_thumb: hsl(0., 0., 69.).opacity(0.9), scrollbar_thumb_hover: hsl(0., 0., 59.), secondary: hsl(240.0, 5.9, 96.9), secondary_active: hsl(240.0, 5.9, 93.), secondary_foreground: hsl(240.0, 59.0, 10.), secondary_hover: hsl(240.0, 5.9, 98.), selection: hsl(211.0, 97.0, 85.0), sidebar: hsl(0.0, 0.0, 98.0), sidebar_accent: hsl(240.0, 4.8, 95.9), sidebar_accent_foreground: hsl(240.0, 5.9, 10.0), sidebar_border: hsl(220.0, 13.0, 91.0), sidebar_foreground: hsl(240.0, 5.3, 26.1), sidebar_primary: hsl(240.0, 5.9, 10.0), sidebar_primary_foreground: hsl(0.0, 0.0, 98.0), skeleton: hsl(223.0, 5.9, 10.0).opacity(0.1), slider_bar: hsl(223.0, 5.9, 10.0), slider_thumb: hsl(0.0, 0.0, 100.0), success: crate::green_500(), success_active: crate::green_600(), success_hover: crate::green_500().opacity(0.9), success_foreground: crate::gray_50(), switch: hsl(240.0, 5.9, 90.0), tab: gpui::transparent_black(), tab_active: hsl(0.0, 0.0, 100.0), tab_active_foreground: hsl(240.0, 10., 3.9), tab_bar: hsl(240.0, 14.3, 95.9), tab_bar_segmented: hsl(240.0, 14.3, 95.9), tab_foreground: hsl(240.0, 10., 33.9), table: hsl(0.0, 0.0, 100.), table_active: hsl(211.0, 97.0, 85.0).opacity(0.2), table_active_border: hsl(211.0, 97.0, 85.0), table_even: hsl(240.0, 5.0, 96.0), table_head: hsl(0.0, 0.0, 100.), table_head_foreground: hsl(240.0, 5.0, 34.), table_hover: hsl(240.0, 4.8, 95.0), table_row_border: hsl(240.0, 7.7, 94.5), tiles: hsl(0.0, 0.0, 95.), title_bar: hsl(0.0, 0.0, 100.), title_bar_border: hsl(240.0, 5.9, 90.0), warning: crate::yellow_500(), warning_active: crate::yellow_600(), warning_hover: crate::yellow_500().opacity(0.9), warning_foreground: crate::gray_50(), window_border: hsl(240.0, 5.9, 78.0), } } pub fn dark() -> Self { Self { accent: hsl(240.0, 3.7, 15.9), accent_foreground: hsl(0.0, 0.0, 78.0), accordion: hsl(299.0, 2., 11.), accordion_active: hsl(240.0, 3.7, 16.9), accordion_hover: hsl(240.0, 3.7, 15.9).opacity(0.7), background: hsl(0.0, 0.0, 8.0), border: hsl(240.0, 3.7, 16.9), card: hsl(0.0, 0.0, 8.0), card_foreground: hsl(0.0, 0.0, 78.0), caret: hsl(0., 0., 78.), danger: crate::red_800(), danger_active: crate::red_800().darken(0.2), danger_foreground: crate::red_50(), danger_hover: crate::red_800().opacity(0.9), description_list_label: hsl(240.0, 0., 13.0), description_list_label_foreground: hsl(0.0, 0.0, 78.0), drag_border: crate::blue_500(), drop_target: hsl(235.0, 30., 44.0).opacity(0.1), foreground: hsl(0., 0., 78.), info: crate::sky_900(), info_active: crate::sky_900().darken(0.2), info_foreground: crate::sky_50(), info_hover: crate::sky_900().opacity(0.8), input: hsl(240.0, 3.7, 15.9), link: hsl(221.0, 83.0, 53.0), link_active: hsl(221.0, 83.0, 53.0).darken(0.2), link_hover: hsl(221.0, 83.0, 53.0).lighten(0.2), list: hsl(0.0, 0.0, 8.0), list_active: hsl(240.0, 3.7, 15.0).opacity(0.2), list_active_border: hsl(240.0, 5.9, 35.5), list_even: hsl(240.0, 3.7, 10.0), list_head: hsl(0.0, 0.0, 8.0), list_hover: hsl(240.0, 3.7, 15.9), muted: hsl(240.0, 3.7, 15.9), muted_foreground: hsl(240.0, 5.0, 34.), popover: hsl(0.0, 0.0, 10.), popover_foreground: hsl(0.0, 0.0, 78.0), primary: hsl(223.0, 0.0, 98.0), primary_active: hsl(223.0, 0.0, 80.0), primary_foreground: hsl(223.0, 5.9, 10.0), primary_hover: hsl(223.0, 0.0, 90.0), progress_bar: hsl(223.0, 0.0, 98.0), ring: hsl(240.0, 4.9, 83.9), scrollbar: hsl(240., 1., 15.).opacity(0.75), scrollbar_thumb: hsl(0., 0., 48.).opacity(0.9), scrollbar_thumb_hover: hsl(0., 0., 68.), secondary: hsl(240.0, 0., 13.0), secondary_active: hsl(240.0, 0., 13.), secondary_foreground: hsl(0.0, 0.0, 78.0), secondary_hover: hsl(240.0, 0., 15.), selection: hsl(211.0, 97.0, 22.0), sidebar: hsl(240.0, 0.0, 10.0), sidebar_accent: hsl(240.0, 3.7, 15.9), sidebar_accent_foreground: hsl(240.0, 4.8, 95.9), sidebar_border: hsl(240.0, 3.7, 15.9), sidebar_foreground: hsl(240.0, 4.8, 95.9), sidebar_primary: hsl(0.0, 0.0, 98.0), sidebar_primary_foreground: hsl(240.0, 5.9, 10.0), skeleton: hsla(223.0, 0.0, 98.0, 0.1), slider_bar: hsl(223.0, 0.0, 98.0), slider_thumb: hsl(0.0, 0.0, 8.0), success: crate::green_800(), success_active: crate::green_800().darken(0.2), success_foreground: crate::green_50(), success_hover: crate::green_800().opacity(0.8), switch: hsl(0., 0., 31.), tab: gpui::transparent_black(), tab_active: hsl(0.0, 0.0, 8.0), tab_active_foreground: hsl(0., 0., 78.), tab_bar: hsl(299.0, 0., 5.5), tab_bar_segmented: hsl(299.0, 0., 5.5), tab_foreground: hsl(0., 0., 78.), table: hsl(0.0, 0.0, 8.0), table_active: hsl(240.0, 3.7, 15.0).opacity(0.2), table_active_border: hsl(240.0, 5.9, 35.5), table_even: hsl(240.0, 3.7, 10.0), table_head: hsl(0.0, 0.0, 8.0), table_head_foreground: hsl(0., 0., 78.).opacity(0.7), table_hover: hsl(240.0, 3.7, 15.9).opacity(0.5), table_row_border: hsl(240.0, 3.7, 16.9).opacity(0.5), tiles: hsl(0.0, 0.0, 5.0), title_bar: hsl(0., 0., 9.7), title_bar_border: hsl(240.0, 3.7, 15.9), warning: crate::yellow_800(), warning_active: crate::yellow_800().darken(0.2), warning_foreground: crate::yellow_50(), warning_hover: crate::yellow_800().opacity(0.9), window_border: hsl(240.0, 3.7, 28.0), } } } #[derive(Debug, Clone)] pub struct Theme { colors: ThemeColor, pub mode: ThemeMode, pub font_family: SharedString, pub font_size: Pixels, pub radius: Pixels, pub shadow: bool, pub transparent: Hsla, /// Show the scrollbar mode, default: Scrolling pub scrollbar_show: ScrollbarShow, /// Tile grid size, default is 4px. pub tile_grid_size: Pixels, /// The shadow of the tile panel. pub tile_shadow: bool, } impl Deref for Theme { type Target = ThemeColor; fn deref(&self) -> &Self::Target { &self.colors } } impl DerefMut for Theme { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.colors } } impl Global for Theme {} impl Theme { /// Returns the global theme reference #[inline(always)] pub fn global(cx: &App) -> &Theme { cx.global::() } /// Returns the global theme mutable reference #[inline(always)] pub fn global_mut(cx: &mut App) -> &mut Theme { cx.global_mut::() } /// Returns true if the theme is dark. #[inline(always)] pub fn is_dark(&self) -> bool { self.mode.is_dark() } /// Apply a mask color to the theme. pub fn apply_color(&mut self, mask_color: Hsla) { self.title_bar = self.title_bar.apply(mask_color); self.title_bar_border = self.title_bar_border.apply(mask_color); self.background = self.background.apply(mask_color); self.foreground = self.foreground.apply(mask_color); self.card = self.card.apply(mask_color); self.card_foreground = self.card_foreground.apply(mask_color); self.caret = self.caret.apply(mask_color); self.popover = self.popover.apply(mask_color); self.popover_foreground = self.popover_foreground.apply(mask_color); self.primary = self.primary.apply(mask_color); self.primary_hover = self.primary_hover.apply(mask_color); self.primary_active = self.primary_active.apply(mask_color); self.primary_foreground = self.primary_foreground.apply(mask_color); self.secondary = self.secondary.apply(mask_color); self.secondary_hover = self.secondary_hover.apply(mask_color); self.secondary_active = self.secondary_active.apply(mask_color); self.secondary_foreground = self.secondary_foreground.apply(mask_color); // self.danger = self.danger.apply(mask_color); // self.danger_hover = self.danger_hover.apply(mask_color); // self.danger_active = self.danger_active.apply(mask_color); // self.danger_foreground = self.danger_foreground.apply(mask_color); self.muted = self.muted.apply(mask_color); self.muted_foreground = self.muted_foreground.apply(mask_color); self.accent = self.accent.apply(mask_color); self.accent_foreground = self.accent_foreground.apply(mask_color); self.border = self.border.apply(mask_color); self.input = self.input.apply(mask_color); self.ring = self.ring.apply(mask_color); // self.selection = self.selection.apply(mask_color); self.scrollbar = self.scrollbar.apply(mask_color); self.scrollbar_thumb = self.scrollbar_thumb.apply(mask_color); self.scrollbar_thumb_hover = self.scrollbar_thumb_hover.apply(mask_color); self.drag_border = self.drag_border.apply(mask_color); self.drop_target = self.drop_target.apply(mask_color); self.tab_bar = self.tab_bar.apply(mask_color); self.tab = self.tab.apply(mask_color); self.tab_active = self.tab_active.apply(mask_color); self.tab_foreground = self.tab_foreground.apply(mask_color); self.tab_active_foreground = self.tab_active_foreground.apply(mask_color); self.tab_bar_segmented = self.tab_bar_segmented.apply(mask_color); self.progress_bar = self.progress_bar.apply(mask_color); self.slider_bar = self.slider_bar.apply(mask_color); self.slider_thumb = self.slider_thumb.apply(mask_color); self.list = self.list.apply(mask_color); self.list_even = self.list_even.apply(mask_color); self.list_head = self.list_head.apply(mask_color); self.list_active = self.list_active.apply(mask_color); self.list_active_border = self.list_active_border.apply(mask_color); self.list_hover = self.list_hover.apply(mask_color); self.table = self.table.apply(mask_color); self.table_even = self.table_even.apply(mask_color); self.table_active = self.table_active.apply(mask_color); self.table_active_border = self.table_active_border.apply(mask_color); self.table_hover = self.table_hover.apply(mask_color); self.table_row_border = self.table_row_border.apply(mask_color); self.table_head = self.table_head.apply(mask_color); self.table_head_foreground = self.table_head_foreground.apply(mask_color); self.link = self.link.apply(mask_color); self.link_hover = self.link_hover.apply(mask_color); self.link_active = self.link_active.apply(mask_color); self.skeleton = self.skeleton.apply(mask_color); self.accordion = self.accordion.apply(mask_color); self.accordion_hover = self.accordion_hover.apply(mask_color); self.accordion_active = self.accordion_active.apply(mask_color); self.title_bar = self.title_bar.apply(mask_color); self.title_bar_border = self.title_bar_border.apply(mask_color); self.sidebar = self.sidebar.apply(mask_color); self.sidebar_accent = self.sidebar_accent.apply(mask_color); self.sidebar_accent_foreground = self.sidebar_accent_foreground.apply(mask_color); self.sidebar_border = self.sidebar_border.apply(mask_color); self.sidebar_foreground = self.sidebar_foreground.apply(mask_color); self.sidebar_primary = self.sidebar_primary.apply(mask_color); self.sidebar_primary_foreground = self.sidebar_primary_foreground.apply(mask_color); self.tiles = self.tiles.apply(mask_color); self.description_list_label = self.description_list_label.apply(mask_color); self.description_list_label_foreground = self.description_list_label_foreground.apply(mask_color); } /// Sync the theme with the system appearance pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) { // Better use window.appearance() for avoid error on Linux. // https://github.com/longbridge/gpui-component/issues/104 let appearance = window .as_ref() .map(|window| window.appearance()) .unwrap_or_else(|| cx.window_appearance()); Self::change(appearance, window, cx); } /// Sync the Scrollbar showing behavior with the system pub fn sync_scrollbar_appearance(cx: &mut App) { if cx.should_auto_hide_scrollbars() { cx.global_mut::().scrollbar_show = ScrollbarShow::Scrolling; } else { cx.global_mut::().scrollbar_show = ScrollbarShow::Hover; } } pub fn change(mode: impl Into, window: Option<&mut Window>, cx: &mut App) { let mode = mode.into(); let colors = match mode { ThemeMode::Light => ThemeColor::light(), ThemeMode::Dark => ThemeColor::dark(), }; if !cx.has_global::() { let theme = Theme::from(colors); cx.set_global(theme); } let theme = cx.global_mut::(); theme.mode = mode; theme.colors = colors; if let Some(window) = window { window.refresh(); } } } impl From for Theme { fn from(colors: ThemeColor) -> Self { let mode = ThemeMode::default(); Theme { mode, transparent: Hsla::transparent_black(), font_size: px(16.), font_family: if cfg!(target_os = "macos") { ".SystemUIFont".into() } else if cfg!(target_os = "windows") { "Segoe UI".into() } else { "FreeMono".into() }, radius: px(4.), shadow: true, scrollbar_show: ScrollbarShow::default(), tile_grid_size: px(8.), tile_shadow: true, colors, } } } #[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ThemeMode { Light, #[default] Dark, } impl ThemeMode { #[inline(always)] pub fn is_dark(&self) -> bool { matches!(self, Self::Dark) } } impl From for ThemeMode { fn from(appearance: WindowAppearance) -> Self { match appearance { WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, } } }