chore: audit components
This commit is contained in:
@@ -7,6 +7,7 @@ description = "Native theme engine: reads the OS theme and resolves it into sema
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
gpui = { workspace = true }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
|
||||
|
||||
@@ -9,37 +9,37 @@ use crate::ThemeVariant;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
|
||||
pub struct ThemeDefinition {
|
||||
pub mode: Option<ThemeVariant>,
|
||||
pub control_radius: Option<f32>,
|
||||
pub overlay_radius: Option<f32>,
|
||||
pub sm_radius: Option<f32>,
|
||||
pub accent: Option<String>,
|
||||
pub accent_foreground: Option<String>,
|
||||
pub background: Option<String>,
|
||||
pub foreground: Option<String>,
|
||||
pub surface: Option<String>,
|
||||
pub surface_foreground: Option<String>,
|
||||
pub primary: Option<String>,
|
||||
pub primary_foreground: Option<String>,
|
||||
pub secondary: Option<String>,
|
||||
pub secondary_foreground: Option<String>,
|
||||
pub muted: Option<String>,
|
||||
pub muted_hover: Option<String>,
|
||||
pub muted_foreground: Option<String>,
|
||||
pub destructive: Option<String>,
|
||||
pub destructive_foreground: Option<String>,
|
||||
pub success: Option<String>,
|
||||
pub success_foreground: Option<String>,
|
||||
pub warning: Option<String>,
|
||||
pub warning_foreground: Option<String>,
|
||||
pub info: Option<String>,
|
||||
pub info_foreground: Option<String>,
|
||||
pub border: Option<String>,
|
||||
pub input: Option<String>,
|
||||
pub ring: Option<String>,
|
||||
pub selection: Option<String>,
|
||||
pub overlay: Option<String>,
|
||||
pub scrollbar_thumb: Option<String>,
|
||||
mode: Option<ThemeVariant>,
|
||||
control_radius: Option<f32>,
|
||||
overlay_radius: Option<f32>,
|
||||
sm_radius: Option<f32>,
|
||||
accent: Option<String>,
|
||||
accent_foreground: Option<String>,
|
||||
background: Option<String>,
|
||||
foreground: Option<String>,
|
||||
surface: Option<String>,
|
||||
surface_foreground: Option<String>,
|
||||
primary: Option<String>,
|
||||
primary_foreground: Option<String>,
|
||||
secondary: Option<String>,
|
||||
secondary_foreground: Option<String>,
|
||||
muted: Option<String>,
|
||||
muted_hover: Option<String>,
|
||||
muted_foreground: Option<String>,
|
||||
destructive: Option<String>,
|
||||
destructive_foreground: Option<String>,
|
||||
success: Option<String>,
|
||||
success_foreground: Option<String>,
|
||||
warning: Option<String>,
|
||||
warning_foreground: Option<String>,
|
||||
info: Option<String>,
|
||||
info_foreground: Option<String>,
|
||||
border: Option<String>,
|
||||
input: Option<String>,
|
||||
ring: Option<String>,
|
||||
selection: Option<String>,
|
||||
overlay: Option<String>,
|
||||
scrollbar_thumb: Option<String>,
|
||||
}
|
||||
|
||||
impl ThemeDefinition {
|
||||
@@ -47,112 +47,140 @@ impl ThemeDefinition {
|
||||
Ok(toml::from_str(source)?)
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> Option<ThemeVariant> {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn control_radius(&self) -> Option<f32> {
|
||||
self.control_radius
|
||||
}
|
||||
|
||||
pub fn overlay_radius(&self) -> Option<f32> {
|
||||
self.overlay_radius
|
||||
}
|
||||
|
||||
pub fn sm_radius(&self) -> Option<f32> {
|
||||
self.sm_radius
|
||||
}
|
||||
|
||||
pub fn accent(&self) -> Option<Hsla> {
|
||||
parse_hex(self.accent.as_deref()?)
|
||||
self.color(&self.accent, "accent")
|
||||
}
|
||||
|
||||
pub fn accent_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.accent_foreground.as_deref()?)
|
||||
self.color(&self.accent_foreground, "accent_foreground")
|
||||
}
|
||||
|
||||
pub fn background(&self) -> Option<Hsla> {
|
||||
parse_hex(self.background.as_deref()?)
|
||||
self.color(&self.background, "background")
|
||||
}
|
||||
|
||||
pub fn foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.foreground.as_deref()?)
|
||||
self.color(&self.foreground, "foreground")
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> Option<Hsla> {
|
||||
parse_hex(self.surface.as_deref()?)
|
||||
self.color(&self.surface, "surface")
|
||||
}
|
||||
|
||||
pub fn surface_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.surface_foreground.as_deref()?)
|
||||
self.color(&self.surface_foreground, "surface_foreground")
|
||||
}
|
||||
|
||||
pub fn primary(&self) -> Option<Hsla> {
|
||||
parse_hex(self.primary.as_deref()?)
|
||||
self.color(&self.primary, "primary")
|
||||
}
|
||||
|
||||
pub fn primary_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.primary_foreground.as_deref()?)
|
||||
self.color(&self.primary_foreground, "primary_foreground")
|
||||
}
|
||||
|
||||
pub fn secondary(&self) -> Option<Hsla> {
|
||||
parse_hex(self.secondary.as_deref()?)
|
||||
self.color(&self.secondary, "secondary")
|
||||
}
|
||||
|
||||
pub fn secondary_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.secondary_foreground.as_deref()?)
|
||||
self.color(&self.secondary_foreground, "secondary_foreground")
|
||||
}
|
||||
|
||||
pub fn muted(&self) -> Option<Hsla> {
|
||||
parse_hex(self.muted.as_deref()?)
|
||||
self.color(&self.muted, "muted")
|
||||
}
|
||||
|
||||
pub fn muted_hover(&self) -> Option<Hsla> {
|
||||
parse_hex(self.muted_hover.as_deref()?)
|
||||
self.color(&self.muted_hover, "muted_hover")
|
||||
}
|
||||
|
||||
pub fn muted_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.muted_foreground.as_deref()?)
|
||||
self.color(&self.muted_foreground, "muted_foreground")
|
||||
}
|
||||
|
||||
pub fn destructive(&self) -> Option<Hsla> {
|
||||
parse_hex(self.destructive.as_deref()?)
|
||||
self.color(&self.destructive, "destructive")
|
||||
}
|
||||
|
||||
pub fn destructive_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.destructive_foreground.as_deref()?)
|
||||
self.color(&self.destructive_foreground, "destructive_foreground")
|
||||
}
|
||||
|
||||
pub fn success(&self) -> Option<Hsla> {
|
||||
parse_hex(self.success.as_deref()?)
|
||||
self.color(&self.success, "success")
|
||||
}
|
||||
|
||||
pub fn success_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.success_foreground.as_deref()?)
|
||||
self.color(&self.success_foreground, "success_foreground")
|
||||
}
|
||||
|
||||
pub fn warning(&self) -> Option<Hsla> {
|
||||
parse_hex(self.warning.as_deref()?)
|
||||
self.color(&self.warning, "warning")
|
||||
}
|
||||
|
||||
pub fn warning_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.warning_foreground.as_deref()?)
|
||||
self.color(&self.warning_foreground, "warning_foreground")
|
||||
}
|
||||
|
||||
pub fn info(&self) -> Option<Hsla> {
|
||||
parse_hex(self.info.as_deref()?)
|
||||
self.color(&self.info, "info")
|
||||
}
|
||||
|
||||
pub fn info_foreground(&self) -> Option<Hsla> {
|
||||
parse_hex(self.info_foreground.as_deref()?)
|
||||
self.color(&self.info_foreground, "info_foreground")
|
||||
}
|
||||
|
||||
pub fn border(&self) -> Option<Hsla> {
|
||||
parse_hex(self.border.as_deref()?)
|
||||
self.color(&self.border, "border")
|
||||
}
|
||||
|
||||
pub fn input(&self) -> Option<Hsla> {
|
||||
parse_hex(self.input.as_deref()?)
|
||||
self.color(&self.input, "input")
|
||||
}
|
||||
|
||||
pub fn ring(&self) -> Option<Hsla> {
|
||||
parse_hex(self.ring.as_deref()?)
|
||||
self.color(&self.ring, "ring")
|
||||
}
|
||||
|
||||
pub fn selection(&self) -> Option<Hsla> {
|
||||
parse_hex(self.selection.as_deref()?)
|
||||
self.color(&self.selection, "selection")
|
||||
}
|
||||
|
||||
pub fn overlay(&self) -> Option<Hsla> {
|
||||
parse_hex(self.overlay.as_deref()?)
|
||||
self.color(&self.overlay, "overlay")
|
||||
}
|
||||
|
||||
pub fn scrollbar_thumb(&self) -> Option<Hsla> {
|
||||
parse_hex(self.scrollbar_thumb.as_deref()?)
|
||||
self.color(&self.scrollbar_thumb, "scrollbar_thumb")
|
||||
}
|
||||
|
||||
/// Parses a color field, warning when a stated value is malformed.
|
||||
fn color(&self, field: &Option<String>, role: &str) -> Option<Hsla> {
|
||||
let value = field.as_deref()?;
|
||||
match parse_hex(value) {
|
||||
Some(color) => Some(color),
|
||||
None => {
|
||||
log::warn!("theme definition {role} is not #rgb/#rrggbb: {value:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,12 +254,12 @@ mod tests {
|
||||
"mode = \"dark\"\naccent = \"#7aa2f7\"\nbackground = \"#1a1b26\"\ninfo = \"#38bdf8\"\noverlay_radius = 15.0\nsm_radius = 6.0\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(def.mode, Some(crate::ThemeVariant::Dark));
|
||||
assert_eq!(def.mode(), Some(crate::ThemeVariant::Dark));
|
||||
assert!(def.accent().is_some());
|
||||
assert!(def.info().is_some());
|
||||
assert!(def.foreground().is_none());
|
||||
assert_eq!(def.overlay_radius, Some(15.0));
|
||||
assert_eq!(def.sm_radius, Some(6.0));
|
||||
assert_eq!(def.overlay_radius(), Some(15.0));
|
||||
assert_eq!(def.sm_radius(), Some(6.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+116
-40
@@ -20,7 +20,9 @@ pub use ramps::{AccentShades, Oklch};
|
||||
pub use resolve::{Ramps, ResolvedTheme};
|
||||
pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme};
|
||||
|
||||
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, WindowBackgroundAppearance, px};
|
||||
use gpui::{
|
||||
App, Global, Hsla, Pixels, SpringConfig, WindowAppearance, WindowBackgroundAppearance, hsla, px,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
/// The hairline width of the control border every button and field paints.
|
||||
@@ -46,7 +48,51 @@ pub const TRAFFIC_LIGHT_PAD: Pixels = px(78.0);
|
||||
/// The width of a Windows caption button.
|
||||
pub const WINDOWS_CAPTION_WIDTH: Pixels = px(36.0);
|
||||
/// Windows paints its caption close-button hover red in every theme.
|
||||
pub const WINDOWS_CLOSE_HOVER_HEX: u32 = 0xE81120FF;
|
||||
pub const WINDOWS_CLOSE_HOVER: Hsla = hsla(0.986043, 0.863406, 0.488235, 1.0);
|
||||
/// The close-button hover dimmed while pressed.
|
||||
pub const WINDOWS_CLOSE_PRESSED: Hsla = hsla(0.986043, 0.863406, 0.488235, 0.8);
|
||||
/// The close glyph's color over the red hover.
|
||||
pub const WINDOWS_CLOSE_FOREGROUND: Hsla = gpui::white();
|
||||
/// The width a sidebar opens at.
|
||||
pub const SIDEBAR_DEFAULT_WIDTH: Pixels = px(240.0);
|
||||
/// The narrowest a sidebar may be dragged.
|
||||
pub const SIDEBAR_MIN_WIDTH: Pixels = px(160.0);
|
||||
/// The widest a sidebar may be dragged.
|
||||
pub const SIDEBAR_MAX_WIDTH: Pixels = px(480.0);
|
||||
/// The size a pane starts at before its group has measured the extent.
|
||||
pub const PANE_DEFAULT_SIZE: Pixels = px(200.0);
|
||||
/// The floor a pane keeps while dragging unless the caller lowers it.
|
||||
pub const PANE_MIN_SIZE: Pixels = px(48.0);
|
||||
/// The hairline a resize seam paints on a shared boundary.
|
||||
pub const HAIRLINE_WIDTH: Pixels = px(0.5);
|
||||
/// How much of the window shadow's strength survives losing focus.
|
||||
pub const INACTIVE_SHADOW_OPACITY: f32 = 0.7;
|
||||
/// The spread of the keyboard focus ring.
|
||||
pub const FOCUS_RING_SPREAD: Pixels = px(3.0);
|
||||
/// How far a pushed alert slides in on entry.
|
||||
pub const ENTER_SLIDE_DISTANCE: Pixels = px(24.0);
|
||||
/// The widest a toast may grow.
|
||||
pub const TOAST_MAX_WIDTH: Pixels = px(480.0);
|
||||
/// One revolution of the spinner.
|
||||
pub const SPINNER_PERIOD: Duration = Duration::from_millis(800);
|
||||
/// One sweep of the indeterminate progress bar.
|
||||
pub const PROGRESS_ANIMATION_PERIOD: Duration = Duration::from_millis(1500);
|
||||
/// How long an autohide scrollbar thumb stays fully visible before fading.
|
||||
pub const SCROLLBAR_AUTOHIDE_DELAY: Duration = Duration::from_millis(700);
|
||||
/// How long the autohide scrollbar's fade runs.
|
||||
pub const SCROLLBAR_FADE_DURATION: Duration = Duration::from_millis(250);
|
||||
/// How much a change wash tints the rows it covers.
|
||||
pub const WASH_ALPHA: f32 = 0.14;
|
||||
/// The stronger wash under number gutters and intra-line word changes.
|
||||
pub const WASH_ALPHA_STRONG: f32 = 0.34;
|
||||
/// The subtle wash that sets the number gutter off from the code.
|
||||
pub const WASH_ALPHA_GUTTER: f32 = 0.18;
|
||||
/// The soft wash that marks the active code row.
|
||||
pub const WASH_ALPHA_SOFT: f32 = 0.45;
|
||||
/// How far a scrolling list renders past its viewport.
|
||||
pub const CODE_OVERSCROLL_MAX: Pixels = px(400.0);
|
||||
/// The spring every control's toggle and segment animations ride.
|
||||
pub const CONTROL_SPRING: SpringConfig = SpringConfig::new(500., 35., 1.);
|
||||
/// The monospace advance estimate as a share of the text size.
|
||||
pub const MONO_ADVANCE_RATIO: f32 = 0.62;
|
||||
/// How much a pixel avatar's tint washes over its background.
|
||||
@@ -73,6 +119,15 @@ impl ThemeVariant {
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
|
||||
}
|
||||
}
|
||||
|
||||
/// Classifies a surface by its lightness, for platforms that state no mode.
|
||||
pub fn by_lightness(background: Hsla) -> Self {
|
||||
if background.l >= 0.5 {
|
||||
Self::Light
|
||||
} else {
|
||||
Self::Dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved theme, installed as the app-wide global.
|
||||
@@ -587,21 +642,23 @@ impl Theme {
|
||||
reapply(cx);
|
||||
}
|
||||
|
||||
/// Re-resolves when a window's appearance differs from the resolved variant.
|
||||
/// Re-resolves when no override is set and a window's appearance differs
|
||||
/// from the resolved variant; an override pins the variant instead.
|
||||
///
|
||||
/// Wire to `Window::observe_window_appearance`.
|
||||
pub fn sync_from_window(window: &mut gpui::Window, cx: &mut App) {
|
||||
let variant = ThemeVariant::from_appearance(window.appearance());
|
||||
if cx.global::<Theme>().variant != variant
|
||||
|| cx.global::<ThemeEngine>().mode_override.is_none()
|
||||
{
|
||||
let window_variant = ThemeVariant::from_appearance(window.appearance());
|
||||
let theme = cx.global::<Theme>();
|
||||
let engine = cx.global::<ThemeEngine>();
|
||||
if engine.mode_override.is_none() && theme.variant != window_variant {
|
||||
reapply(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sizes the window's rem to the theme's root font size.
|
||||
pub fn sync_window_rem_size(window: &mut gpui::Window, cx: &App) {
|
||||
window.set_rem_size(cx.global::<Theme>().root_font_size);
|
||||
let theme = cx.global::<Theme>();
|
||||
window.set_rem_size(theme.root_font_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,20 +674,17 @@ impl Global for ThemeEngine {}
|
||||
/// Reads the system, resolves through the current knobs,
|
||||
/// installs the theme global, and repaints every window.
|
||||
pub(crate) fn reapply(cx: &mut App) {
|
||||
let (system_appearance, preset, mode_override) = {
|
||||
let (preset, mode_override, system) = {
|
||||
let engine = cx.global::<ThemeEngine>();
|
||||
(
|
||||
ThemeVariant::from_appearance(cx.window_appearance()),
|
||||
engine.preset.as_ref().map(|(_, def)| def.clone()),
|
||||
engine.mode_override,
|
||||
)
|
||||
let system_appearance = ThemeVariant::from_appearance(cx.window_appearance());
|
||||
let preset = engine.preset.as_ref().map(|(_, def)| def.clone());
|
||||
let mode_override = engine.mode_override;
|
||||
let variant = mode_override
|
||||
.or(preset.as_ref().and_then(|def| def.mode()))
|
||||
.unwrap_or(system_appearance);
|
||||
(preset, mode_override, engine.source.snapshot(variant))
|
||||
};
|
||||
|
||||
let variant = mode_override
|
||||
.or(preset.as_ref().and_then(|def| def.mode))
|
||||
.unwrap_or(system_appearance);
|
||||
|
||||
let system = cx.global::<ThemeEngine>().source.snapshot(variant);
|
||||
let resolved = resolve::resolve(&system, preset.as_ref(), mode_override);
|
||||
|
||||
cx.set_reduce_motion(resolved.accessibility.reduce_motion);
|
||||
@@ -640,41 +694,39 @@ pub(crate) fn reapply(cx: &mut App) {
|
||||
|
||||
/// Forces a variant, independent of the OS. `None` follows the system.
|
||||
pub fn set_mode_override(variant: Option<ThemeVariant>, cx: &mut App) {
|
||||
cx.global_mut::<ThemeEngine>().mode_override = variant;
|
||||
let engine = cx.global_mut::<ThemeEngine>();
|
||||
engine.mode_override = variant;
|
||||
reapply(cx);
|
||||
}
|
||||
|
||||
/// Installs a built-in preset, or `None` to follow the system again.
|
||||
pub fn set_preset(name: Option<&str>, cx: &mut App) -> anyhow::Result<()> {
|
||||
let preset = match name {
|
||||
Some(name) => {
|
||||
let def =
|
||||
presets::builtin(name).ok_or_else(|| anyhow::anyhow!("unknown preset: {name}"))?;
|
||||
let static_name = presets::preset_names()
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|candidate| *candidate == name)
|
||||
.ok_or_else(|| anyhow::anyhow!("unknown preset: {name}"))?;
|
||||
Some((static_name, def))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
cx.global_mut::<ThemeEngine>().preset = preset;
|
||||
let preset = name
|
||||
.map(|name| {
|
||||
presets::builtin(name).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"unknown preset: {name}; available: {}",
|
||||
presets::preset_names().collect::<Vec<_>>().join(", ")
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let engine = cx.global_mut::<ThemeEngine>();
|
||||
engine.preset = preset;
|
||||
reapply(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The variant the mode override forces, if any.
|
||||
pub fn mode_override(cx: &App) -> Option<ThemeVariant> {
|
||||
cx.global::<ThemeEngine>().mode_override
|
||||
let engine = cx.global::<ThemeEngine>();
|
||||
engine.mode_override
|
||||
}
|
||||
|
||||
/// The active preset name, if one is installed.
|
||||
pub fn active_preset(cx: &App) -> Option<&'static str> {
|
||||
cx.global::<ThemeEngine>()
|
||||
.preset
|
||||
.as_ref()
|
||||
.map(|(name, _)| *name)
|
||||
let engine = cx.global::<ThemeEngine>();
|
||||
engine.preset.as_ref().map(|(name, _)| *name)
|
||||
}
|
||||
|
||||
/// Clears the mode override and preset; the theme follows the system again.
|
||||
@@ -687,5 +739,29 @@ pub fn follow_system(cx: &mut App) {
|
||||
|
||||
/// Which adapter produced the current system snapshot.
|
||||
pub fn detected_source(cx: &App) -> &'static str {
|
||||
cx.global::<ThemeEngine>().source.label()
|
||||
let engine = cx.global::<ThemeEngine>();
|
||||
engine.source.label()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::Rgba;
|
||||
|
||||
#[test]
|
||||
fn the_windows_close_colors_match_the_legacy_hex() {
|
||||
let hover = Hsla::from(gpui::rgba(0xE81123FF));
|
||||
let pressed = hover.opacity(0.8);
|
||||
for (token, expected) in [
|
||||
(WINDOWS_CLOSE_HOVER, hover),
|
||||
(WINDOWS_CLOSE_PRESSED, pressed),
|
||||
] {
|
||||
let (token, expected) = (Rgba::from(token), Rgba::from(expected));
|
||||
assert!((token.r - expected.r).abs() < 1e-4);
|
||||
assert!((token.g - expected.g).abs() < 1e-4);
|
||||
assert!((token.b - expected.b).abs() < 1e-4);
|
||||
assert!((token.a - expected.a).abs() < 1e-4);
|
||||
}
|
||||
assert_eq!(WINDOWS_CLOSE_FOREGROUND, gpui::white());
|
||||
}
|
||||
}
|
||||
|
||||
+31
-14
@@ -2,22 +2,39 @@
|
||||
|
||||
use crate::definition::ThemeDefinition;
|
||||
|
||||
const TOKYO_NIGHT: &str = include_str!("../presets/tokyo-night.toml");
|
||||
/// The bundled presets: `(name, TOML source)`. Enumeration and lookup both
|
||||
/// read this one table.
|
||||
const PRESETS: &[(&str, &str)] = &[("tokyo-night", include_str!("../presets/tokyo-night.toml"))];
|
||||
|
||||
pub fn preset_names() -> &'static [&'static str] {
|
||||
&["tokyo-night"]
|
||||
/// Every bundled preset's name.
|
||||
pub fn preset_names() -> impl Iterator<Item = &'static str> {
|
||||
PRESETS.iter().map(|(name, _)| *name)
|
||||
}
|
||||
|
||||
/// Parses the named preset.
|
||||
pub fn builtin(name: &str) -> Option<ThemeDefinition> {
|
||||
match name {
|
||||
"tokyo-night" => match ThemeDefinition::parse_toml(TOKYO_NIGHT) {
|
||||
Ok(def) => Some(def),
|
||||
Err(err) => {
|
||||
eprintln!("bundled preset tokyo-night failed to parse: {err}");
|
||||
None
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
/// Parses the named preset, returning its static name and definition.
|
||||
pub fn builtin(name: &str) -> Option<(&'static str, ThemeDefinition)> {
|
||||
let (name, source) = PRESETS
|
||||
.iter()
|
||||
.find(|(candidate, _)| *candidate == name)
|
||||
.copied()?;
|
||||
match ThemeDefinition::parse_toml(source) {
|
||||
Ok(definition) => Some((name, definition)),
|
||||
Err(error) => {
|
||||
log::error!("bundled preset {name} failed to parse: {error}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_name_table_and_lookup_agree() {
|
||||
for name in preset_names() {
|
||||
assert!(builtin(name).is_some(), "{name} fails to parse");
|
||||
}
|
||||
assert!(builtin("no-such-preset").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ pub struct AccentShades {
|
||||
/// Derives hover/pressed/subtle shades for an accent against its background.
|
||||
///
|
||||
/// Where a platform states ramps (WinUI, KDE) those are used instead,
|
||||
/// every other platform derives its shades here.
|
||||
pub fn derive_shades(accent: Hsla, background: Hsla, is_dark: bool) -> AccentShades {
|
||||
let accent_ok = Oklch::from_hsla(accent);
|
||||
let step = if is_dark { 0.05 } else { -0.05 };
|
||||
|
||||
@@ -55,7 +55,7 @@ pub(crate) fn resolve(
|
||||
mode_override: Option<ThemeVariant>,
|
||||
) -> ResolvedTheme {
|
||||
let variant = mode_override
|
||||
.or(preset.and_then(|def| def.mode))
|
||||
.or(preset.and_then(|def| def.mode()))
|
||||
.unwrap_or(system.variant);
|
||||
|
||||
let base = match variant {
|
||||
@@ -246,7 +246,7 @@ pub(crate) fn resolve(
|
||||
let mut radius = Radius::default();
|
||||
|
||||
if let Some(control) = preset
|
||||
.and_then(|def| def.control_radius)
|
||||
.and_then(|def| def.control_radius())
|
||||
.map(px)
|
||||
.or(system.control_radius)
|
||||
{
|
||||
@@ -254,7 +254,7 @@ pub(crate) fn resolve(
|
||||
}
|
||||
|
||||
if let Some(overlay) = preset
|
||||
.and_then(|def| def.overlay_radius)
|
||||
.and_then(|def| def.overlay_radius())
|
||||
.map(px)
|
||||
.or(system.overlay_radius)
|
||||
{
|
||||
@@ -262,7 +262,7 @@ pub(crate) fn resolve(
|
||||
}
|
||||
|
||||
if let Some(sm) = preset
|
||||
.and_then(|def| def.sm_radius)
|
||||
.and_then(|def| def.sm_radius())
|
||||
.map(px)
|
||||
.or(system.sm_radius)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,16 @@ pub(crate) trait ThemeSource: 'static {
|
||||
fn label(&self) -> &'static str;
|
||||
}
|
||||
|
||||
/// Narrows an f64 color component to f32, clamping to the 0..=1 range and
|
||||
/// mapping non-finite values to 0.0 rather than truncating garbage silently.
|
||||
pub(crate) fn narrow_component(value: f64) -> f32 {
|
||||
if value.is_finite() {
|
||||
value.clamp(0.0, 1.0) as f32
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn platform_source() -> Box<dyn ThemeSource> {
|
||||
Box::new(macos::MacosSource::default())
|
||||
|
||||
@@ -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