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.dll and dwmapi.dll, then fall back to defaults::windows_11_light if any DLL fails.
  • Linux. XDG Desktop Portal (D-Bus, raw socket), then CLI discovery (gsettings, kreadconfig5, Hyprland config, pywal cache), then defaults::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 from gsettings 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 from kreadconfig5 --file kdeglobals --group ... --key ... for ColorScheme, Font, ColorEffects:Disabled, etc.
  • discover_riced_style. Parses $XDG_CONFIG_HOME/hypr/hyprland.conf, $HOME/.cache/wal/colors.json, i3/config, and sway/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:

  1. Backend fallback when dlopen / D-Bus fails.
  2. Headless / test rendering.
  3. The feature = "io" is off and runtime discovery is unavailable.
  4. 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 trait SystemStyleMenuExt.

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_env checks XDG_CURRENT_DESKTOP, DESKTOP_SESSION, then specific markers (GNOME_DESKTOP_SESSION_ID, KDE_FULL_SESSION, HYPRLAND_INSTANCE_SIGNATURE, SWAYSOCK, I3SOCK). Returns DesktopEnvironment::{Gnome, Kde, Other(name)}.
  • detect_system_language checks LANGUAGE, LC_ALL, LC_MESSAGES, LANG in priority, strips .UTF-8 suffixes and :-separated alternatives, and normalises de_DE to de-DE. Returns "en-US" on failure. Native discovery overrides this on macOS (via NSLocale) and Windows.

Where SystemStyle is read

The struct is consumed in:

  • dll/src/desktop/csd.rs for titlebar styling and the menu-bar dropdown callback.
  • dll/src/desktop/menu_renderer.rs for menu colour and font lookup.
  • layout/src/widgets/scrollbar.rs for scrollbar visual style and ScrollbarPreferences.visibility.
  • layout/src/widgets/titlebar.rs for Titlebar::from_system_style_csd and tm.buttons / tm.button_side.
  • layout/src/managers/scroll_state.rs for ScrollPhysics (momentum and overscroll) per platform.
  • Anywhere a callback wants the live system theme: every CallbackInfo exposes get_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:

  1. Add the field to the struct in css/src/system.rs. Make it Default-able.
  2. Update SystemStyle::to_json_string so debug output matches.
  3. Set the field in every defaults::* constructor that should differ from Default.
  4. 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.)
  5. If exposed via FFI, add a repr(C) Option-wrapper if the type isn't already FFI-safe, and regenerate api.json.

See also

Coming Up Next

Back to guide index