chore: audit components
This commit is contained in:
@@ -77,7 +77,7 @@ impl KdeScheme {
|
||||
//
|
||||
// Older setups fall back to the scheme's background lightness.
|
||||
variant: portal_variant
|
||||
.or(self.background.map(variant_by_lightness))
|
||||
.or(self.background.map(crate::ThemeVariant::by_lightness))
|
||||
.unwrap_or(crate::ThemeVariant::Light),
|
||||
accent: self.accent.or(portal_accent),
|
||||
palette: self.palette,
|
||||
@@ -99,14 +99,6 @@ impl KdeScheme {
|
||||
}
|
||||
}
|
||||
|
||||
fn variant_by_lightness(background: Hsla) -> crate::ThemeVariant {
|
||||
if background.l >= 0.5 {
|
||||
crate::ThemeVariant::Light
|
||||
} else {
|
||||
crate::ThemeVariant::Dark
|
||||
}
|
||||
}
|
||||
|
||||
/// The root font size of a `[General]` group's `font=` QFont string.
|
||||
fn root_font_size(group: Option<&HashMap<&str, &str>>) -> Option<Pixels> {
|
||||
let fields: Vec<&str> = group?.get("font")?.split(',').map(str::trim).collect();
|
||||
@@ -158,7 +150,10 @@ fn color_list(value: &str) -> Option<Hsla> {
|
||||
let green = components.next()?.parse::<f32>().ok()?;
|
||||
let blue = components.next()?.parse::<f32>().ok()?;
|
||||
let alpha = components.next().and_then(|a| a.parse::<f32>().ok());
|
||||
if red < 0.0 || green < 0.0 || blue < 0.0 {
|
||||
if !(0.0..=255.0).contains(&red)
|
||||
|| !(0.0..=255.0).contains(&green)
|
||||
|| !(0.0..=255.0).contains(&blue)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let scale = |component: f32| component / 255.0;
|
||||
@@ -335,19 +330,18 @@ activeBackground=39,44,49
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scheme_states_breeze_metrics_and_the_default_font() {
|
||||
fn the_default_font_applies_without_a_stated_font() {
|
||||
let system = KdeScheme::parse(BREEZE_DARK).into_system(Default::default(), None, None);
|
||||
assert!((system.root_font_size.expect("default font") - px(13.33)).abs() < px(0.01));
|
||||
}
|
||||
|
||||
assert_eq!(system.control_height, Some(px(33.)));
|
||||
assert_eq!(system.input_height, Some(px(29.)));
|
||||
assert_eq!(system.menu_item_height, Some(px(29.)));
|
||||
assert_eq!(system.control_radius, Some(px(5.)));
|
||||
assert_eq!(system.overlay_radius, Some(px(5.)));
|
||||
assert_eq!(system.switch_height, Some(px(18.)));
|
||||
assert_eq!(system.switch_width, Some(px(36.)));
|
||||
assert_eq!(system.switch_knob, Some(px(18.)));
|
||||
assert_eq!(system.titlebar_height, Some(px(44.)));
|
||||
assert!((system.root_font_size.unwrap() - px(13.33)).abs() < px(0.01));
|
||||
#[test]
|
||||
fn color_components_outside_0_to_255_are_rejected() {
|
||||
let above = KdeScheme::parse("[Colors:Window]\nBackgroundNormal=300,20,20\n");
|
||||
assert!(above.palette.background.is_none());
|
||||
|
||||
let below = KdeScheme::parse("[Colors:Window]\nBackgroundNormal=-5,20,20\n");
|
||||
assert!(below.palette.background.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,9 +13,30 @@ use super::kde;
|
||||
use super::omarchy;
|
||||
use super::portal;
|
||||
use super::signal::ReapplySignal;
|
||||
|
||||
use crate::ThemeVariant;
|
||||
|
||||
use crate::system::{PartialPalette, SourceKind, SystemTheme};
|
||||
|
||||
/// libadwaita's control corner radius, in px at 100% scaling.
|
||||
const CONTROL_RADIUS_PX: f32 = 9.0;
|
||||
/// libadwaita's popover corner radius.
|
||||
const OVERLAY_RADIUS_PX: f32 = 15.0;
|
||||
/// The checkbox indicator's corner radius.
|
||||
const SM_RADIUS_PX: f32 = 6.0;
|
||||
/// libadwaita's standard control and text-field height.
|
||||
const CONTROL_HEIGHT_PX: f32 = 34.0;
|
||||
/// The GTK menu row height.
|
||||
const MENU_ITEM_HEIGHT_PX: f32 = 32.0;
|
||||
/// The GTK switch: a 46x26 track with a 20px knob.
|
||||
const SWITCH_HEIGHT_PX: f32 = 26.0;
|
||||
const SWITCH_WIDTH_PX: f32 = 46.0;
|
||||
const SWITCH_KNOB_PX: f32 = 20.0;
|
||||
/// GNOME's headerbar height, in px at 100% scaling.
|
||||
const TITLEBAR_HEIGHT_PX: f32 = 47.0;
|
||||
/// The default 11pt interface font, in px.
|
||||
const ROOT_FONT_SIZE_PX: f32 = 14.67;
|
||||
|
||||
pub(crate) struct LinuxSource {
|
||||
backend: Backend,
|
||||
portal: Option<portal::io::PortalClient>,
|
||||
@@ -154,20 +175,20 @@ impl LinuxSource {
|
||||
variant,
|
||||
accent,
|
||||
palette: PartialPalette::default(),
|
||||
control_radius: Some(gpui::px(9.)),
|
||||
overlay_radius: Some(gpui::px(15.)),
|
||||
sm_radius: Some(gpui::px(6.)),
|
||||
control_height: Some(gpui::px(34.)),
|
||||
input_height: Some(gpui::px(34.)),
|
||||
menu_item_height: Some(gpui::px(32.)),
|
||||
switch_height: Some(gpui::px(26.)),
|
||||
switch_width: Some(gpui::px(46.)),
|
||||
switch_knob: Some(gpui::px(20.)),
|
||||
titlebar_height: Some(gpui::px(47.)),
|
||||
control_radius: Some(gpui::px(CONTROL_RADIUS_PX)),
|
||||
overlay_radius: Some(gpui::px(OVERLAY_RADIUS_PX)),
|
||||
sm_radius: Some(gpui::px(SM_RADIUS_PX)),
|
||||
control_height: Some(gpui::px(CONTROL_HEIGHT_PX)),
|
||||
input_height: Some(gpui::px(CONTROL_HEIGHT_PX)),
|
||||
menu_item_height: Some(gpui::px(MENU_ITEM_HEIGHT_PX)),
|
||||
switch_height: Some(gpui::px(SWITCH_HEIGHT_PX)),
|
||||
switch_width: Some(gpui::px(SWITCH_WIDTH_PX)),
|
||||
switch_knob: Some(gpui::px(SWITCH_KNOB_PX)),
|
||||
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)),
|
||||
root_font_size: Some(
|
||||
self.gsettings
|
||||
.and_then(|gsettings| gsettings.root_font_size)
|
||||
.unwrap_or(gpui::px(14.67)),
|
||||
.unwrap_or(gpui::px(ROOT_FONT_SIZE_PX)),
|
||||
),
|
||||
accessibility,
|
||||
window_background: None,
|
||||
@@ -237,41 +258,44 @@ impl ThemeSource for LinuxSource {
|
||||
|
||||
fn install_watchers(&mut self, async_cx: gpui::AsyncApp) {
|
||||
let signal = ReapplySignal::install(&async_cx);
|
||||
let file_signal = signal.sender();
|
||||
|
||||
if let Some(portal) = &self.portal {
|
||||
portal.spawn_watch(signal.sender());
|
||||
}
|
||||
|
||||
// Stored before any fallible step: an early return below
|
||||
// must not drop and cancel the reapply loop.
|
||||
self._signal = Some(signal);
|
||||
|
||||
match self.backend {
|
||||
Backend::Omarchy => {
|
||||
let Some(current) = omarchy::io::current_dir() else {
|
||||
return;
|
||||
};
|
||||
let reapply_signal = signal.sender();
|
||||
|
||||
let watcher = notify::recommended_watcher(
|
||||
move |event: std::result::Result<notify::Event, notify::Error>| match event {
|
||||
Ok(_) => ReapplySignal::poke(&reapply_signal),
|
||||
Err(error) => eprintln!("theme: omarchy watch error: {error}"),
|
||||
Ok(_) => ReapplySignal::poke(&file_signal),
|
||||
Err(error) => log::warn!("theme: omarchy watch error: {error}"),
|
||||
},
|
||||
);
|
||||
|
||||
match watcher {
|
||||
Ok(mut watcher) => {
|
||||
if let Err(error) = watcher.watch(¤t, RecursiveMode::NonRecursive) {
|
||||
eprintln!("theme: omarchy watch failed: {error}");
|
||||
log::warn!("theme: omarchy watch failed: {error}");
|
||||
return;
|
||||
}
|
||||
self.file_watcher = Some(watcher);
|
||||
}
|
||||
Err(error) => eprintln!("theme: omarchy watcher unavailable: {error}"),
|
||||
Err(error) => log::warn!("theme: omarchy watcher unavailable: {error}"),
|
||||
}
|
||||
}
|
||||
Backend::Kde => {
|
||||
let Some(config) = home_dir().map(|home| home.join(".config")) else {
|
||||
return;
|
||||
};
|
||||
let reapply_signal = signal.sender();
|
||||
|
||||
let watcher = notify::recommended_watcher(
|
||||
move |event: std::result::Result<notify::Event, notify::Error>| match event {
|
||||
@@ -279,27 +303,25 @@ impl ThemeSource for LinuxSource {
|
||||
if event.paths.iter().any(|path| {
|
||||
path.file_name().is_some_and(|name| name == "kdeglobals")
|
||||
}) {
|
||||
ReapplySignal::poke(&reapply_signal);
|
||||
ReapplySignal::poke(&file_signal);
|
||||
}
|
||||
}
|
||||
Err(error) => eprintln!("theme: kde watch error: {error}"),
|
||||
Err(error) => log::warn!("theme: kde watch error: {error}"),
|
||||
},
|
||||
);
|
||||
match watcher {
|
||||
Ok(mut watcher) => {
|
||||
if let Err(error) = watcher.watch(&config, RecursiveMode::NonRecursive) {
|
||||
eprintln!("theme: kde watch failed: {error}");
|
||||
log::warn!("theme: kde watch failed: {error}");
|
||||
return;
|
||||
}
|
||||
self.file_watcher = Some(watcher);
|
||||
}
|
||||
Err(error) => eprintln!("theme: kde watcher unavailable: {error}"),
|
||||
Err(error) => log::warn!("theme: kde watcher unavailable: {error}"),
|
||||
}
|
||||
}
|
||||
Backend::Portal | Backend::Fallback => {}
|
||||
}
|
||||
|
||||
self._signal = Some(signal);
|
||||
}
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
@@ -354,7 +376,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_gnome_snapshot_states_the_libadwaita_metrics() {
|
||||
fn the_gnome_snapshot_labels_gnome_and_defaults_the_root_font() {
|
||||
let source = LinuxSource {
|
||||
backend: Backend::Portal,
|
||||
portal: None,
|
||||
@@ -369,17 +391,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(system.source, SourceKind::Gnome);
|
||||
assert_eq!(system.control_height, Some(gpui::px(34.)));
|
||||
assert_eq!(system.input_height, Some(gpui::px(34.)));
|
||||
assert_eq!(system.menu_item_height, Some(gpui::px(32.)));
|
||||
assert_eq!(system.control_radius, Some(gpui::px(9.)));
|
||||
assert_eq!(system.overlay_radius, Some(gpui::px(15.)));
|
||||
assert_eq!(system.sm_radius, Some(gpui::px(6.)));
|
||||
assert_eq!(system.switch_height, Some(gpui::px(26.)));
|
||||
assert_eq!(system.switch_width, Some(gpui::px(46.)));
|
||||
assert_eq!(system.switch_knob, Some(gpui::px(20.)));
|
||||
assert_eq!(system.titlebar_height, Some(gpui::px(47.)));
|
||||
// The default 11pt font when gsettings states none.
|
||||
assert_eq!(system.root_font_size, Some(gpui::px(14.67)));
|
||||
assert_eq!(system.root_font_size, Some(gpui::px(ROOT_FONT_SIZE_PX)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ use objc2_foundation::{
|
||||
};
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use super::narrow_component;
|
||||
|
||||
use super::ThemeSource;
|
||||
use crate::ThemeVariant;
|
||||
use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme};
|
||||
@@ -46,10 +48,10 @@ impl MacosSource {
|
||||
let converted = color.colorUsingColorSpace(&srgb)?;
|
||||
|
||||
Some(gpui::Hsla::from(gpui::Rgba {
|
||||
r: converted.redComponent() as f32,
|
||||
g: converted.greenComponent() as f32,
|
||||
b: converted.blueComponent() as f32,
|
||||
a: converted.alphaComponent() as f32,
|
||||
r: narrow_component(converted.redComponent()),
|
||||
g: narrow_component(converted.greenComponent()),
|
||||
b: narrow_component(converted.blueComponent()),
|
||||
a: narrow_component(converted.alphaComponent()),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ impl OmarchyTheme {
|
||||
.get("mode")
|
||||
.and_then(toml::Value::as_str)
|
||||
.and_then(mode_variant)
|
||||
.unwrap_or_else(|| variant_by_lightness(background));
|
||||
.unwrap_or_else(|| ThemeVariant::by_lightness(background));
|
||||
|
||||
let accent = hex_color(&table, "accent").or_else(|| hex_color(&table, "color4"));
|
||||
let selection = hex_color(&table, "selection");
|
||||
@@ -86,14 +86,6 @@ fn mode_variant(mode: &str) -> Option<ThemeVariant> {
|
||||
}
|
||||
}
|
||||
|
||||
fn variant_by_lightness(background: Hsla) -> ThemeVariant {
|
||||
if background.l >= 0.5 {
|
||||
ThemeVariant::Light
|
||||
} else {
|
||||
ThemeVariant::Dark
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_color(table: &Table, key: &str) -> Option<Hsla> {
|
||||
let value = table.get(key)?.as_str()?;
|
||||
let digits = value.trim().strip_prefix('#')?;
|
||||
@@ -224,18 +216,10 @@ color15 = \"#635d6f\"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omarchy_shares_the_gnome_control_metrics() {
|
||||
fn omarchy_states_no_titlebar_or_root_font() {
|
||||
let theme = OmarchyTheme::parse(TOKYO_NIGHT).expect("parses");
|
||||
let system = theme.into_system(Default::default());
|
||||
|
||||
assert_eq!(system.control_height, Some(gpui::px(34.)));
|
||||
assert_eq!(system.input_height, Some(gpui::px(34.)));
|
||||
assert_eq!(system.menu_item_height, Some(gpui::px(32.)));
|
||||
assert_eq!(system.control_radius, Some(gpui::px(9.)));
|
||||
assert_eq!(system.overlay_radius, Some(gpui::px(15.)));
|
||||
assert_eq!(system.switch_height, Some(gpui::px(26.)));
|
||||
assert_eq!(system.switch_width, Some(gpui::px(46.)));
|
||||
assert_eq!(system.switch_knob, Some(gpui::px(20.)));
|
||||
assert_eq!(system.titlebar_height, None);
|
||||
assert_eq!(system.root_font_size, None);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
use gpui::Hsla;
|
||||
|
||||
use super::narrow_component;
|
||||
|
||||
use crate::{ThemeVariant, system::AccessibilityPreferences};
|
||||
|
||||
/// The raw portal values, already converted off the DBus wire types.
|
||||
@@ -26,9 +28,9 @@ impl AppearanceValues {
|
||||
color_scheme: self.color_scheme,
|
||||
accent: self.accent_rgb.map(|(r, g, b)| {
|
||||
Hsla::from(gpui::Rgba {
|
||||
r: r as f32,
|
||||
g: g as f32,
|
||||
b: b as f32,
|
||||
r: narrow_component(r),
|
||||
g: narrow_component(g),
|
||||
b: narrow_component(b),
|
||||
a: 1.0,
|
||||
})
|
||||
}),
|
||||
@@ -98,7 +100,9 @@ pub(crate) mod io {
|
||||
|
||||
/// The most recently read appearance.
|
||||
pub(crate) fn appearance(&self) -> Appearance {
|
||||
*self.appearance.lock().expect("portal cache poisoned")
|
||||
// A watch-thread panic must not crash the UI thread.
|
||||
let cache = self.appearance.lock();
|
||||
*cache.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Forwards portal `SettingChanged` events to the reapply signal from a dedicated thread.
|
||||
@@ -107,17 +111,17 @@ pub(crate) mod io {
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let Some(connection) = Connection::session().ok() else {
|
||||
eprintln!("theme: portal watch skipped, no session bus");
|
||||
log::warn!("theme: portal watch skipped, no session bus");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(proxy) = settings_proxy(connection) else {
|
||||
eprintln!("theme: portal watch skipped, settings proxy unavailable");
|
||||
log::warn!("theme: portal watch skipped, settings proxy unavailable");
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(signals) = proxy.receive_signal("SettingChanged") else {
|
||||
eprintln!("theme: portal watch skipped, signal subscription failed");
|
||||
log::warn!("theme: portal watch skipped, signal subscription failed");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -134,10 +138,13 @@ pub(crate) mod io {
|
||||
|
||||
match read_appearance(&proxy) {
|
||||
Some(latest) => {
|
||||
*appearance.lock().expect("portal cache poisoned") = latest;
|
||||
let mut cache = appearance
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
*cache = latest;
|
||||
super::super::signal::ReapplySignal::poke(&reapply_signal);
|
||||
}
|
||||
None => eprintln!("theme: portal read failed on SettingChanged"),
|
||||
None => log::warn!("theme: portal read failed on SettingChanged"),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ impl ReapplySignal {
|
||||
/// Pokes the pipeline from a thread holding a [`Self::sender`] clone.
|
||||
pub(crate) fn poke(sender: &async_channel::Sender<()>) {
|
||||
if let Err(error) = sender.try_send(()) {
|
||||
eprintln!("theme: reapply signal failed: {error}");
|
||||
log::warn!("theme: reapply signal failed: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,16 +74,19 @@ pub(crate) struct WindowsSource {
|
||||
}
|
||||
|
||||
pub(crate) fn platform_source() -> Box<dyn ThemeSource> {
|
||||
// SAFETY: WinRT activation requires COM on this thread; the STA matches
|
||||
// the UI thread's message pump, and RPC_E_CHANGED_MODE means COM is
|
||||
// already initialized in another apartment.
|
||||
let initialized = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
|
||||
|
||||
if initialized.is_err() && initialized != RPC_E_CHANGED_MODE {
|
||||
eprintln!("theme: COM initialization failed: {initialized:?}");
|
||||
log::error!("theme: COM initialization failed: {initialized:?}");
|
||||
}
|
||||
|
||||
match WindowsSource::new() {
|
||||
Some(source) => Box::new(source),
|
||||
None => {
|
||||
eprintln!("theme: Windows system theme unavailable; using fallback");
|
||||
log::warn!("theme: Windows system theme unavailable; using fallback");
|
||||
Box::new(super::fallback::FallbackSource)
|
||||
}
|
||||
}
|
||||
@@ -94,7 +97,7 @@ impl WindowsSource {
|
||||
let ui_settings = match UISettings::new() {
|
||||
Ok(ui_settings) => ui_settings,
|
||||
Err(error) => {
|
||||
eprintln!("theme: UISettings unavailable: {error}");
|
||||
log::warn!("theme: UISettings unavailable: {error}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -102,7 +105,7 @@ impl WindowsSource {
|
||||
let accessibility_settings = match AccessibilitySettings::new() {
|
||||
Ok(accessibility_settings) => accessibility_settings,
|
||||
Err(error) => {
|
||||
eprintln!("theme: AccessibilitySettings unavailable: {error}");
|
||||
log::warn!("theme: AccessibilitySettings unavailable: {error}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -157,13 +160,7 @@ impl ThemeSource for WindowsSource {
|
||||
let foreground = self.color(UIColorType::Foreground);
|
||||
|
||||
let variant = background
|
||||
.map(|background| {
|
||||
if background.l >= 0.5 {
|
||||
ThemeVariant::Light
|
||||
} else {
|
||||
ThemeVariant::Dark
|
||||
}
|
||||
})
|
||||
.map(ThemeVariant::by_lightness)
|
||||
.or(self.registry.apps_use_light_theme.map(|light| {
|
||||
if light {
|
||||
ThemeVariant::Light
|
||||
@@ -212,7 +209,7 @@ impl ThemeSource for WindowsSource {
|
||||
});
|
||||
|
||||
if let Err(error) = self.ui_settings.ColorValuesChanged(&color_handler) {
|
||||
eprintln!("theme: color change watch failed: {error}");
|
||||
log::warn!("theme: color change watch failed: {error}");
|
||||
}
|
||||
|
||||
let contrast_signal = signal.sender();
|
||||
@@ -226,7 +223,7 @@ impl ThemeSource for WindowsSource {
|
||||
.accessibility_settings
|
||||
.HighContrastChanged(&contrast_handler)
|
||||
{
|
||||
eprintln!("theme: high contrast watch failed: {error}");
|
||||
log::warn!("theme: high contrast watch failed: {error}");
|
||||
}
|
||||
|
||||
self._signal = Some(signal);
|
||||
|
||||
Reference in New Issue
Block a user