feat: add native material backgrounds

This commit is contained in:
2026-11-09 09:52:29 +07:00
parent 8c438ed8a6
commit 7784e8d570
14 changed files with 279 additions and 36 deletions
+30 -1
View File
@@ -96,6 +96,7 @@ impl SidebarEntry {
pub struct Sidebar { pub struct Sidebar {
id: ElementId, id: ElementId,
selected: Option<usize>, selected: Option<usize>,
transparent: bool,
a11y_label: Option<SharedString>, a11y_label: Option<SharedString>,
on_change: Option<ChangeHandler>, on_change: Option<ChangeHandler>,
entries: Option<EntriesFn>, entries: Option<EntriesFn>,
@@ -110,6 +111,7 @@ impl Sidebar {
Self { Self {
id: id.into(), id: id.into(),
selected: None, selected: None,
transparent: false,
a11y_label: None, a11y_label: None,
on_change: None, on_change: None,
entries: None, entries: None,
@@ -122,6 +124,17 @@ impl Sidebar {
self self
} }
/// Paints translucent material fills so the window's native backdrop shows through.
pub fn transparent(mut self, transparent: bool) -> Self {
self.transparent = transparent;
self
}
/// Whether translucent material fills are painted.
pub fn is_transparent(&self) -> bool {
self.transparent
}
/// Reports the next selected row, from clicks and keys alike. /// Reports the next selected row, from clicks and keys alike.
pub fn on_change<F>(mut self, handler: F) -> Self pub fn on_change<F>(mut self, handler: F) -> Self
where where
@@ -175,6 +188,7 @@ impl RenderOnce for Sidebar {
.clone() .clone()
}); });
let theme = cx.theme(); let theme = cx.theme();
let material = self.transparent;
let label = self let label = self
.a11y_label .a11y_label
@@ -195,6 +209,7 @@ impl RenderOnce for Sidebar {
index, index,
entries.len(), entries.len(),
self.selected == Some(index), self.selected == Some(index),
material,
&self.on_change, &self.on_change,
&focus, &focus,
) )
@@ -207,6 +222,7 @@ impl RenderOnce for Sidebar {
.size_full() .size_full()
.flex() .flex()
.flex_col() .flex_col()
.when(material, |sidebar| sidebar.bg(theme.material.background))
.text_size(theme.sizes.medium.text_size) .text_size(theme.sizes.medium.text_size)
.track_focus(&focus) .track_focus(&focus)
.focusable() .focusable()
@@ -237,16 +253,19 @@ impl RenderOnce for Sidebar {
} }
} }
#[allow(clippy::too_many_arguments)]
fn render_entry( fn render_entry(
cx: &App, cx: &App,
entry: &SidebarEntry, entry: &SidebarEntry,
index: usize, index: usize,
count: usize, count: usize,
selected: bool, selected: bool,
material: bool,
on_change: &Option<ChangeHandler>, on_change: &Option<ChangeHandler>,
focus: &FocusHandle, focus: &FocusHandle,
) -> AnyElement { ) -> AnyElement {
let theme = cx.theme(); let theme = cx.theme();
match entry { match entry {
SidebarEntry::Header { label } => div() SidebarEntry::Header { label } => div()
.px_2() .px_2()
@@ -277,7 +296,17 @@ fn render_entry(
.aria_selected(selected) .aria_selected(selected)
.aria_position_in_set(index + 1) .aria_position_in_set(index + 1)
.aria_size_of_set(count) .aria_size_of_set(count)
.map(|row| paint_selectable_row(cx, row, selected, *row_disabled)); .map(|row| {
if material && !*row_disabled {
if selected {
row.bg(theme.material.selection)
} else {
row.hover(|row| row.bg(theme.material.hover))
}
} else {
paint_selectable_row(cx, row, selected, *row_disabled)
}
});
if !*row_disabled { if !*row_disabled {
row = row.cursor_pointer().on_click({ row = row.cursor_pointer().on_click({
+4 -2
View File
@@ -1078,8 +1078,10 @@ fn render_pane(
.w_full() .w_full()
.h(titlebar.height) .h(titlebar.height)
.items_center() .items_center()
.when(titlebar.traffic_light_pad, |bar| { .pl(if titlebar.traffic_light_pad {
bar.pl(px(TRAFFIC_LIGHT_PAD)) px(TRAFFIC_LIGHT_PAD)
} else {
padding_x
}) })
.pr(padding_x) .pr(padding_x)
.child(tabs) .child(tabs)
+28 -4
View File
@@ -178,9 +178,11 @@ impl RenderOnce for Shell {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme(); let theme = cx.theme();
let metrics = theme.sizes.resolve(Size::default()); let metrics = theme.sizes.resolve(Size::default());
let background = theme.colors.background;
let titlebar_height = theme.titlebar_height;
let state = self.state;
let id = self.id.clone(); let id = self.id.clone();
let state = self.state;
let mut left = self.left_sidebar; let mut left = self.left_sidebar;
let mut right = self.right_sidebar; let mut right = self.right_sidebar;
@@ -188,6 +190,9 @@ impl RenderOnce for Shell {
let left_visible = state.read(cx).is_left_sidebar_visible(); let left_visible = state.read(cx).is_left_sidebar_visible();
let right_visible = state.read(cx).is_right_sidebar_visible(); let right_visible = state.read(cx).is_right_sidebar_visible();
let left_material = left
.as_ref()
.is_some_and(|sidebar| sidebar.is_transparent());
let layout = div() let layout = div()
.flex() .flex()
@@ -204,8 +209,9 @@ impl RenderOnce for Shell {
.map(|sidebar| sidebar.into_any_element()) .map(|sidebar| sidebar.into_any_element())
.unwrap_or_else(|| div().into_any_element()), .unwrap_or_else(|| div().into_any_element()),
Side::Left, Side::Left,
left_material,
&state, &state,
metrics.strip_height, titlebar_height,
metrics.dense_padding_x, metrics.dense_padding_x,
window, window,
cx, cx,
@@ -223,6 +229,7 @@ impl RenderOnce for Shell {
.h_full() .h_full()
.min_w_0() .min_w_0()
.overflow_hidden() .overflow_hidden()
.bg(background)
.child(center.take().unwrap_or_else(|| div().into_any_element())), .child(center.take().unwrap_or_else(|| div().into_any_element())),
) )
.when(right_visible && right.is_some(), |layout| { .when(right_visible && right.is_some(), |layout| {
@@ -238,8 +245,9 @@ impl RenderOnce for Shell {
width, width,
right.take().unwrap_or_else(|| div().into_any_element()), right.take().unwrap_or_else(|| div().into_any_element()),
Side::Right, Side::Right,
false,
&state, &state,
metrics.strip_height, titlebar_height,
metrics.dense_padding_x, metrics.dense_padding_x,
window, window,
cx, cx,
@@ -257,17 +265,26 @@ fn sidebar_region(
width: Pixels, width: Pixels,
content: AnyElement, content: AnyElement,
side: Side, side: Side,
material: bool,
state: &Entity<LayoutState>, state: &Entity<LayoutState>,
strip_height: Pixels, strip_height: Pixels,
padding_x: Pixels, padding_x: Pixels,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> AnyElement { ) -> AnyElement {
let theme = cx.theme();
let background = theme.colors.background;
let strip_background = match (side, material) {
(Side::Left, true) => theme.material.background,
_ => theme.colors.background,
};
let strip = h_flex() let strip = h_flex()
.id(SharedString::from(format!("sidebar-strip-{}", side.name()))) .id(SharedString::from(format!("sidebar-strip-{}", side.name())))
.debug_selector(move || format!("sidebar-strip-{}", side.name())) .debug_selector(move || format!("sidebar-strip-{}", side.name()))
.flex_none() .flex_none()
.h(strip_height) .h(strip_height)
.bg(strip_background)
.items_center() .items_center()
.justify_end() .justify_end()
.map(|strip| titlebar_strip(strip, window, cx)) .map(|strip| titlebar_strip(strip, window, cx))
@@ -305,6 +322,13 @@ fn sidebar_region(
.h_full() .h_full()
.overflow_hidden() .overflow_hidden()
.child(strip) .child(strip)
.child(div().flex_1().min_h_0().overflow_hidden().child(content)) .child(
div()
.flex_1()
.min_h_0()
.overflow_hidden()
.when(!material, |content| content.bg(background))
.child(content),
)
.into_any_element() .into_any_element()
} }
+2 -1
View File
@@ -146,12 +146,13 @@ fn the_header_toggle_hides_its_sidebar() {
// icon-only small button: derive its center from the theme metrics. // icon-only small button: derive its center from the theme metrics.
let metrics = cx.update(|cx| cx.theme().sizes.resolve(Size::default())); let metrics = cx.update(|cx| cx.theme().sizes.resolve(Size::default()));
let small = cx.update(|cx| cx.theme().sizes.resolve(Size::Small)); let small = cx.update(|cx| cx.theme().sizes.resolve(Size::Small));
let strip_height = cx.update(|cx| cx.theme().titlebar_height);
let region = bounds(&mut visual, "sidebar-test-left").expect("left sidebar missing"); let region = bounds(&mut visual, "sidebar-test-left").expect("left sidebar missing");
// The Sidebar fills the content area below the strip; the toggle sits at // The Sidebar fills the content area below the strip; the toggle sits at
// the strip's trailing end, one strip height above the sidebar's top. // the strip's trailing end, one strip height above the sidebar's top.
let toggle = point( let toggle = point(
region.right() - metrics.dense_padding_x - small.control_height / 2.0, region.right() - metrics.dense_padding_x - small.control_height / 2.0,
region.top() - metrics.strip_height / 2.0, region.top() - strip_height / 2.0,
); );
press(&mut visual, toggle, 1); press(&mut visual, toggle, 1);
redraw(&mut visual); redraw(&mut visual);
+43
View File
@@ -3,6 +3,8 @@
use gpui::{Hsla, hsla, rgb}; use gpui::{Hsla, hsla, rgb};
use crate::ThemeVariant;
fn hex(hex: u32) -> Hsla { fn hex(hex: u32) -> Hsla {
rgb(hex).into() 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),
}
}
}
+8 -2
View File
@@ -14,13 +14,13 @@ mod resolve;
mod source; mod source;
mod system; mod system;
pub use colors::ColorTokens; pub use colors::{ColorTokens, MaterialTokens};
pub use definition::ThemeDefinition; pub use definition::ThemeDefinition;
pub use ramps::{AccentShades, Oklch}; pub use ramps::{AccentShades, Oklch};
pub use resolve::{Ramps, ResolvedTheme}; pub use resolve::{Ramps, ResolvedTheme};
pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme}; 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; use std::time::Duration;
/// The hairline width of the control border every button and field paints; /// The hairline width of the control border every button and field paints;
@@ -50,6 +50,8 @@ impl ThemeVariant {
pub struct Theme { pub struct Theme {
pub variant: ThemeVariant, pub variant: ThemeVariant,
pub colors: ColorTokens, pub colors: ColorTokens,
/// The translucent paints over the window's native material.
pub material: MaterialTokens,
pub ramps: Ramps, pub ramps: Ramps,
pub radius: Radius, pub radius: Radius,
/// Per-platform border and focus chrome. /// Per-platform border and focus chrome.
@@ -68,6 +70,8 @@ pub struct Theme {
pub mono_font: &'static str, pub mono_font: &'static str,
pub sizes: SizeTable, pub sizes: SizeTable,
pub accessibility: AccessibilityPreferences, pub accessibility: AccessibilityPreferences,
/// The material a window should open with, after accessibility.
pub window_background: WindowBackgroundAppearance,
} }
impl Global for Theme {} impl Global for Theme {}
@@ -524,6 +528,7 @@ impl Theme {
Self { Self {
variant: resolved.variant, variant: resolved.variant,
colors: resolved.colors, colors: resolved.colors,
material: resolved.material,
ramps: resolved.ramps, ramps: resolved.ramps,
radius: resolved.radius, radius: resolved.radius,
borders: resolved.borders, borders: resolved.borders,
@@ -536,6 +541,7 @@ impl Theme {
mono_font: resolved.mono_font, mono_font: resolved.mono_font,
sizes: resolved.sizes, sizes: resolved.sizes,
accessibility: resolved.accessibility, accessibility: resolved.accessibility,
window_background: resolved.window_background,
} }
} }
+77 -7
View File
@@ -2,9 +2,10 @@ use crate::definition::ThemeDefinition;
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix}; use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
use crate::system::SystemTheme; use crate::system::SystemTheme;
use crate::{ 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 /// 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. /// macOS and Linux titlebars draw at regardless of the control scale.
@@ -26,6 +27,8 @@ pub struct Ramps {
pub struct ResolvedTheme { pub struct ResolvedTheme {
pub variant: ThemeVariant, pub variant: ThemeVariant,
pub colors: ColorTokens, pub colors: ColorTokens,
/// The translucent paints over the window's native material.
pub material: MaterialTokens,
pub ramps: Ramps, pub ramps: Ramps,
pub radius: Radius, pub radius: Radius,
pub borders: Borders, pub borders: Borders,
@@ -41,6 +44,8 @@ pub struct ResolvedTheme {
pub mono_font: &'static str, pub mono_font: &'static str,
pub sizes: SizeTable, pub sizes: SizeTable,
pub accessibility: crate::system::AccessibilityPreferences, 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. /// 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; let accessibility = system.accessibility;
if accessibility.high_contrast { if accessibility.high_contrast {
colors.border = mix( colors.border = mix(
@@ -255,9 +269,7 @@ pub(crate) fn resolve(
radius.sm = sm; radius.sm = sm;
} }
ResolvedTheme { let ramps = Ramps {
variant,
ramps: Ramps {
primary: derive_shades(primary, colors.background, variant == ThemeVariant::Dark), primary: derive_shades(primary, colors.background, variant == ThemeVariant::Dark),
secondary: derive_shades( secondary: derive_shades(
colors.secondary, colors.secondary,
@@ -272,8 +284,20 @@ pub(crate) fn resolve(
success: derive_shades(success, colors.background, variant == ThemeVariant::Dark), success: derive_shades(success, colors.background, variant == ThemeVariant::Dark),
warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark), warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark),
info: derive_shades(info, 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,
colors, colors,
material,
radius, radius,
borders: Borders::for_platform(system.source, variant), borders: Borders::for_platform(system.source, variant),
spacing: Spacing { spacing: Spacing {
@@ -290,6 +314,7 @@ pub(crate) fn resolve(
mono_font: mono_font(), mono_font: mono_font(),
sizes, sizes,
accessibility, accessibility,
window_background,
} }
} }
@@ -308,7 +333,7 @@ fn mono_font() -> &'static str {
mod tests { mod tests {
use super::*; use super::*;
use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind}; use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind};
use gpui::{Rgba, rgb}; use gpui::{Rgba, WindowBackgroundAppearance, rgb};
fn hsla(hex: u32) -> gpui::Hsla { fn hsla(hex: u32) -> gpui::Hsla {
gpui::Hsla::from(rgb(hex)) gpui::Hsla::from(rgb(hex))
@@ -335,6 +360,7 @@ mod tests {
titlebar_height: None, titlebar_height: None,
root_font_size: None, root_font_size: None,
accessibility: AccessibilityPreferences::default(), accessibility: AccessibilityPreferences::default(),
window_background: None,
source: SourceKind::Macos, 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] #[test]
fn overlay_is_translucent_and_overridable() { fn overlay_is_translucent_and_overridable() {
let light = resolve( let light = resolve(
+1
View File
@@ -93,6 +93,7 @@ impl KdeScheme {
titlebar_height: Some(px(44.)), titlebar_height: Some(px(44.)),
root_font_size: Some(self.root_font_size.unwrap_or(px(13.33))), root_font_size: Some(self.root_font_size.unwrap_or(px(13.33))),
accessibility, accessibility,
window_background: None,
source: SourceKind::Kde, source: SourceKind::Kde,
} }
} }
+19 -3
View File
@@ -38,6 +38,12 @@ fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from) 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 { fn detect(home: &Path, portal_reachable: bool, gsettings_reachable: bool) -> Backend {
let omarchy_marker = home let omarchy_marker = home
.join(".local/state/omarchy/current/theme.name") .join(".local/state/omarchy/current/theme.name")
@@ -164,6 +170,7 @@ impl LinuxSource {
.unwrap_or(gpui::px(14.67)), .unwrap_or(gpui::px(14.67)),
), ),
accessibility, accessibility,
window_background: None,
source: SourceKind::Gnome, source: SourceKind::Gnome,
} }
} }
@@ -203,15 +210,24 @@ impl ThemeSource for LinuxSource {
match self.backend { match self.backend {
Backend::Omarchy => omarchy::io::read_current() 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)), .unwrap_or_else(|| SystemTheme::fallback(variant)),
Backend::Kde => kde::io::read_current() Backend::Kde => kde::io::read_current()
.map(|scheme| { .map(|scheme| {
scheme.into_system( let mut system = scheme.into_system(
accessibility, accessibility,
portal_appearance.and_then(|appearance| appearance.accent), portal_appearance.and_then(|appearance| appearance.accent),
portal_appearance.map(|appearance| appearance.variant(variant)), 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)), .unwrap_or_else(|| SystemTheme::fallback(variant)),
Backend::Portal => self.portal_snapshot(variant, portal_appearance, accessibility), Backend::Portal => self.portal_snapshot(variant, portal_appearance, accessibility),
+2
View File
@@ -121,6 +121,8 @@ impl ThemeSource for MacosSource {
variant, variant,
accent: Self::color_to_hsla(&NSColor::controlAccentColor()), accent: Self::color_to_hsla(&NSColor::controlAccentColor()),
palette, palette,
// macOS blurs what is behind the window natively.
window_background: Some(gpui::WindowBackgroundAppearance::Blurred),
control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)), control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)),
overlay_radius: None, overlay_radius: None,
sm_radius: None, sm_radius: None,
+1
View File
@@ -72,6 +72,7 @@ impl OmarchyTheme {
titlebar_height: None, titlebar_height: None,
root_font_size: None, root_font_size: None,
accessibility, accessibility,
window_background: None,
source: SourceKind::Omarchy, source: SourceKind::Omarchy,
} }
} }
+21 -2
View File
@@ -31,12 +31,12 @@ const TITLEBAR_HEIGHT_PX: f32 = 32.0;
/// WinUI 11's default body text size, in pixels at 100% scaling. /// WinUI 11's default body text size, in pixels at 100% scaling.
const ROOT_FONT_SIZE_PX: f32 = 14.0; const ROOT_FONT_SIZE_PX: f32 = 14.0;
/// The `Personalize` registry values, read once at startup as the fallback /// The `Personalize` registry values and the OS build number.
/// when the WinRT surface is unavailable.
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
struct RegistrySettings { struct RegistrySettings {
apps_use_light_theme: Option<bool>, apps_use_light_theme: Option<bool>,
enable_transparency: Option<bool>, enable_transparency: Option<bool>,
current_build: Option<u32>,
} }
impl RegistrySettings { impl RegistrySettings {
@@ -45,6 +45,9 @@ impl RegistrySettings {
windows_registry::CURRENT_USER windows_registry::CURRENT_USER
.open(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize") .open(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")
}; };
let current_version = || {
windows_registry::LOCAL_MACHINE.open(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion")
};
Self { Self {
apps_use_light_theme: personalize() apps_use_light_theme: personalize()
.and_then(|key| key.get_u32("AppsUseLightTheme")) .and_then(|key| key.get_u32("AppsUseLightTheme"))
@@ -54,6 +57,10 @@ impl RegistrySettings {
.and_then(|key| key.get_u32("EnableTransparency")) .and_then(|key| key.get_u32("EnableTransparency"))
.ok() .ok()
.map(|value| value != 0), .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), .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 { impl ThemeSource for WindowsSource {
@@ -180,6 +198,7 @@ impl ThemeSource for WindowsSource {
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)), titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)),
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PX * text_scale as f32)), root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PX * text_scale as f32)),
accessibility: self.accessibility(), accessibility: self.accessibility(),
window_background: self.window_material(),
source: SourceKind::Windows, source: SourceKind::Windows,
} }
} }
+4 -1
View File
@@ -1,6 +1,6 @@
//! The normalized snapshot a platform adapter produces. //! The normalized snapshot a platform adapter produces.
use gpui::{Hsla, Pixels, px}; use gpui::{Hsla, Pixels, WindowBackgroundAppearance, px};
use crate::ThemeVariant; use crate::ThemeVariant;
@@ -115,6 +115,8 @@ pub struct SystemTheme {
/// The platform's native root font size, if it has one. /// The platform's native root font size, if it has one.
pub root_font_size: Option<Pixels>, pub root_font_size: Option<Pixels>,
pub accessibility: AccessibilityPreferences, pub accessibility: AccessibilityPreferences,
/// The window material the platform natively applies, if any.
pub window_background: Option<WindowBackgroundAppearance>,
pub source: SourceKind, pub source: SourceKind,
} }
@@ -136,6 +138,7 @@ impl SystemTheme {
titlebar_height: None, titlebar_height: None,
root_font_size: None, root_font_size: None,
accessibility: AccessibilityPreferences::default(), accessibility: AccessibilityPreferences::default(),
window_background: None,
source: SourceKind::Fallback, source: SourceKind::Fallback,
} }
} }
+27 -1
View File
@@ -223,6 +223,8 @@ struct Gallery {
palette_runs: Vec<SharedString>, palette_runs: Vec<SharedString>,
dialog_open: bool, dialog_open: bool,
alert_open: bool, alert_open: bool,
/// Whether the app sidebar paints the window's native material.
sidebar_transparent: bool,
} }
impl Gallery { impl Gallery {
@@ -254,6 +256,7 @@ impl Gallery {
let left_sidebar = Sidebar::new("gallery-sidebar") let left_sidebar = Sidebar::new("gallery-sidebar")
.selected(selected) .selected(selected)
.transparent(self.sidebar_transparent)
.accessibility_label("Component catalog") .accessibility_label("Component catalog")
.entries(|_, _| { .entries(|_, _| {
CATALOG CATALOG
@@ -404,10 +407,12 @@ impl Gallery {
PanelDescriptor::new("welcome", "Welcome", { PanelDescriptor::new("welcome", "Welcome", {
let layout_state = layout_state.clone(); let layout_state = layout_state.clone();
let dock_state = dock_state.clone(); let dock_state = dock_state.clone();
let gallery = gallery.clone();
move |_, cx| { move |_, cx| {
cx.new(|_| WelcomePanel { cx.new(|_| WelcomePanel {
layout_state: layout_state.clone(), layout_state: layout_state.clone(),
dock_state: dock_state.clone(), dock_state: dock_state.clone(),
gallery: gallery.clone(),
}) })
.into() .into()
} }
@@ -729,7 +734,6 @@ impl Render for Gallery {
palette.update(cx, |palette, cx| palette.toggle(window, cx)); palette.update(cx, |palette, cx| palette.toggle(window, cx));
} }
}) })
.bg(theme.colors.background)
.text_color(theme.colors.foreground) .text_color(theme.colors.foreground)
.child(self.render_shell(cx)) .child(self.render_shell(cx))
.child(dialog) .child(dialog)
@@ -745,6 +749,7 @@ impl Render for Gallery {
struct WelcomePanel { struct WelcomePanel {
layout_state: Entity<LayoutState>, layout_state: Entity<LayoutState>,
dock_state: Entity<DockState>, dock_state: Entity<DockState>,
gallery: WeakEntity<Gallery>,
} }
impl Render for WelcomePanel { impl Render for WelcomePanel {
@@ -753,6 +758,7 @@ impl Render for WelcomePanel {
let layout_left = self.layout_state.clone(); let layout_left = self.layout_state.clone();
let layout_right = self.layout_state.clone(); let layout_right = self.layout_state.clone();
let dock_state = self.dock_state.clone(); let dock_state = self.dock_state.clone();
let gallery = self.gallery.clone();
ScrollArea::new("welcome-demo") ScrollArea::new("welcome-demo")
.size_full() .size_full()
@@ -811,6 +817,21 @@ impl Render for WelcomePanel {
}); });
}), }),
) )
.child(
Button::new("welcome-toggle-blur")
.label("Toggle sidebar blur")
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, _, cx| {
gallery
.update(cx, |gallery, cx| {
gallery.sidebar_transparent =
!gallery.sidebar_transparent;
cx.notify();
})
.ok();
}),
)
.child( .child(
Button::new("welcome-split") Button::new("welcome-split")
.label("Split active right") .label("Split active right")
@@ -3774,6 +3795,7 @@ impl Render for SidebarDemo {
.child( .child(
Sidebar::new("sidebar-demo-list") Sidebar::new("sidebar-demo-list")
.selected(self.selected) .selected(self.selected)
.transparent(true)
.on_change(move |selected, _, cx| { .on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| { view.update(cx, |demo, cx| {
demo.selected = selected; demo.selected = selected;
@@ -4077,6 +4099,9 @@ fn main() {
// The strips move the window themselves, so a tab dragged inside // The strips move the window themselves, so a tab dragged inside
// the titlebar band never drags the window with it. // the titlebar band never drags the window with it.
app_owns_titlebar_drag: true, app_owns_titlebar_drag: true,
// The platform's native backdrop; the theme's material paints
// resolve over it and go opaque under reduced transparency.
window_background: cx.theme().window_background,
..Default::default() ..Default::default()
}, },
|window, cx| { |window, cx| {
@@ -4106,6 +4131,7 @@ fn main() {
palette_runs: Vec::new(), palette_runs: Vec::new(),
dialog_open: false, dialog_open: false,
alert_open: false, alert_open: false,
sidebar_transparent: true,
} }
}); });
let palette = cx.new(|cx| { let palette = cx.new(|cx| {