forked from reya/gpui-native-kit
feat: add native material backgrounds
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
|
||||
use gpui::{Hsla, hsla, rgb};
|
||||
|
||||
use crate::ThemeVariant;
|
||||
|
||||
fn hex(hex: u32) -> Hsla {
|
||||
rgb(hex).into()
|
||||
}
|
||||
@@ -115,3 +117,44 @@ impl ColorTokens {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The translucent paints surfaces lay over a native window material.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct MaterialTokens {
|
||||
/// The window-sized fill the material shows through.
|
||||
pub background: Hsla,
|
||||
/// The hover wash over material surfaces.
|
||||
pub hover: Hsla,
|
||||
/// The selected-row fill over material surfaces.
|
||||
pub selection: Hsla,
|
||||
}
|
||||
|
||||
impl MaterialTokens {
|
||||
/// Derives the material paints from the resolved palette.
|
||||
pub(crate) fn derive(
|
||||
colors: &ColorTokens,
|
||||
selection: Hsla,
|
||||
variant: ThemeVariant,
|
||||
translucent: bool,
|
||||
) -> Self {
|
||||
if !translucent {
|
||||
return Self {
|
||||
background: colors.background,
|
||||
hover: colors.muted,
|
||||
selection,
|
||||
};
|
||||
}
|
||||
|
||||
// Light tints heavier than dark: the blur carries less contrast there.
|
||||
let background_alpha = match variant {
|
||||
ThemeVariant::Light => 0.6,
|
||||
ThemeVariant::Dark => 0.5,
|
||||
};
|
||||
Self {
|
||||
background: colors.background.alpha(background_alpha),
|
||||
// A foreground wash keeps hover visible over any backdrop hue.
|
||||
hover: colors.foreground.alpha(0.06),
|
||||
selection: selection.alpha(0.85),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ mod resolve;
|
||||
mod source;
|
||||
mod system;
|
||||
|
||||
pub use colors::ColorTokens;
|
||||
pub use colors::{ColorTokens, MaterialTokens};
|
||||
pub use definition::ThemeDefinition;
|
||||
pub use ramps::{AccentShades, Oklch};
|
||||
pub use resolve::{Ramps, ResolvedTheme};
|
||||
pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme};
|
||||
|
||||
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, px};
|
||||
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, WindowBackgroundAppearance, px};
|
||||
use std::time::Duration;
|
||||
|
||||
/// The hairline width of the control border every button and field paints;
|
||||
@@ -50,6 +50,8 @@ impl ThemeVariant {
|
||||
pub struct Theme {
|
||||
pub variant: ThemeVariant,
|
||||
pub colors: ColorTokens,
|
||||
/// The translucent paints over the window's native material.
|
||||
pub material: MaterialTokens,
|
||||
pub ramps: Ramps,
|
||||
pub radius: Radius,
|
||||
/// Per-platform border and focus chrome.
|
||||
@@ -68,6 +70,8 @@ pub struct Theme {
|
||||
pub mono_font: &'static str,
|
||||
pub sizes: SizeTable,
|
||||
pub accessibility: AccessibilityPreferences,
|
||||
/// The material a window should open with, after accessibility.
|
||||
pub window_background: WindowBackgroundAppearance,
|
||||
}
|
||||
|
||||
impl Global for Theme {}
|
||||
@@ -524,6 +528,7 @@ impl Theme {
|
||||
Self {
|
||||
variant: resolved.variant,
|
||||
colors: resolved.colors,
|
||||
material: resolved.material,
|
||||
ramps: resolved.ramps,
|
||||
radius: resolved.radius,
|
||||
borders: resolved.borders,
|
||||
@@ -536,6 +541,7 @@ impl Theme {
|
||||
mono_font: resolved.mono_font,
|
||||
sizes: resolved.sizes,
|
||||
accessibility: resolved.accessibility,
|
||||
window_background: resolved.window_background,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+89
-19
@@ -2,9 +2,10 @@ use crate::definition::ThemeDefinition;
|
||||
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
||||
use crate::system::SystemTheme;
|
||||
use crate::{
|
||||
Borders, ColorTokens, Elevations, Motion, Radius, Scrollbar, SizeTable, Spacing, ThemeVariant,
|
||||
Borders, ColorTokens, Elevations, MaterialTokens, Motion, Radius, Scrollbar, SizeTable,
|
||||
Spacing, ThemeVariant,
|
||||
};
|
||||
use gpui::px;
|
||||
use gpui::{WindowBackgroundAppearance, px};
|
||||
|
||||
/// The floor of a custom titlebar's height, in px at 100% scaling: the height
|
||||
/// macOS and Linux titlebars draw at regardless of the control scale.
|
||||
@@ -26,6 +27,8 @@ pub struct Ramps {
|
||||
pub struct ResolvedTheme {
|
||||
pub variant: ThemeVariant,
|
||||
pub colors: ColorTokens,
|
||||
/// The translucent paints over the window's native material.
|
||||
pub material: MaterialTokens,
|
||||
pub ramps: Ramps,
|
||||
pub radius: Radius,
|
||||
pub borders: Borders,
|
||||
@@ -41,6 +44,8 @@ pub struct ResolvedTheme {
|
||||
pub mono_font: &'static str,
|
||||
pub sizes: SizeTable,
|
||||
pub accessibility: crate::system::AccessibilityPreferences,
|
||||
/// The material a window should open with, after accessibility.
|
||||
pub window_background: WindowBackgroundAppearance,
|
||||
}
|
||||
|
||||
/// Overlay order (later wins): neutral base → system → preset definition.
|
||||
@@ -187,6 +192,15 @@ pub(crate) fn resolve(
|
||||
),
|
||||
};
|
||||
|
||||
// Reduced transparency forces the window opaque.
|
||||
let window_background = if system.accessibility.reduce_transparency {
|
||||
WindowBackgroundAppearance::Opaque
|
||||
} else {
|
||||
system
|
||||
.window_background
|
||||
.unwrap_or(WindowBackgroundAppearance::Opaque)
|
||||
};
|
||||
|
||||
let accessibility = system.accessibility;
|
||||
if accessibility.high_contrast {
|
||||
colors.border = mix(
|
||||
@@ -255,25 +269,35 @@ pub(crate) fn resolve(
|
||||
radius.sm = sm;
|
||||
}
|
||||
|
||||
let ramps = Ramps {
|
||||
primary: derive_shades(primary, colors.background, variant == ThemeVariant::Dark),
|
||||
secondary: derive_shades(
|
||||
colors.secondary,
|
||||
colors.background,
|
||||
variant == ThemeVariant::Dark,
|
||||
),
|
||||
destructive: derive_shades(
|
||||
destructive,
|
||||
colors.background,
|
||||
variant == ThemeVariant::Dark,
|
||||
),
|
||||
success: derive_shades(success, colors.background, variant == ThemeVariant::Dark),
|
||||
warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark),
|
||||
info: derive_shades(info, colors.background, variant == ThemeVariant::Dark),
|
||||
};
|
||||
|
||||
let material = MaterialTokens::derive(
|
||||
&colors,
|
||||
ramps.primary.base,
|
||||
variant,
|
||||
window_background != WindowBackgroundAppearance::Opaque,
|
||||
);
|
||||
|
||||
ResolvedTheme {
|
||||
variant,
|
||||
ramps: Ramps {
|
||||
primary: derive_shades(primary, colors.background, variant == ThemeVariant::Dark),
|
||||
secondary: derive_shades(
|
||||
colors.secondary,
|
||||
colors.background,
|
||||
variant == ThemeVariant::Dark,
|
||||
),
|
||||
destructive: derive_shades(
|
||||
destructive,
|
||||
colors.background,
|
||||
variant == ThemeVariant::Dark,
|
||||
),
|
||||
success: derive_shades(success, colors.background, variant == ThemeVariant::Dark),
|
||||
warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark),
|
||||
info: derive_shades(info, colors.background, variant == ThemeVariant::Dark),
|
||||
},
|
||||
ramps,
|
||||
colors,
|
||||
material,
|
||||
radius,
|
||||
borders: Borders::for_platform(system.source, variant),
|
||||
spacing: Spacing {
|
||||
@@ -290,6 +314,7 @@ pub(crate) fn resolve(
|
||||
mono_font: mono_font(),
|
||||
sizes,
|
||||
accessibility,
|
||||
window_background,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +333,7 @@ fn mono_font() -> &'static str {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind};
|
||||
use gpui::{Rgba, rgb};
|
||||
use gpui::{Rgba, WindowBackgroundAppearance, rgb};
|
||||
|
||||
fn hsla(hex: u32) -> gpui::Hsla {
|
||||
gpui::Hsla::from(rgb(hex))
|
||||
@@ -335,6 +360,7 @@ mod tests {
|
||||
titlebar_height: None,
|
||||
root_font_size: None,
|
||||
accessibility: AccessibilityPreferences::default(),
|
||||
window_background: None,
|
||||
source: SourceKind::Macos,
|
||||
}
|
||||
}
|
||||
@@ -537,6 +563,50 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_platforms_window_material_resolves_into_the_theme() {
|
||||
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
||||
stated.window_background = Some(WindowBackgroundAppearance::MicaBackdrop);
|
||||
let resolved = resolve(&stated, None, None);
|
||||
|
||||
assert_eq!(
|
||||
resolved.window_background,
|
||||
WindowBackgroundAppearance::MicaBackdrop
|
||||
);
|
||||
// Material surfaces stay translucent over the stated backdrop.
|
||||
assert!(resolved.material.background.a < resolved.colors.background.a);
|
||||
assert!(resolved.material.selection.a < 1.0);
|
||||
|
||||
let unstated = resolve(
|
||||
&system(ThemeVariant::Light, PartialPalette::default(), None),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
unstated.window_background,
|
||||
WindowBackgroundAppearance::Opaque
|
||||
);
|
||||
assert_eq!(unstated.material.background, unstated.colors.background);
|
||||
assert_eq!(unstated.material.hover, unstated.colors.muted);
|
||||
assert_eq!(unstated.material.selection, unstated.ramps.primary.base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reduced_transparency_forces_an_opaque_window_and_material() {
|
||||
let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None);
|
||||
stated.window_background = Some(WindowBackgroundAppearance::Blurred);
|
||||
stated.accessibility.reduce_transparency = true;
|
||||
let resolved = resolve(&stated, None, None);
|
||||
|
||||
assert_eq!(
|
||||
resolved.window_background,
|
||||
WindowBackgroundAppearance::Opaque
|
||||
);
|
||||
assert_eq!(resolved.material.background, resolved.colors.background);
|
||||
assert_eq!(resolved.material.hover, resolved.colors.muted);
|
||||
assert_eq!(resolved.material.selection, resolved.ramps.primary.base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlay_is_translucent_and_overridable() {
|
||||
let light = resolve(
|
||||
|
||||
@@ -93,6 +93,7 @@ impl KdeScheme {
|
||||
titlebar_height: Some(px(44.)),
|
||||
root_font_size: Some(self.root_font_size.unwrap_or(px(13.33))),
|
||||
accessibility,
|
||||
window_background: None,
|
||||
source: SourceKind::Kde,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,12 @@ fn home_dir() -> Option<PathBuf> {
|
||||
std::env::var_os("HOME").map(PathBuf::from)
|
||||
}
|
||||
|
||||
/// Whether the session runs on Wayland, where compositors state blur protocols.
|
||||
fn wayland_session() -> bool {
|
||||
std::env::var_os("WAYLAND_DISPLAY").is_some()
|
||||
|| std::env::var("XDG_SESSION_TYPE").is_ok_and(|session| session == "wayland")
|
||||
}
|
||||
|
||||
fn detect(home: &Path, portal_reachable: bool, gsettings_reachable: bool) -> Backend {
|
||||
let omarchy_marker = home
|
||||
.join(".local/state/omarchy/current/theme.name")
|
||||
@@ -164,6 +170,7 @@ impl LinuxSource {
|
||||
.unwrap_or(gpui::px(14.67)),
|
||||
),
|
||||
accessibility,
|
||||
window_background: None,
|
||||
source: SourceKind::Gnome,
|
||||
}
|
||||
}
|
||||
@@ -203,15 +210,24 @@ impl ThemeSource for LinuxSource {
|
||||
|
||||
match self.backend {
|
||||
Backend::Omarchy => omarchy::io::read_current()
|
||||
.map(|theme| theme.into_system(accessibility))
|
||||
.map(|theme| {
|
||||
let mut system = theme.into_system(accessibility);
|
||||
// Hyprland blurs transparent surfaces on its own.
|
||||
system.window_background = Some(gpui::WindowBackgroundAppearance::Blurred);
|
||||
system
|
||||
})
|
||||
.unwrap_or_else(|| SystemTheme::fallback(variant)),
|
||||
Backend::Kde => kde::io::read_current()
|
||||
.map(|scheme| {
|
||||
scheme.into_system(
|
||||
let mut system = scheme.into_system(
|
||||
accessibility,
|
||||
portal_appearance.and_then(|appearance| appearance.accent),
|
||||
portal_appearance.map(|appearance| appearance.variant(variant)),
|
||||
)
|
||||
);
|
||||
// KWin states the blur protocols; X11 compositors do not.
|
||||
system.window_background =
|
||||
wayland_session().then_some(gpui::WindowBackgroundAppearance::Blurred);
|
||||
system
|
||||
})
|
||||
.unwrap_or_else(|| SystemTheme::fallback(variant)),
|
||||
Backend::Portal => self.portal_snapshot(variant, portal_appearance, accessibility),
|
||||
|
||||
@@ -121,6 +121,8 @@ impl ThemeSource for MacosSource {
|
||||
variant,
|
||||
accent: Self::color_to_hsla(&NSColor::controlAccentColor()),
|
||||
palette,
|
||||
// macOS blurs what is behind the window natively.
|
||||
window_background: Some(gpui::WindowBackgroundAppearance::Blurred),
|
||||
control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)),
|
||||
overlay_radius: None,
|
||||
sm_radius: None,
|
||||
|
||||
@@ -72,6 +72,7 @@ impl OmarchyTheme {
|
||||
titlebar_height: None,
|
||||
root_font_size: None,
|
||||
accessibility,
|
||||
window_background: None,
|
||||
source: SourceKind::Omarchy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,12 @@ const TITLEBAR_HEIGHT_PX: f32 = 32.0;
|
||||
/// WinUI 11's default body text size, in pixels at 100% scaling.
|
||||
const ROOT_FONT_SIZE_PX: f32 = 14.0;
|
||||
|
||||
/// The `Personalize` registry values, read once at startup as the fallback
|
||||
/// when the WinRT surface is unavailable.
|
||||
/// The `Personalize` registry values and the OS build number.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct RegistrySettings {
|
||||
apps_use_light_theme: Option<bool>,
|
||||
enable_transparency: Option<bool>,
|
||||
current_build: Option<u32>,
|
||||
}
|
||||
|
||||
impl RegistrySettings {
|
||||
@@ -45,6 +45,9 @@ impl RegistrySettings {
|
||||
windows_registry::CURRENT_USER
|
||||
.open(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")
|
||||
};
|
||||
let current_version = || {
|
||||
windows_registry::LOCAL_MACHINE.open(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion")
|
||||
};
|
||||
Self {
|
||||
apps_use_light_theme: personalize()
|
||||
.and_then(|key| key.get_u32("AppsUseLightTheme"))
|
||||
@@ -54,6 +57,10 @@ impl RegistrySettings {
|
||||
.and_then(|key| key.get_u32("EnableTransparency"))
|
||||
.ok()
|
||||
.map(|value| value != 0),
|
||||
current_build: current_version()
|
||||
.and_then(|key| key.get_string("CurrentBuildNumber"))
|
||||
.ok()
|
||||
.and_then(|build| build.parse::<u32>().ok()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,6 +137,17 @@ impl WindowsSource {
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// The native backdrop: Mica on Windows 11, acrylic on Windows 10 1809+.
|
||||
fn window_material(&self) -> Option<gpui::WindowBackgroundAppearance> {
|
||||
match self.registry.current_build {
|
||||
// 22621: Windows 11 22H2, where the DWM backdrop types exist.
|
||||
Some(build) if build >= 22621 => Some(gpui::WindowBackgroundAppearance::MicaBackdrop),
|
||||
// 17763: Windows 10 1809, where acrylic blur-behind exists.
|
||||
Some(build) if build >= 17763 => Some(gpui::WindowBackgroundAppearance::Blurred),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThemeSource for WindowsSource {
|
||||
@@ -180,6 +198,7 @@ impl ThemeSource for WindowsSource {
|
||||
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)),
|
||||
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PX * text_scale as f32)),
|
||||
accessibility: self.accessibility(),
|
||||
window_background: self.window_material(),
|
||||
source: SourceKind::Windows,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The normalized snapshot a platform adapter produces.
|
||||
|
||||
use gpui::{Hsla, Pixels, px};
|
||||
use gpui::{Hsla, Pixels, WindowBackgroundAppearance, px};
|
||||
|
||||
use crate::ThemeVariant;
|
||||
|
||||
@@ -115,6 +115,8 @@ pub struct SystemTheme {
|
||||
/// The platform's native root font size, if it has one.
|
||||
pub root_font_size: Option<Pixels>,
|
||||
pub accessibility: AccessibilityPreferences,
|
||||
/// The window material the platform natively applies, if any.
|
||||
pub window_background: Option<WindowBackgroundAppearance>,
|
||||
pub source: SourceKind,
|
||||
}
|
||||
|
||||
@@ -136,6 +138,7 @@ impl SystemTheme {
|
||||
titlebar_height: None,
|
||||
root_font_size: None,
|
||||
accessibility: AccessibilityPreferences::default(),
|
||||
window_background: None,
|
||||
source: SourceKind::Fallback,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user