203 lines
6.9 KiB
Rust
203 lines
6.9 KiB
Rust
//! Windows 11 adapter: WinRT `UISettings` semantic colors with a
|
|
//! `Personalize` registry fallback, watched through `ColorValuesChanged`.
|
|
|
|
use windows::Foundation::TypedEventHandler;
|
|
use windows::UI::ViewManagement::{AccessibilitySettings, UIColorType, UISettings};
|
|
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;
|
|
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
|
|
use windows::core::IInspectable;
|
|
|
|
use super::ThemeSource;
|
|
use super::signal::ReapplySignal;
|
|
use crate::ThemeVariant;
|
|
use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme};
|
|
|
|
/// WinUI 11's `ControlCornerRadius`, in pixels at 100% scaling.
|
|
const CONTROL_RADIUS_PX: f32 = 4.0;
|
|
/// WinUI 11's default text-box height, in pixels at 100% scaling.
|
|
const INPUT_HEIGHT_PX: f32 = 32.0;
|
|
/// The window caption's height, in pixels at 100% scaling, as Win32 reports it.
|
|
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.
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct RegistrySettings {
|
|
apps_use_light_theme: Option<bool>,
|
|
enable_transparency: Option<bool>,
|
|
}
|
|
|
|
impl RegistrySettings {
|
|
fn read() -> Self {
|
|
let personalize = || {
|
|
windows_registry::CURRENT_USER
|
|
.open(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")
|
|
};
|
|
Self {
|
|
apps_use_light_theme: personalize()
|
|
.and_then(|key| key.get_u32("AppsUseLightTheme"))
|
|
.ok()
|
|
.map(|value| value != 0),
|
|
enable_transparency: personalize()
|
|
.and_then(|key| key.get_u32("EnableTransparency"))
|
|
.ok()
|
|
.map(|value| value != 0),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct WindowsSource {
|
|
ui_settings: UISettings,
|
|
accessibility_settings: AccessibilitySettings,
|
|
registry: RegistrySettings,
|
|
/// Keeps the main-thread channel loop alive.
|
|
_signal: Option<ReapplySignal>,
|
|
}
|
|
|
|
pub(crate) fn platform_source() -> Box<dyn ThemeSource> {
|
|
let initialized = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
|
|
|
|
if initialized.is_err() && initialized != RPC_E_CHANGED_MODE {
|
|
eprintln!("theme: COM initialization failed: {initialized:?}");
|
|
}
|
|
|
|
match WindowsSource::new() {
|
|
Some(source) => Box::new(source),
|
|
None => {
|
|
eprintln!("theme: Windows system theme unavailable; using fallback");
|
|
Box::new(super::fallback::FallbackSource)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WindowsSource {
|
|
fn new() -> Option<Self> {
|
|
let ui_settings = match UISettings::new() {
|
|
Ok(ui_settings) => ui_settings,
|
|
Err(error) => {
|
|
eprintln!("theme: UISettings unavailable: {error}");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let accessibility_settings = match AccessibilitySettings::new() {
|
|
Ok(accessibility_settings) => accessibility_settings,
|
|
Err(error) => {
|
|
eprintln!("theme: AccessibilitySettings unavailable: {error}");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
Some(Self {
|
|
ui_settings,
|
|
accessibility_settings,
|
|
registry: RegistrySettings::read(),
|
|
_signal: None,
|
|
})
|
|
}
|
|
|
|
fn color(&self, kind: UIColorType) -> Option<gpui::Hsla> {
|
|
let color = self.ui_settings.GetColorValue(kind).ok()?;
|
|
Some(gpui::Hsla::from(gpui::Rgba {
|
|
r: color.R as f32 / 255.0,
|
|
g: color.G as f32 / 255.0,
|
|
b: color.B as f32 / 255.0,
|
|
a: color.A as f32 / 255.0,
|
|
}))
|
|
}
|
|
|
|
fn accessibility(&self) -> AccessibilityPreferences {
|
|
AccessibilityPreferences {
|
|
high_contrast: self.accessibility_settings.HighContrast().unwrap_or(false),
|
|
// Windows states animations separately in Win32; not read yet.
|
|
reduce_motion: false,
|
|
reduce_transparency: self
|
|
.registry
|
|
.enable_transparency
|
|
.map(|enabled| !enabled)
|
|
.unwrap_or(false),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ThemeSource for WindowsSource {
|
|
fn snapshot(&self, variant: ThemeVariant) -> SystemTheme {
|
|
let accent = self.color(UIColorType::Accent);
|
|
let background = self.color(UIColorType::Background);
|
|
let foreground = self.color(UIColorType::Foreground);
|
|
|
|
let variant = background
|
|
.map(|background| {
|
|
if background.l >= 0.5 {
|
|
ThemeVariant::Light
|
|
} else {
|
|
ThemeVariant::Dark
|
|
}
|
|
})
|
|
.or(self.registry.apps_use_light_theme.map(|light| {
|
|
if light {
|
|
ThemeVariant::Light
|
|
} else {
|
|
ThemeVariant::Dark
|
|
}
|
|
}))
|
|
.unwrap_or(variant);
|
|
|
|
let text_scale = self.ui_settings.TextScaleFactor().unwrap_or(1.0);
|
|
|
|
SystemTheme {
|
|
variant,
|
|
accent,
|
|
// Windows states its ramp through UIColorType's accent variants.
|
|
palette: PartialPalette {
|
|
background,
|
|
foreground,
|
|
// Windows paints keyboard focus with the accent itself.
|
|
ring: accent,
|
|
..PartialPalette::default()
|
|
},
|
|
control_radius: Some(gpui::px(CONTROL_RADIUS_PX)),
|
|
input_height: Some(gpui::px(INPUT_HEIGHT_PX)),
|
|
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(),
|
|
source: SourceKind::Windows,
|
|
}
|
|
}
|
|
|
|
fn install_watchers(&mut self, async_cx: gpui::AsyncApp) {
|
|
let signal = ReapplySignal::install(&async_cx);
|
|
let color_signal = signal.sender();
|
|
let color_handler = TypedEventHandler::<UISettings, IInspectable>::new(move |_, _| {
|
|
ReapplySignal::poke(&color_signal);
|
|
Ok(())
|
|
});
|
|
|
|
if let Err(error) = self.ui_settings.ColorValuesChanged(&color_handler) {
|
|
eprintln!("theme: color change watch failed: {error}");
|
|
}
|
|
|
|
let contrast_signal = signal.sender();
|
|
let contrast_handler =
|
|
TypedEventHandler::<AccessibilitySettings, IInspectable>::new(move |_, _| {
|
|
ReapplySignal::poke(&contrast_signal);
|
|
Ok(())
|
|
});
|
|
|
|
if let Err(error) = self
|
|
.accessibility_settings
|
|
.HighContrastChanged(&contrast_handler)
|
|
{
|
|
eprintln!("theme: high contrast watch failed: {error}");
|
|
}
|
|
|
|
self._signal = Some(signal);
|
|
}
|
|
|
|
fn label(&self) -> &'static str {
|
|
SourceKind::Windows.label()
|
|
}
|
|
}
|