System Style Discovery
Overview
SystemStyle is the shared bag of OS-derived values — colours, fonts, scrollbar look, double-click time, reduced-motion preference, accent colour, OS version. WIP — the runtime discovery paths in dll/src/desktop/shell2/*/system_style.rs work but are platform-pinned (no portable abstraction yet); the compile-time defaults in css::system::defaults are stable. It is #[repr(C)], FFI-safe, populated once at app start, and read everywhere as Arc<SystemStyle>. CSD, menu rendering, scrollbar drawing, focus ring drawing, and most widgets all consult it.
The struct serves four roles: it provides the user-agent default values that go into the cascade (system colours, system fonts, scrollbar metrics), it carries OS-level metrics callbacks may want to read (double-click interval, scroll wheel lines), it exposes the user's accessibility preferences (prefers_reduced_motion, prefers_high_contrast), and it holds an optional app_specific_stylesheet for ricing — a CSS file at ~/.config/azul/styles/<exe-name>.css that's loaded at start and applied last in the cascade.
This page describes the shape of the struct, where each platform's discovery happens, and the compile-time defaults that act as fallback. For the cascade machinery that consumes UA values, see Cascade, Inheritance, Restyle.
The shape of SystemStyle
#[repr(C)]
pub struct SystemStyle {
pub fonts: SystemFonts,
pub metrics: SystemMetrics,
pub linux: LinuxCustomization,
pub platform: Platform,
pub focus_visuals: FocusVisuals,
pub language: AzString, // BCP 47, e.g. "en-US"
pub app_specific_stylesheet: Option<Box<Css>>,
pub scrollbar: Option<Box<ComputedScrollbarStyle>>,
pub scroll_physics: ScrollPhysics,
pub theme: Theme, // Light | Dark
pub os_version: OsVersion,
pub prefers_reduced_motion: BoolCondition,
pub prefers_high_contrast: BoolCondition,
pub accessibility: AccessibilitySettings,
pub input: InputMetrics,
pub text_rendering: TextRenderingHints,
pub scrollbar_preferences: ScrollbarPreferences,
pub visual_hints: VisualHints,
pub animation: AnimationMetrics,
pub colors: SystemColors,
pub icon_style: IconStyleOptions,
pub audio: AudioMetrics,
}
Box<Css> and Box<ComputedScrollbarStyle> are heap-indirected so the struct's FFI size is stable across feature flags.
Platform is one of Windows | MacOs | Linux(DesktopEnvironment) | Android | Ios | Unknown. Platform::current() is the compile-time cfg(target_os) answer. The runtime discovery in dll/ overrides it on Linux to fill in the actual desktop env.
The discovery pipeline
There is no portable discover() in azul-css. The crate exposes only SystemStyle::detect(), which is a thin wrapper over the compile-time defaults:
pub fn detect() -> Self {
Self::default_for_platform()
}
pub fn default_for_platform() -> Self {
#[cfg(target_os = "windows")] { defaults::windows_11_light() }
#[cfg(target_os = "macos")] { defaults::macos_modern_light() }
#[cfg(target_os = "linux")] { defaults::gnome_adwaita_light() }
#[cfg(target_os = "android")] { defaults::android_material_light() }
#[cfg(target_os = "ios")] { defaults::ios_light() }
// ...
}
Real OS discovery lives in azul-dll. The single dispatch point is discover_system_style:
pub(crate) fn discover_system_style() -> azul_css::system::SystemStyle {
#[cfg(target_os = "macos")] { shell2::macos::system_style::discover() }
#[cfg(target_os = "windows")] { shell2::windows::system_style::discover() }
#[cfg(target_os = "linux")] { shell2::linux::system_style::discover() }
#[cfg(not(...))] { azul_css::system::SystemStyle::detect() }
}
App::create calls this once and stores the result in the AppConfig. Per-platform priority chains:
- macOS. dlopen AppKit, then Objective-C runtime, then fall back to
defaults::macos_modern_*if dlopen fails. - Windows. LoadLibrary
user32.dllanddwmapi.dll, then fall back todefaults::windows_11_lightif any DLL fails. - Linux. XDG Desktop Portal (D-Bus, raw socket), then CLI discovery (
gsettings,kreadconfig5, Hyprland config, pywal cache), thendefaults::gnome_adwaita_light.
Every backend starts by cloning a hard-coded default and then mutates fields based on what the OS actually returned. This means a query failure for a single value (e.g. accent colour) leaves the rest of the style intact.
macOS: dlopen + Objective-C
pub(crate) fn discover() -> SystemStyle {
let lib = match ObjcLib::load() {
Some(l) => l,
None => return defaults::macos_modern_light(),
};
let mut style = defaults::macos_modern_light();
unsafe {
// 1. theme — [[NSApplication sharedApplication] effectiveAppearance]
// 2. colours — [NSColor labelColor], etc. (15 semantic colours)
// 3. fonts — [NSFont systemFontOfSize:0], monospacedSystemFontOfSize:weight:
// 4. input — [NSEvent doubleClickInterval]
// 5. scrolls — [NSScroller preferredScrollerStyle]
// 6. a11y — [[NSWorkspace sharedWorkspace] accessibilityDisplay…]
// 7. version — [[NSProcessInfo processInfo] operatingSystemVersion]
// 8. locale — [[NSLocale currentLocale] localeIdentifier]
}
// visual_hints fixed by HIG: show_button_images = false, show_menu_images = true
style
}
ObjcLib is a hand-rolled dlopen wrapper that resolves objc_msgSend, objc_getClass, and sel_registerName from libobjc.A.dylib plus the NS* symbols from AppKit.framework. No objc2 linkage. This code path runs even if the user disables every feature. The fallback notes that objc_msgSend returning floats is ABI-different on x86_64 (fpret); the implementation targets arm64 and accepts default-value fallback on x86_64.
The Apple version-numbering jump is encoded literally:
26 => OsVersion::MACOS_TAHOE, // Apple skipped 16-25 in 2025
15 => OsVersion::MACOS_SEQUOIA,
14 => OsVersion::MACOS_SONOMA,
// ...
Windows: LoadLibrary + GetProcAddress
pub(crate) fn discover() -> SystemStyle {
let u32_lib = match User32::load() { /* loads user32.dll */ };
let mut style = defaults::windows_11_light();
// GetDoubleClickTime / GetSystemMetrics(SM_CXDOUBLECLK, SM_CXDRAG)
// GetCaretBlinkTime
// SystemParametersInfoW(SPI_GETCARETWIDTH | SPI_GETWHEELSCROLLLINES |
// SPI_GETMOUSEHOVERTIME | SPI_GETFONTSMOOTHING |
// SPI_GETFONTSMOOTHINGTYPE)
// GetSysColor(COLOR_WINDOW=5, COLOR_WINDOWTEXT=8, COLOR_HIGHLIGHT=13,
// COLOR_HIGHLIGHTTEXT=14, COLOR_BTNFACE=15, COLOR_BTNTEXT=18,
// COLOR_GRAYTEXT=17)
style
}
ClearType is detected via SPI_GETFONTSMOOTHINGTYPE. When the smoothing type is FE_FONTSMOOTHINGCLEARTYPE the subpixel layout is set to Rgb (ClearType's horizontal default), otherwise None. The BGR and vertical variants of SubpixelType are not currently produced by Windows discovery.
Linux: D-Bus first, then CLI, then defaults
pub(crate) fn discover() -> SystemStyle {
// 1. XDG Desktop Portal via raw D-Bus
if let Some((color_scheme, accent_rgb)) = query_xdg_portal() {
let mut style = match color_scheme {
1 => defaults::gnome_adwaita_dark(),
_ => defaults::gnome_adwaita_light(),
};
if let Some((r, g, b)) = accent_rgb {
style.colors.accent = OptionColorU::Some(ColorU::new_rgb(...));
}
discover_linux_extras(&mut style); // gsettings: cursor theme etc.
// ... + language, OS version, reduced-motion, ricing
return style;
}
// 2. CLI discovery
let force_riced = matches!(
azul_css::system::ricing_mode(),
azul_css::system::RicingMode::Force,
);
let mut style = if force_riced {
// AZ_RICING=force: try riced first
discover_riced_style()
.or_else(|_| discover_kde_style())
.or_else(|_| discover_gnome_style())
.unwrap_or_else(|_| defaults::gnome_adwaita_light())
} else {
let de = detect_linux_desktop_env();
match &de {
DesktopEnvironment::Kde => discover_kde_style().or_else(|_| discover_gnome_style())...,
DesktopEnvironment::Gnome => discover_gnome_style().or_else(|_| discover_kde_style())...,
DesktopEnvironment::Other(_) => discover_riced_style()
.or_else(|_| discover_gnome_style())
.or_else(|_| discover_kde_style())...,
}
};
// ... + ricing
style
}
XDG Desktop Portal (raw D-Bus)
The Linux discovery path implements just enough of the D-Bus wire protocol to call org.freedesktop.portal.Settings.Read, with no zbus or dbus crate dependency. It reads two keys from org.freedesktop.appearance: color-scheme (uint32: 0 / 1 / 2 = no-pref / dark / light) and accent-color (variant of three doubles).
Per-DE CLI discovery
discover_gnome_style. Reads fromgsettings get org.gnome.desktop.interface ...for color-scheme, gtk-theme, font-name, monospace-font-name, accent-color, cursor-theme, and cursor-size.discover_kde_style. Reads fromkreadconfig5 --file kdeglobals --group ... --key ...forColorScheme,Font,ColorEffects:Disabled, etc.discover_riced_style. Parses$XDG_CONFIG_HOME/hypr/hyprland.conf,$HOME/.cache/wal/colors.json,i3/config, andsway/config.
AZ_RICING=force reorders the chain so a tiling-WM user with a GNOME session set in XDG_CURRENT_DESKTOP still gets their pywal palette. AZ_RICING=off skips the riced sources entirely.
Linux extras
discover_linux_extras runs after either path completes and fills in fields that aren't in the portal's API but ARE in gsettings: cursor theme, cursor size, icon theme, GTK theme name, and titlebar button layout ("close,minimize,maximize:").
Compile-time defaults
css::system::defaults has constructors that each return a fully-populated SystemStyle. They serve four roles:
- Backend fallback when dlopen / D-Bus fails.
- Headless / test rendering.
- The
feature = "io"is off and runtime discovery is unavailable. - Nostalgia themes that aren't reachable from real OS settings.
Available constructors:
- Modern.
windows_11_light/dark,macos_modern_light/dark,gnome_adwaita_light/dark,kde_breeze_light. - Mobile.
android_material_light,ios_light. - Nostalgia.
windows_7_aero,windows_xp_luna,macos_aqua,gtk2_clearlooks,android_holo_dark.
The nostalgia constructors are public so applications can opt-in via SystemStyle::with_* if they want a vintage theme. They aren't reachable from runtime discovery. OsVersion::WIN_XP is never produced by discover().
Each constructor uses ..Default::default() to fill the long tail of fields. This is safe because SystemStyle derives Default and every nested type (InputMetrics, AnimationMetrics, AccessibilitySettings, ...) implements its own Default with sensible values (e.g. InputMetrics::double_click_time_ms = 500).
App-specific ricing
After discovery succeeds, every Linux path calls:
fn load_app_specific_stylesheet() -> Option<Css> {
if !azul_css::system::ricing_enabled() { return None; }
let exe_name = std::env::current_exe()?.file_stem()?.to_string_lossy().into_owned();
let config_dir = get_config_dir()?; // $XDG_CONFIG_HOME or ~/.config
let css_path = format!("{}/azul/styles/{}.css", config_dir, exe_name);
let css_str = std::fs::read_to_string(&css_path).ok()?;
let (css, _warnings) = new_from_str(&css_str);
if css.is_empty() { None } else { Some(css) }
}
The result lands in app_specific_stylesheet. Parser warnings are discarded. Invalid user CSS does not abort discovery.
- Linux.
$XDG_CONFIG_HOME/azul/styles/<exe>.css, else~/.config/azul/styles/<exe>.css. - macOS.
~/Library/Application Support/azul/styles/<exe>.css. - Windows.
%APPDATA%\azul\styles\<exe>.css.
exe is the Path::file_stem() of the running executable. myapp matches both myapp and myapp.exe.
Css generators on SystemStyle
SystemStyle carries two CSS-emitting methods used by the menu / CSD pipeline (see Menus and Client-Side Decorations):
create_csd_stylesheet() -> Css. Emits.csd-titlebar,.csd-title,.csd-buttons,.csd-button,.csd-button:hover,.csd-close:hover, plus macOS and Linux specialisations.create_menu_stylesheet() -> Css. Emits.menu-container,.menu-item,.menu-item:hover,.menu-item-disabled/-greyed,.menu-item-icon,.menu-item-label,.menu-item-shortcut,.menu-item-arrow, and.menu-separator. Defined as an extension traitSystemStyleMenuExt.
Both build a String and parse it back to a Css via parser2::new_from_str, then tag every rule with rule_priority::SYSTEM so author CSS overrides win. This is fragile — format! typos are caught only at parse time, not compile time. Errors are log-routed via log_debug!(LogCategory::General, ...) but the build still succeeds with whatever rules parsed correctly.
Detecting the desktop environment and language
Two helpers in azul-css work without azul-dll:
detect_linux_desktop_envchecksXDG_CURRENT_DESKTOP,DESKTOP_SESSION, then specific markers (GNOME_DESKTOP_SESSION_ID,KDE_FULL_SESSION,HYPRLAND_INSTANCE_SIGNATURE,SWAYSOCK,I3SOCK). ReturnsDesktopEnvironment::{Gnome, Kde, Other(name)}.detect_system_languagechecksLANGUAGE,LC_ALL,LC_MESSAGES,LANGin priority, strips.UTF-8suffixes and:-separated alternatives, and normalisesde_DEtode-DE. Returns"en-US"on failure. Native discovery overrides this on macOS (viaNSLocale) and Windows.
Where SystemStyle is read
The struct is consumed in:
dll/src/desktop/csd.rsfor titlebar styling and the menu-bar dropdown callback.dll/src/desktop/menu_renderer.rsfor menu colour and font lookup.layout/src/widgets/scrollbar.rsfor scrollbar visual style andScrollbarPreferences.visibility.layout/src/widgets/titlebar.rsforTitlebar::from_system_style_csdandtm.buttons/tm.button_side.layout/src/managers/scroll_state.rsforScrollPhysics(momentum and overscroll) per platform.- Anywhere a callback wants the live system theme: every
CallbackInfoexposesget_system_style() -> Arc<SystemStyle>.
Not by reading SystemStyle directly, but by walking Arc<SystemStyle>. Cloning is one atomic refcount bump, so widgets pass the Arc around freely.
Adding a field
The cross-cutting checklist when extending SystemStyle:
- Add the field to the struct in
css/src/system.rs. Make itDefault-able. - Update
SystemStyle::to_json_stringso debug output matches. - Set the field in every
defaults::*constructor that should differ fromDefault. - Wire each native discovery path:
- macOS:
dll/src/desktop/shell2/macos/system_style.rs::discover - Windows:
dll/src/desktop/shell2/windows/system_style.rs::discover - Linux: pick the right helper inside
dll/src/desktop/shell2/linux/system_style.rs(discover_gnome_style,discover_kde_style,discover_linux_extras, etc.)
- macOS:
- If exposed via FFI, add a
repr(C)Option-wrapper if the type isn't already FFI-safe, and regenerateapi.json.
See also
- Cascade, Inheritance, Restyle — UA values from
SystemStylefeed the cascade as priority-1 defaults. - Menus and Client-Side Decorations — consumer of
create_csd_stylesheetandcreate_menu_stylesheet. - Accessibility Backends — consumer of
accessibilityandprefers_*fields. - Styling Subsystem — parent overview of the styling pipeline.
Coming Up Next
- Accessibility Backends — Per-platform a11y back-ends - UIA, AT-SPI, NSAccessibility
- Shell2 Common Layer — Shared shell infrastructure across platforms
- Cascade, Inheritance, Restyle — Selector matching, specificity, and computed values