Skip to main content

azul_css/
system.rs

1//! Discovers system-native styling for colors, fonts, and other metrics.
2//!
3//! This module provides a best-effort attempt to query the host operating system
4//! for its UI theme information. This is gated behind the **`io`** feature flag.
5//!
6//! **End-user customization (`AZ_RICING`):**
7//! By default (if the `io` feature is enabled), Azul looks for an
8//! application-specific stylesheet at `~/.config/azul/styles/<app_name>.css`
9//! (or `%APPDATA%\azul\styles\<app_name>.css` on Windows) and applies it as
10//! the last layer of the cascade, letting end-users "rice" any Azul app.
11//!
12//! The `AZ_RICING` env var has three modes (case-insensitive):
13//!
14//! - unset (default): load the user CSS if present; on Linux, the
15//!   detection chain is `KDE > GNOME > riced > defaults`.
16//! - `AZ_RICING=off` (aliases: `disabled`, `none`, `0`): skip the user
17//!   CSS file and the riced-desktop sources (Hyprland config, pywal
18//!   cache). Use for kiosk builds or CI runs that mustn't pick up local
19//!   customization.
20//! - `AZ_RICING=force` (aliases: `prefer`, `aggressive`, `1`): on Linux,
21//!   reorder the detection chain so riced-desktop sources win over
22//!   GNOME/KDE — useful for tiling-WM users whose `XDG_CURRENT_DESKTOP`
23//!   still says `gnome`. The user CSS file still loads.
24
25#![cfg(feature = "parser")]
26
27use alloc::{
28    boxed::Box,
29    string::{String, ToString},
30    vec::Vec,
31};
32use crate::{
33    corety::{AzString, OptionF32, OptionString, OptionU16},
34    css::Css,
35    parser2::{new_from_str, CssParseWarnMsg},
36    props::{
37        basic::{
38            color::{parse_css_color, ColorU, OptionColorU},
39            pixel::{PixelValue, OptionPixelValue},
40        },
41        style::scrollbar::{ComputedScrollbarStyle, OverscrollBehavior, ScrollBehavior, ScrollPhysics},
42    },
43};
44
45use crate::dynamic_selector::{BoolCondition, OsVersion};
46use core::fmt::Write;
47
48// --- End-user customization mode ---
49
50/// User-customization mode controlled by the `AZ_RICING` env var.
51///
52/// See the module-level documentation for the full description.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[derive(Default)]
55pub enum RicingMode {
56    /// `AZ_RICING=off` (or `disabled` / `none` / `0`). Skip the user
57    /// CSS file *and* the riced-desktop sources. Vanilla detection.
58    Off,
59    /// Unset. Load the user CSS if present; standard detection chain
60    /// (`KDE > GNOME > riced > defaults` on Linux).
61    #[default]
62    Default,
63    /// `AZ_RICING=force` (or `prefer` / `aggressive` / `1`). Reorder
64    /// the Linux detection chain so riced-desktop sources win over
65    /// GNOME/KDE. The user CSS file still loads.
66    Force,
67}
68
69
70/// Read the `AZ_RICING` env var and classify it. Case-insensitive.
71/// Anything we don't recognise falls through to `Default` so a typo
72/// degrades gracefully instead of disabling the feature silently.
73#[must_use] pub fn ricing_mode() -> RicingMode {
74    let Ok(raw) = std::env::var("AZ_RICING") else {
75        return RicingMode::Default;
76    };
77    match raw.trim().to_ascii_lowercase().as_str() {
78        "off" | "disabled" | "none" | "0" | "false" => RicingMode::Off,
79        "force" | "prefer" | "aggressive" | "1" | "true" => RicingMode::Force,
80        _ => RicingMode::Default,
81    }
82}
83
84/// True when the user CSS file at `~/.config/azul/styles/<app>.css`
85/// should be read. False only when `AZ_RICING=off` is set.
86#[must_use] pub fn ricing_enabled() -> bool {
87    !matches!(ricing_mode(), RicingMode::Off)
88}
89
90// --- Public Data Structures ---
91#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
92/// Represents the detected platform.
93#[derive(Debug, Default, Clone, PartialEq, Eq)]
94#[repr(C, u8)]
95pub enum Platform {
96    Windows,
97    MacOs,
98    Linux(DesktopEnvironment),
99    Android,
100    Ios,
101    #[default]
102    Unknown,
103}
104
105impl Platform {
106    /// Get the current platform at compile time.
107    #[inline]
108    #[must_use] pub const fn current() -> Self {
109        #[cfg(target_os = "macos")]
110        { Self::MacOs }
111        #[cfg(target_os = "windows")]
112        { Self::Windows }
113        #[cfg(target_os = "linux")]
114        { Self::Linux(DesktopEnvironment::Other(AzString::from_const_str("unknown"))) }
115        #[cfg(target_os = "android")]
116        { Self::Android }
117        #[cfg(target_os = "ios")]
118        { Self::Ios }
119        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux", target_os = "android", target_os = "ios")))]
120        { Self::Unknown }
121    }
122}
123#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
124/// Represents the detected Linux Desktop Environment.
125#[derive(Debug, Clone, PartialEq, Eq)]
126#[repr(C, u8)]
127pub enum DesktopEnvironment {
128    Gnome,
129    Kde,
130    Other(AzString),
131}
132
133/// The overall theme type.
134#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
135#[repr(C)]
136pub enum Theme {
137    #[default]
138    Light,
139    Dark,
140}
141
142/// A unified collection of discovered system style properties.
143#[derive(Debug, Clone, PartialEq)]
144#[repr(C)]
145pub struct SystemStyle {
146    pub fonts: SystemFonts,
147    pub metrics: SystemMetrics,
148    /// Linux-specific customisation (icon theme, cursor theme, GTK theme, ...)
149    pub linux: LinuxCustomization,
150    pub platform: Platform,
151    /// Focus ring / indicator visual style
152    pub focus_visuals: FocusVisuals,
153    /// System language/locale in BCP 47 format (e.g., "en-US", "de-DE")
154    /// Detected from OS settings at startup
155    pub language: AzString,
156    /// An optional, user-provided stylesheet loaded from a conventional
157    /// location (`~/.config/azul/styles/<app_name>.css`), allowing for
158    /// application-specific "ricing". Only loaded when the "io" feature
159    /// is enabled and `AZ_RICING` is not set to `off`.
160    pub app_specific_stylesheet: Option<Box<Css>>,
161    /// Scrollbar style information (boxed to ensure stable FFI size)
162    pub scrollbar: Option<Box<ComputedScrollbarStyle>>,
163    /// Global scroll physics configuration (momentum, friction, rubber-banding).
164    /// Platform-specific defaults are applied during system style discovery.
165    /// Applications can override this to change the "feel" of scrolling globally.
166    pub scroll_physics: ScrollPhysics,
167    pub theme: Theme,
168    /// Detected OS version (e.g., Windows 11 22H2, macOS Sonoma, etc.)
169    pub os_version: OsVersion,
170    /// User prefers reduced motion (accessibility setting)
171    pub prefers_reduced_motion: BoolCondition,
172    /// User prefers high contrast (accessibility setting)
173    pub prefers_high_contrast: BoolCondition,
174    /// Detailed accessibility settings (superset of `prefers_reduced_motion` / `prefers_high_contrast`)
175    pub accessibility: AccessibilitySettings,
176    /// Input interaction timing / distance thresholds from the OS
177    pub input: InputMetrics,
178    /// Text rendering / anti-aliasing hints from the OS
179    pub text_rendering: TextRenderingHints,
180    /// OS-level scrollbar visibility / click-behaviour preferences
181    pub scrollbar_preferences: ScrollbarPreferences,
182    /// Visual hints: icons in menus/buttons, toolbar style, tooltips
183    pub visual_hints: VisualHints,
184    /// Animation enable/disable, speed factor, focus indicator behaviour
185    pub animation: AnimationMetrics,
186    pub colors: SystemColors,
187    /// Icon-specific styling options (grayscale, tinting, etc.)
188    pub icon_style: IconStyleOptions,
189    /// Audio feedback preferences (event sounds, input sounds)
190    pub audio: AudioMetrics,
191    /// FFI double-drop guard. `SystemStyle` owns two heap pointers
192    /// (`app_specific_stylesheet`, `scrollbar`). The codegen Az wrapper
193    /// (`AzSystemStyle`) gets an `impl Drop` -> `AzSystemStyle_delete` ->
194    /// `drop_in_place::<SystemStyle>`, and is nested by value as
195    /// `AzAppConfig.system_style`. Dropping an `AzAppConfig` by value
196    /// therefore drops the real `SystemStyle` once (freeing both Boxes) and
197    /// then re-runs `_delete` on the SAME bytes via drop-glue -> double free.
198    /// Same class as `GlContextPtr` / `IconProviderHandle` (see core/src/icon.rs).
199    /// The first `Drop` disarms this flag; the second sees it cleared and
200    /// neutralizes itself (takes + forgets the already-freed Boxes) so the
201    /// redundant drop-glue is a no-op. Defaults to `true` (own + free once).
202    pub run_destructor: bool,
203}
204
205impl Default for SystemStyle {
206    fn default() -> Self {
207        Self {
208            fonts: SystemFonts::default(),
209            metrics: SystemMetrics::default(),
210            linux: LinuxCustomization::default(),
211            platform: Platform::default(),
212            focus_visuals: FocusVisuals::default(),
213            language: AzString::default(),
214            app_specific_stylesheet: None,
215            scrollbar: None,
216            scroll_physics: ScrollPhysics::default(),
217            theme: Theme::default(),
218            os_version: OsVersion::default(),
219            prefers_reduced_motion: BoolCondition::default(),
220            prefers_high_contrast: BoolCondition::default(),
221            accessibility: AccessibilitySettings::default(),
222            input: InputMetrics::default(),
223            text_rendering: TextRenderingHints::default(),
224            scrollbar_preferences: ScrollbarPreferences::default(),
225            visual_hints: VisualHints::default(),
226            animation: AnimationMetrics::default(),
227            colors: SystemColors::default(),
228            icon_style: IconStyleOptions::default(),
229            audio: AudioMetrics::default(),
230            run_destructor: true,
231        }
232    }
233}
234
235impl Drop for SystemStyle {
236    fn drop(&mut self) {
237        // Gate the heap frees on `run_destructor` to defuse the codegen
238        // double-drop (see the `run_destructor` field docs). drop_in_place
239        // runs THIS method, then the field drop-glue; so:
240        //  * FIRST drop (flag set): disarm the flag, then let the field
241        //    drop-glue free the two Boxes exactly once.
242        //  * SECOND drop on the same bytes (flag cleared by the first): the
243        //    Boxes are already freed but the fields still hold dangling
244        //    `Some(ptr)`. Take them out (-> None) and forget the dangling
245        //    values so the trailing drop-glue is a no-op (never derefs/frees).
246        if self.run_destructor {
247            self.run_destructor = false;
248        } else {
249            core::mem::forget(self.app_specific_stylesheet.take());
250            core::mem::forget(self.scrollbar.take());
251        }
252    }
253}
254
255/// Icon-specific styling options for accessibility and theming.
256///
257/// These settings affect how icons are rendered, supporting accessibility
258/// needs like reduced colors and high contrast modes.
259#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
260#[repr(C)]
261pub struct IconStyleOptions {
262    /// If true, icons should be rendered in grayscale (for color-blind users
263    /// or reduced color preference). Applies a CSS grayscale filter.
264    pub prefer_grayscale: bool,
265    /// Optional tint color to apply to icons. Useful for matching icons
266    /// to the current theme or for high contrast modes.
267    pub tint_color: OptionColorU,
268    /// If true, icons should inherit the current text color instead of
269    /// using their original colors. Works well with font-based icons.
270    pub inherit_text_color: bool,
271}
272
273/// System font types that can be resolved at runtime based on OS settings.
274/// 
275/// This enum allows specifying semantic font roles that get resolved to
276/// actual font families based on the current platform and user preferences.
277/// For example, `Monospace` resolves to:
278/// - macOS: SF Mono or Menlo
279/// - Windows: Cascadia Mono or Consolas
280/// - Linux: Ubuntu Mono or `DejaVu` Sans Mono
281/// 
282/// Font variants (bold, italic) can be combined with the base type.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
284#[repr(C)]
285#[derive(Default)]
286pub enum SystemFontType {
287    /// UI font for buttons, labels, menus (SF Pro, Segoe UI, Cantarell)
288    #[default]
289    Ui,
290    /// Bold variant of UI font
291    UiBold,
292    /// Monospace font for code (SF Mono, Consolas, Ubuntu Mono)
293    Monospace,
294    /// Bold variant of monospace font
295    MonospaceBold,
296    /// Italic variant of monospace font
297    MonospaceItalic,
298    /// Font for window titles
299    Title,
300    /// Bold variant of title font
301    TitleBold,
302    /// Font for menu items
303    Menu,
304    /// Small/caption font
305    Small,
306    /// Serif font for reading content (New York on macOS, Georgia on Windows)
307    Serif,
308    /// Bold variant of serif font
309    SerifBold,
310}
311
312
313impl SystemFontType {
314    /// Parse a `SystemFontType` from a CSS string.
315    /// 
316    /// Supported formats:
317    /// - `system:ui`, `system:ui:bold`
318    /// - `system:monospace`, `system:monospace:bold`, `system:monospace:italic`
319    /// - `system:title`, `system:title:bold`
320    /// - `system:menu`
321    /// - `system:small`
322    /// - `system:serif`, `system:serif:bold`
323    #[must_use] pub fn from_css_str(s: &str) -> Option<Self> {
324        let s = s.trim();
325        if !s.starts_with("system:") {
326            return None;
327        }
328        let rest = &s[7..]; // Skip "system:"
329        match rest {
330            "ui" => Some(Self::Ui),
331            "ui:bold" => Some(Self::UiBold),
332            "monospace" => Some(Self::Monospace),
333            "monospace:bold" => Some(Self::MonospaceBold),
334            "monospace:italic" => Some(Self::MonospaceItalic),
335            "title" => Some(Self::Title),
336            "title:bold" => Some(Self::TitleBold),
337            "menu" => Some(Self::Menu),
338            "small" => Some(Self::Small),
339            "serif" => Some(Self::Serif),
340            "serif:bold" => Some(Self::SerifBold),
341            _ => None,
342        }
343    }
344    
345    /// Get the CSS syntax for this system font type.
346    #[must_use] pub const fn as_css_str(&self) -> &'static str {
347        match self {
348            Self::Ui => "system:ui",
349            Self::UiBold => "system:ui:bold",
350            Self::Monospace => "system:monospace",
351            Self::MonospaceBold => "system:monospace:bold",
352            Self::MonospaceItalic => "system:monospace:italic",
353            Self::Title => "system:title",
354            Self::TitleBold => "system:title:bold",
355            Self::Menu => "system:menu",
356            Self::Small => "system:small",
357            Self::Serif => "system:serif",
358            Self::SerifBold => "system:serif:bold",
359        }
360    }
361    
362    /// Returns true if this system font type implies bold weight.
363    /// Used when resolving system fonts to pass the correct weight to fontconfig.
364    #[must_use] pub const fn is_bold(&self) -> bool {
365        matches!(
366            self,
367            Self::UiBold
368                | Self::MonospaceBold
369                | Self::TitleBold
370                | Self::SerifBold
371        )
372    }
373    
374    /// Returns true if this system font type implies italic style.
375    #[must_use] pub const fn is_italic(&self) -> bool {
376        matches!(self, Self::MonospaceItalic)
377    }
378}
379
380/// Accessibility settings detected from the operating system.
381/// 
382/// These settings allow apps to adapt their UI for users with accessibility needs.
383/// Detection methods:
384/// - macOS: `UIAccessibility` APIs (isBoldTextEnabled, isReduceMotionEnabled, etc.)
385/// - Windows: `SystemParametersInfo` (`SPI_GETHIGHCONTRAST`, `SPI_GETCLIENTAREAANIMATION`)
386/// - Linux: gsettings (org.gnome.desktop.interface, org.gnome.desktop.a11y)
387#[derive(Debug, Default, Clone, Copy, PartialEq)]
388#[repr(C)]
389pub struct AccessibilitySettings {
390    /// Text scaling factor (1.0 = normal, 1.5 = 150%, etc.)
391    pub text_scale_factor: f32,
392    /// User prefers bold text for better readability
393    /// macOS: UIAccessibility.isBoldTextEnabled
394    /// Windows: N/A (font scaling)
395    /// Linux: org.gnome.desktop.interface text-scaling-factor
396    pub prefers_bold_text: bool,
397    /// User prefers larger text
398    /// macOS: preferredContentSizeCategory
399    /// Windows: `SystemParametersInfo` text scale factor
400    /// Linux: org.gnome.desktop.interface text-scaling-factor
401    pub prefers_larger_text: bool,
402    /// User prefers high contrast colors
403    /// macOS: UIAccessibility.isDarkerSystemColorsEnabled
404    /// Windows: `SPI_GETHIGHCONTRAST`
405    /// Linux: org.gnome.desktop.a11y.interface high-contrast
406    pub prefers_high_contrast: bool,
407    /// User prefers reduced motion/animations
408    /// macOS: UIAccessibility.isReduceMotionEnabled
409    /// Windows: `SPI_GETCLIENTAREAANIMATION` (inverted)
410    /// Linux: org.gnome.desktop.interface enable-animations (inverted)
411    pub prefers_reduced_motion: bool,
412    /// User prefers reduced transparency
413    /// macOS: UIAccessibility.isReduceTransparencyEnabled
414    /// Windows: N/A
415    /// Linux: N/A
416    pub prefers_reduced_transparency: bool,
417    /// Screen reader is active (`VoiceOver`, Narrator, Orca)
418    pub screen_reader_active: bool,
419    /// User prefers differentiate without color
420    /// macOS: UIAccessibility.shouldDifferentiateWithoutColor
421    pub differentiate_without_color: bool,
422}
423
424/// Common system colors used for UI elements.
425/// 
426/// These colors are queried from the operating system and automatically adapt
427/// to the current theme (light/dark mode) and accent color settings.
428/// 
429/// On macOS, these correspond to `NSColor` semantic colors.
430/// On Windows, these come from `UISettings`.
431/// On Linux/GTK, these come from the GTK theme.
432#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
433#[repr(C)]
434pub struct SystemColors {
435    // === Primary semantic colors ===
436    /// Primary text color (NSColor.textColor on macOS)
437    pub text: OptionColorU,
438    /// Secondary text color for less prominent text (NSColor.secondaryLabelColor)
439    pub secondary_text: OptionColorU,
440    /// Tertiary text color for disabled/placeholder text (NSColor.tertiaryLabelColor)
441    pub tertiary_text: OptionColorU,
442    /// Background color for content areas (NSColor.textBackgroundColor)
443    pub background: OptionColorU,
444    
445    // === Accent colors ===
446    /// System accent color chosen by user (NSColor.controlAccentColor on macOS)
447    pub accent: OptionColorU,
448    /// Text color on accent backgrounds
449    pub accent_text: OptionColorU,
450    
451    // === Control colors ===
452    /// Button/control background (NSColor.controlColor)
453    pub button_face: OptionColorU,
454    /// Button/control text color (NSColor.controlTextColor)
455    pub button_text: OptionColorU,
456    /// Disabled control text color (NSColor.disabledControlTextColor)
457    pub disabled_text: OptionColorU,
458    
459    // === Window colors ===
460    /// Window background color (NSColor.windowBackgroundColor)
461    pub window_background: OptionColorU,
462    /// Under-page background color (NSColor.underPageBackgroundColor)
463    pub under_page_background: OptionColorU,
464    
465    // === Selection colors ===
466    /// Selection background when window is focused (NSColor.selectedContentBackgroundColor)
467    pub selection_background: OptionColorU,
468    /// Selection text color when window is focused
469    pub selection_text: OptionColorU,
470    /// Selection background when window is NOT focused (NSColor.unemphasizedSelectedContentBackgroundColor)
471    /// This is used for :backdrop state styling
472    pub selection_background_inactive: OptionColorU,
473    /// Selection text color when window is NOT focused
474    pub selection_text_inactive: OptionColorU,
475    
476    // === Additional semantic colors ===
477    /// Link color (NSColor.linkColor)
478    pub link: OptionColorU,
479    /// Separator/divider color (NSColor.separatorColor)
480    pub separator: OptionColorU,
481    /// Grid/table line color (NSColor.gridColor)
482    pub grid: OptionColorU,
483    /// Find/search highlight color (NSColor.findHighlightColor)
484    pub find_highlight: OptionColorU,
485    
486    // === Sidebar colors (macOS-specific) ===
487    /// Sidebar background color
488    pub sidebar_background: OptionColorU,
489    /// Selected row in sidebar
490    pub sidebar_selection: OptionColorU,
491}
492
493/// Common system font settings.
494/// 
495/// On macOS, these are queried from `NSFont`.
496/// On Windows, these come from `SystemParametersInfo`.
497/// On Linux, these come from GTK/gsettings.
498#[derive(Debug, Default, Clone, PartialEq, Eq)]
499#[repr(C)]
500pub struct SystemFonts {
501    /// The primary font used for UI elements like buttons and labels.
502    /// On macOS: SF Pro (system font)
503    /// On Windows: Segoe UI
504    /// On Linux: Cantarell, Ubuntu, or system default
505    pub ui_font: OptionString,
506    /// The default font size for UI elements, in points.
507    pub ui_font_size: OptionF32,
508    /// The font used for code or other monospaced text.
509    /// On macOS: SF Mono or Menlo
510    /// On Windows: Cascadia Mono or Consolas
511    /// On Linux: Ubuntu Mono or `DejaVu` Sans Mono
512    pub monospace_font: OptionString,
513    /// Monospace font size in points
514    pub monospace_font_size: OptionF32,
515    /// Bold variant of the UI font (if different)
516    pub ui_font_bold: OptionString,
517    /// Font for window titles
518    pub title_font: OptionString,
519    /// Title font size in points
520    pub title_font_size: OptionF32,
521    /// Font for menu items
522    pub menu_font: OptionString,
523    /// Menu font size in points
524    pub menu_font_size: OptionF32,
525    /// Small/caption font for less prominent text
526    pub small_font: OptionString,
527    /// Small font size in points
528    pub small_font_size: OptionF32,
529}
530
531/// Common system metrics for UI element sizing and spacing.
532#[derive(Debug, Default, Clone, PartialEq, Eq)]
533#[repr(C)]
534pub struct SystemMetrics {
535    /// The corner radius for standard elements like buttons.
536    pub corner_radius: OptionPixelValue,
537    /// The width of standard borders.
538    pub border_width: OptionPixelValue,
539    /// The horizontal (left/right) padding for buttons and similar controls.
540    pub button_padding_horizontal: OptionPixelValue,
541    /// The vertical (top/bottom) padding for buttons and similar controls.
542    pub button_padding_vertical: OptionPixelValue,
543    /// Titlebar layout information (button positions, safe areas, etc.)
544    pub titlebar: TitlebarMetrics,
545}
546
547/// Which side of the titlebar the window control buttons are on.
548#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
549#[repr(C)]
550pub enum TitlebarButtonSide {
551    /// Buttons are on the left (macOS default)
552    Left,
553    /// Buttons are on the right (Windows, most Linux DEs)
554    #[default]
555    Right,
556}
557
558/// Which window control buttons are available in the titlebar.
559#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
560#[repr(C)]
561pub struct TitlebarButtons {
562    /// Close button is available
563    pub has_close: bool,
564    /// Minimize button is available
565    pub has_minimize: bool,
566    /// Maximize/zoom button is available
567    pub has_maximize: bool,
568    /// Fullscreen button is available (macOS green button behavior)
569    pub has_fullscreen: bool,
570}
571
572impl Default for TitlebarButtons {
573    fn default() -> Self {
574        Self {
575            has_close: true,
576            has_minimize: true,
577            has_maximize: true,
578            has_fullscreen: false,
579        }
580    }
581}
582
583/// Safe area insets for devices with notches, rounded corners, or sensor housings.
584/// 
585/// On devices like iPhones with notches or Dynamic Island, the safe area
586/// indicates regions where content should not be placed to avoid being
587/// obscured by hardware features.
588#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
589#[repr(C)]
590pub struct SafeAreaInsets {
591    /// Inset from the top edge (notch, camera housing, etc.)
592    pub top: OptionPixelValue,
593    /// Inset from the bottom edge (home indicator on iPhone)
594    pub bottom: OptionPixelValue,
595    /// Inset from the left edge (rounded corners)
596    pub left: OptionPixelValue,
597    /// Inset from the right edge (rounded corners)
598    pub right: OptionPixelValue,
599}
600
601/// Metrics for titlebar layout and window chrome.
602/// 
603/// This provides information needed to correctly position custom titlebar
604/// content when using `WindowDecorations::NoTitle` (expanded title mode).
605#[derive(Debug, Clone, PartialEq, Eq)]
606#[repr(C)]
607pub struct TitlebarMetrics {
608    /// Which side the window control buttons are on
609    pub button_side: TitlebarButtonSide,
610    /// Which buttons are available
611    pub buttons: TitlebarButtons,
612    /// Height of the titlebar in pixels
613    pub height: OptionPixelValue,
614    /// Width reserved for window control buttons (close/min/max)
615    /// This is the space to avoid when drawing custom title text
616    pub button_area_width: OptionPixelValue,
617    /// Horizontal padding inside the titlebar
618    pub padding_horizontal: OptionPixelValue,
619    /// Safe area insets for notched/rounded displays
620    pub safe_area: SafeAreaInsets,
621    /// Title text font (from `SystemFonts::title_font`)
622    pub title_font: OptionString,
623    /// Title text font size
624    pub title_font_size: OptionF32,
625    /// Title text font weight (400 = normal, 600 = semibold, 700 = bold)
626    pub title_font_weight: OptionU16,
627}
628
629impl Default for TitlebarMetrics {
630    fn default() -> Self {
631        Self {
632            button_side: TitlebarButtonSide::Right,
633            buttons: TitlebarButtons::default(),
634            // None = "not detected", like title_font above: SystemMetrics::default()
635            // must be able to represent an unknown titlebar so PixelValueOrSystem's
636            // resolve() falls back to system detection instead of these hardcoded
637            // guesses. (The concrete px values pinned the fallback path unreachable.)
638            height: OptionPixelValue::None,
639            button_area_width: OptionPixelValue::None,
640            padding_horizontal: OptionPixelValue::None,
641            safe_area: SafeAreaInsets::default(),
642            title_font: OptionString::None,
643            title_font_size: OptionF32::Some(13.0),
644            title_font_weight: OptionU16::Some(600), // Semibold
645        }
646    }
647}
648
649impl TitlebarMetrics {
650    /// Windows-style titlebar (buttons on right)
651    #[must_use] pub fn windows() -> Self {
652        Self {
653            button_side: TitlebarButtonSide::Right,
654            buttons: TitlebarButtons {
655                has_close: true,
656                has_minimize: true,
657                has_maximize: true,
658                has_fullscreen: false,
659            },
660            height: OptionPixelValue::Some(PixelValue::px(32.0)),
661            button_area_width: OptionPixelValue::Some(PixelValue::px(138.0)), // 3 buttons * 46px
662            padding_horizontal: OptionPixelValue::Some(PixelValue::px(8.0)),
663            safe_area: SafeAreaInsets::default(),
664            title_font: OptionString::Some("Segoe UI Variable Text".into()),
665            title_font_size: OptionF32::Some(12.0),
666            title_font_weight: OptionU16::Some(400), // Normal
667        }
668    }
669    
670    /// macOS-style titlebar (buttons on left, "traffic lights")
671    #[must_use] pub fn macos() -> Self {
672        Self {
673            button_side: TitlebarButtonSide::Left,
674            buttons: TitlebarButtons {
675                has_close: true,
676                has_minimize: true,
677                has_maximize: false, // macOS has fullscreen instead
678                has_fullscreen: true,
679            },
680            height: OptionPixelValue::Some(PixelValue::px(28.0)),
681            button_area_width: OptionPixelValue::Some(PixelValue::px(78.0)), // 3 buttons with gaps
682            padding_horizontal: OptionPixelValue::Some(PixelValue::px(8.0)),
683            safe_area: SafeAreaInsets::default(),
684            title_font: OptionString::Some(".SF NS".into()),
685            title_font_size: OptionF32::Some(13.0),
686            title_font_weight: OptionU16::Some(600), // Semibold
687        }
688    }
689    
690    /// Linux GNOME-style titlebar (buttons on right by default)
691    #[must_use] pub fn linux_gnome() -> Self {
692        Self {
693            button_side: TitlebarButtonSide::Right, // Default, can be changed in settings
694            buttons: TitlebarButtons {
695                has_close: true,
696                has_minimize: true,
697                has_maximize: true,
698                has_fullscreen: false,
699            },
700            height: OptionPixelValue::Some(PixelValue::px(35.0)),
701            button_area_width: OptionPixelValue::Some(PixelValue::px(100.0)),
702            padding_horizontal: OptionPixelValue::Some(PixelValue::px(12.0)),
703            safe_area: SafeAreaInsets::default(),
704            title_font: OptionString::Some("Cantarell".into()),
705            title_font_size: OptionF32::Some(11.0),
706            title_font_weight: OptionU16::Some(700), // Bold
707        }
708    }
709    
710    /// iOS-style safe area (for notched devices)
711    #[must_use] pub fn ios() -> Self {
712        Self {
713            button_side: TitlebarButtonSide::Left,
714            buttons: TitlebarButtons {
715                has_close: false, // iOS apps don't have close buttons
716                has_minimize: false,
717                has_maximize: false,
718                has_fullscreen: false,
719            },
720            height: OptionPixelValue::Some(PixelValue::px(44.0)),
721            button_area_width: OptionPixelValue::Some(PixelValue::px(0.0)),
722            padding_horizontal: OptionPixelValue::Some(PixelValue::px(16.0)),
723            safe_area: SafeAreaInsets {
724                // iPhone notch safe area
725                top: OptionPixelValue::Some(PixelValue::px(47.0)),
726                bottom: OptionPixelValue::Some(PixelValue::px(34.0)),
727                left: OptionPixelValue::None,
728                right: OptionPixelValue::None,
729            },
730            title_font: OptionString::Some(".SFUI-Semibold".into()),
731            title_font_size: OptionF32::Some(17.0),
732            title_font_weight: OptionU16::Some(600),
733        }
734    }
735    
736    /// Android-style titlebar (action bar)
737    #[must_use] pub fn android() -> Self {
738        Self {
739            button_side: TitlebarButtonSide::Left, // Back button on left
740            buttons: TitlebarButtons {
741                has_close: false,
742                has_minimize: false,
743                has_maximize: false,
744                has_fullscreen: false,
745            },
746            height: OptionPixelValue::Some(PixelValue::px(56.0)),
747            button_area_width: OptionPixelValue::Some(PixelValue::px(48.0)), // Back button
748            padding_horizontal: OptionPixelValue::Some(PixelValue::px(16.0)),
749            safe_area: SafeAreaInsets::default(),
750            title_font: OptionString::Some("Roboto Medium".into()),
751            title_font_size: OptionF32::Some(20.0),
752            title_font_weight: OptionU16::Some(500),
753        }
754    }
755}
756
757// ── Input interaction metrics ────────────────────────────────────────────
758
759/// Input interaction timing and distance thresholds from the OS.
760///
761/// These values are queried from the operating system to match the user's
762/// configured double-click speed, drag sensitivity, caret blink rate, etc.
763///
764/// # Platform APIs
765/// - **macOS:** `NSEvent.doubleClickInterval`
766/// - **Windows:** `GetDoubleClickTime()`, `GetSystemMetrics(SM_CXDOUBLECLK)`,
767///   `GetCaretBlinkTime()`, `SystemParametersInfo(SPI_GETWHEELSCROLLLINES)`
768/// - **Linux:** XDG Desktop Portal / gsettings
769#[derive(Debug, Clone, Copy, PartialEq)]
770#[repr(C)]
771pub struct InputMetrics {
772    /// Max milliseconds between clicks to register a double-click.
773    pub double_click_time_ms: u32,
774    /// Max pixels the mouse can move between clicks and still count.
775    pub double_click_distance_px: f32,
776    /// Pixels the mouse must move while held down before a drag starts.
777    pub drag_threshold_px: f32,
778    /// Caret blink rate in milliseconds (0 = no blink).
779    pub caret_blink_rate_ms: u32,
780    /// Width of the text caret/cursor in pixels (typically 1–2).
781    pub caret_width_px: f32,
782    /// Lines to scroll per mouse wheel notch.
783    pub wheel_scroll_lines: u32,
784    /// Milliseconds to wait before a hover triggers (e.g. tooltip delay).
785    /// Windows: `SystemParametersInfo(SPI_GETMOUSEHOVERTIME)` — default 400.
786    pub hover_time_ms: u32,
787}
788
789impl Default for InputMetrics {
790    fn default() -> Self {
791        Self {
792            double_click_time_ms: 500,
793            double_click_distance_px: 4.0,
794            drag_threshold_px: 5.0,
795            caret_blink_rate_ms: 530,
796            caret_width_px: 1.0,
797            wheel_scroll_lines: 3,
798            hover_time_ms: 400,
799        }
800    }
801}
802
803// ── Text rendering hints ─────────────────────────────────────────────────
804
805/// Subpixel rendering layout for font smoothing.
806#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
807#[repr(C)]
808pub enum SubpixelType {
809    /// No subpixel rendering (grayscale anti-aliasing only).
810    #[default]
811    None,
812    /// Horizontal RGB subpixel layout (most common for LCD monitors).
813    Rgb,
814    /// Horizontal BGR subpixel layout.
815    Bgr,
816    /// Vertical RGB subpixel layout.
817    VRgb,
818    /// Vertical BGR subpixel layout.
819    VBgr,
820}
821
822/// Text rendering configuration from the OS.
823///
824/// These hints allow the framework to match the host's font smoothing
825/// settings for crisp, consistent text rendering.
826#[derive(Debug, Clone, Copy, PartialEq, Eq)]
827#[repr(C)]
828pub struct TextRenderingHints {
829    /// Subpixel rendering type.
830    pub subpixel_type: SubpixelType,
831    /// Font smoothing gamma (1000 = default, higher = more contrast).
832    pub font_smoothing_gamma: u32,
833    /// Whether font smoothing (anti-aliasing) is enabled.
834    pub font_smoothing_enabled: bool,
835    /// User prefers increased text contrast.
836    pub increased_contrast: bool,
837}
838
839impl Default for TextRenderingHints {
840    fn default() -> Self {
841        Self {
842            subpixel_type: SubpixelType::None,
843            font_smoothing_gamma: 1000,
844            font_smoothing_enabled: true,
845            increased_contrast: false,
846        }
847    }
848}
849
850// ── Focus ring visuals ───────────────────────────────────────────────────
851
852/// Focus ring / indicator visual style.
853///
854/// When an element receives keyboard focus the OS typically draws a visible
855/// ring or border.  These values come from the OS preferences.
856#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
857#[repr(C)]
858pub struct FocusVisuals {
859    /// Focus ring / indicator colour.
860    /// macOS: `NSColor.keyboardFocusIndicatorColor`
861    pub focus_ring_color: OptionColorU,
862    /// Width of focus border / ring.
863    /// Windows: `SystemParametersInfo(SPI_GETFOCUSBORDERWIDTH)`
864    pub focus_border_width: OptionPixelValue,
865    /// Height of focus border / ring.
866    pub focus_border_height: OptionPixelValue,
867}
868
869// ── Scrollbar preferences ────────────────────────────────────────────────
870
871/// When scrollbars should be shown (OS-level preference).
872#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
873#[repr(C)]
874pub enum ScrollbarVisibility {
875    /// Always show scrollbars.
876    Always,
877    /// Show only while scrolling, then fade out.
878    #[default]
879    WhenScrolling,
880    /// Automatic: depends on input device (trackpad → overlay, mouse → always).
881    Automatic,
882}
883
884/// What happens when clicking the scrollbar track area.
885#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
886#[repr(C)]
887pub enum ScrollbarTrackClick {
888    /// Jump to the clicked position.
889    JumpToPosition,
890    /// Scroll by one page.
891    #[default]
892    PageUpDown,
893}
894
895/// OS-level scrollbar behaviour preferences.
896///
897/// These are separate from the CSS scrollbar *appearance* (`ComputedScrollbarStyle`).
898/// They control *when* scrollbars appear and *how* clicking the track behaves.
899#[derive(Debug, Clone, Copy, PartialEq, Eq)]
900#[repr(C)]
901pub struct ScrollbarPreferences {
902    /// How scrollbars should be shown.
903    /// macOS: `NSScroller.preferredScrollerStyle`
904    pub visibility: ScrollbarVisibility,
905    /// What happens when clicking the scrollbar track.
906    pub track_click: ScrollbarTrackClick,
907}
908
909impl Default for ScrollbarPreferences {
910    fn default() -> Self {
911        Self {
912            visibility: ScrollbarVisibility::WhenScrolling,
913            track_click: ScrollbarTrackClick::PageUpDown,
914        }
915    }
916}
917
918// ── Linux-specific customisation ─────────────────────────────────────────
919
920/// Linux-specific customisation settings.
921///
922/// Read from GTK / KDE / XDG settings on Linux; `Default` (all `None` / 0)
923/// on other platforms.
924#[derive(Debug, Default, Clone, PartialEq, Eq)]
925#[repr(C)]
926pub struct LinuxCustomization {
927    /// GTK theme name (e.g. "Adwaita", "Breeze", "Numix").
928    pub gtk_theme: OptionString,
929    /// Icon theme name (e.g. "Papirus", "Numix", "Breeze").
930    pub icon_theme: OptionString,
931    /// Cursor theme name (e.g. "`Breeze_Snow`", "DMZ-Black").
932    pub cursor_theme: OptionString,
933    /// Cursor size in pixels (0 = unset / use OS default).
934    pub cursor_size: u32,
935    /// GTK button layout string (e.g. "close,minimize,maximize:menu").
936    /// Determines button side and order for CSD titlebars on Linux.
937    pub titlebar_button_layout: OptionString,
938}
939
940// ── Visual hints (icons in menus / buttons / toolbar style) ──────────────
941
942/// Toolbar display style (icons, text, or both).
943#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
944#[repr(C)]
945pub enum ToolbarStyle {
946    /// Show only icons in toolbars.
947    #[default]
948    IconsOnly,
949    /// Show only text labels in toolbars.
950    TextOnly,
951    /// Show text beside the icon (horizontal).
952    TextBesideIcon,
953    /// Show text below the icon (vertical).
954    TextBelowIcon,
955}
956
957/// Visual hints from the OS about how icons and decorations should be shown.
958///
959/// These preferences differ heavily between Linux desktops (KDE vs GNOME)
960/// and are less configurable on macOS / Windows where HIG rules apply.
961#[derive(Debug, Clone, Copy, PartialEq, Eq)]
962#[repr(C)]
963pub struct VisualHints {
964    /// Toolbar display style.
965    /// Linux: `org.gnome.desktop.interface toolbar-style`, KDE `ToolButtonStyle`.
966    pub toolbar_style: ToolbarStyle,
967    /// Show icons on push buttons?  (Common in KDE, rare in Win/Mac.)
968    /// Linux: `org.gnome.desktop.interface buttons-have-icons`, KDE `ShowIconsOnPushButtons`.
969    pub show_button_images: bool,
970    /// Show icons in context menus?  (GNOME defaults off since 3.x; Win/Mac/KDE usually on.)
971    /// Linux: `org.gnome.desktop.interface menus-have-icons`.
972    pub show_menu_images: bool,
973    /// Should tooltips be shown on hover?
974    pub show_tooltips: bool,
975    /// Flash the window taskbar entry on alert?
976    pub flash_on_alert: bool,
977}
978
979impl Default for VisualHints {
980    fn default() -> Self {
981        Self {
982            toolbar_style: ToolbarStyle::IconsOnly,
983            show_button_images: false,
984            show_menu_images: true,
985            show_tooltips: true,
986            flash_on_alert: true,
987        }
988    }
989}
990
991// ── Animation metrics ────────────────────────────────────────────────────
992
993/// Focus indicator behaviour (always visible vs keyboard-only).
994#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
995#[repr(C)]
996pub enum FocusBehavior {
997    /// Focus indicators are always visible when an element has focus.
998    #[default]
999    AlwaysVisible,
1000    /// Focus indicators are hidden until the user presses a keyboard key
1001    /// (Alt, Tab, arrow keys, etc.).  Windows: `SPI_GETKEYBOARDCUES`.
1002    KeyboardOnly,
1003}
1004
1005/// Animation-related preferences from the OS.
1006///
1007/// These control whether UI animations (transitions, fades, slides) should
1008/// play and at what speed.
1009///
1010/// # Platform APIs
1011/// - **Windows:** `SystemParametersInfo(SPI_GETCLIENTAREAANIMATION)`,
1012///   `SPI_GETKEYBOARDCUES`
1013/// - **macOS:** `NSWorkspace.accessibilityDisplayShouldReduceMotion`
1014/// - **Linux:** `org.gnome.desktop.interface enable-animations`,
1015///   KDE `AnimationDurationFactor`
1016#[derive(Debug, Clone, Copy, PartialEq)]
1017#[repr(C)]
1018pub struct AnimationMetrics {
1019    /// Global enable/disable for UI animations.
1020    pub animations_enabled: bool,
1021    /// Animation speed factor (1.0 = normal, 0.5 = 2× faster, 2.0 = 2× slower).
1022    /// Primarily used in KDE.
1023    pub animation_duration_factor: f32,
1024    /// When to show focus rectangles / rings.
1025    pub focus_indicator_behavior: FocusBehavior,
1026}
1027
1028impl Default for AnimationMetrics {
1029    fn default() -> Self {
1030        Self {
1031            animations_enabled: true,
1032            animation_duration_factor: 1.0,
1033            focus_indicator_behavior: FocusBehavior::AlwaysVisible,
1034        }
1035    }
1036}
1037
1038// ── Audio metrics ────────────────────────────────────────────────────────
1039
1040/// Audio-feedback preferences from the OS.
1041///
1042/// Controls whether the app should make sounds on events (error pings,
1043/// notifications) or on input (clicks, key presses).
1044///
1045/// # Platform APIs
1046/// - **Windows:** `SystemParametersInfo(SPI_GETBEEP)`
1047/// - **macOS:** `NSSound.soundEffectAudioVolume`
1048/// - **Linux:** `org.gnome.desktop.sound event-sounds`,
1049///   `org.gnome.desktop.sound input-feedback-sounds`
1050#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1051#[repr(C)]
1052pub struct AudioMetrics {
1053    /// Should the app make sounds on events?  (Error ping, notification, etc.)
1054    pub event_sounds_enabled: bool,
1055    /// Should the app make sounds on input?  (Clicks, typing feedback.)
1056    pub input_feedback_sounds_enabled: bool,
1057}
1058
1059impl Default for AudioMetrics {
1060    fn default() -> Self {
1061        Self {
1062            event_sounds_enabled: true,
1063            input_feedback_sounds_enabled: false,
1064        }
1065    }
1066}
1067
1068/// Apple system font family names for font fallback chains.
1069/// 
1070/// These are the canonical names for Apple's system fonts, which should
1071/// be used in font fallback chains for proper rendering on Apple platforms.
1072/// Note: The names here must match what rust-fontconfig indexes from the font metadata.
1073pub mod apple_fonts {
1074    /// System Font - Primary system font for macOS
1075    /// This is how rust-fontconfig indexes the SF Pro font family
1076    pub const SYSTEM_FONT: &str = "System Font";
1077    
1078    /// SF NS variants as indexed by rust-fontconfig
1079    pub const SF_NS_ROUNDED: &str = "SF NS Rounded";
1080    
1081    /// SF Compact - System font optimized for watchOS
1082    /// Optimized for small sizes and narrow columns
1083    pub const SF_COMPACT: &str = "SF Compact";
1084    
1085    /// SF Mono - Monospaced font used in Xcode
1086    /// Enables alignment between rows and columns of text
1087    pub const SF_MONO: &str = "SF NS Mono Light";
1088    
1089    /// New York - Serif font for reading
1090    /// Performs as traditional reading face at small sizes
1091    pub const NEW_YORK: &str = "New York";
1092    
1093    /// SF Arabic - Arabic system font
1094    pub const SF_ARABIC: &str = "SF Arabic";
1095    
1096    /// SF Armenian - Armenian system font
1097    pub const SF_ARMENIAN: &str = "SF Armenian";
1098    
1099    /// SF Georgian - Georgian system font
1100    pub const SF_GEORGIAN: &str = "SF Georgian";
1101    
1102    /// SF Hebrew - Hebrew system font with niqqud support
1103    pub const SF_HEBREW: &str = "SF Hebrew";
1104    
1105    /// Legacy macOS fonts for fallback
1106    pub const MENLO: &str = "Menlo";
1107    pub const MENLO_REGULAR: &str = "Menlo Regular";
1108    pub const MENLO_BOLD: &str = "Menlo Bold";
1109    pub const MONACO: &str = "Monaco";
1110    pub const LUCIDA_GRANDE: &str = "Lucida Grande";
1111    pub const LUCIDA_GRANDE_BOLD: &str = "Lucida Grande Bold";
1112    pub const HELVETICA_NEUE: &str = "Helvetica Neue";
1113    pub const HELVETICA_NEUE_BOLD: &str = "Helvetica Neue Bold";
1114}
1115
1116/// Windows system font family names.
1117pub mod windows_fonts {
1118    /// Modern Windows 11 fonts
1119    pub const SEGOE_UI_VARIABLE: &str = "Segoe UI Variable";
1120    pub const SEGOE_UI_VARIABLE_TEXT: &str = "Segoe UI Variable Text";
1121    pub const SEGOE_UI_VARIABLE_DISPLAY: &str = "Segoe UI Variable Display";
1122    
1123    /// Standard Windows fonts
1124    pub const SEGOE_UI: &str = "Segoe UI";
1125    pub const CONSOLAS: &str = "Consolas";
1126    pub const CASCADIA_CODE: &str = "Cascadia Code";
1127    pub const CASCADIA_MONO: &str = "Cascadia Mono";
1128    
1129    /// Legacy Windows fonts
1130    pub const TAHOMA: &str = "Tahoma";
1131    pub const MS_SANS_SERIF: &str = "MS Sans Serif";
1132    pub const LUCIDA_CONSOLE: &str = "Lucida Console";
1133    pub const COURIER_NEW: &str = "Courier New";
1134}
1135
1136/// Linux/GTK common font family names.
1137pub mod linux_fonts {
1138    /// GNOME default fonts
1139    pub const CANTARELL: &str = "Cantarell";
1140    pub const ADWAITA: &str = "Adwaita";
1141    
1142    /// Ubuntu fonts
1143    pub const UBUNTU: &str = "Ubuntu";
1144    pub const UBUNTU_MONO: &str = "Ubuntu Mono";
1145    
1146    /// `DejaVu` fonts (widely available)
1147    pub const DEJAVU_SANS: &str = "DejaVu Sans";
1148    pub const DEJAVU_SANS_MONO: &str = "DejaVu Sans Mono";
1149    pub const DEJAVU_SERIF: &str = "DejaVu Serif";
1150    
1151    /// Liberation fonts (metrically compatible with Windows fonts)
1152    pub const LIBERATION_SANS: &str = "Liberation Sans";
1153    pub const LIBERATION_MONO: &str = "Liberation Mono";
1154    pub const LIBERATION_SERIF: &str = "Liberation Serif";
1155    
1156    /// Noto fonts (broad Unicode coverage)
1157    pub const NOTO_SANS: &str = "Noto Sans";
1158    pub const NOTO_MONO: &str = "Noto Sans Mono";
1159    pub const NOTO_SERIF: &str = "Noto Serif";
1160    
1161    /// KDE default fonts
1162    pub const HACK: &str = "Hack";
1163    
1164    /// Generic fallback names
1165    pub const MONOSPACE: &str = "Monospace";
1166    pub const SANS_SERIF: &str = "Sans";
1167    pub const SERIF: &str = "Serif";
1168}
1169
1170impl SystemFontType {
1171    /// Returns the font fallback chain for this font type on the given platform.
1172    /// 
1173    /// The returned list contains font family names in order of preference.
1174    /// The first available font should be used.
1175    #[must_use] pub fn get_fallback_chain(&self, platform: &Platform) -> Vec<&'static str> {
1176        match platform {
1177            Platform::MacOs | Platform::Ios => self.macos_fallback_chain(),
1178            Platform::Windows => self.windows_fallback_chain(),
1179            Platform::Linux(_) => self.linux_fallback_chain(),
1180            Platform::Android => self.android_fallback_chain(),
1181            Platform::Unknown => self.generic_fallback_chain(),
1182        }
1183    }
1184    
1185    fn macos_fallback_chain(self) -> Vec<&'static str> {
1186        match self {
1187            // Normal weight: System Font first, then Helvetica Neue.
1188            Self::Ui => vec![
1189                apple_fonts::SYSTEM_FONT,
1190                apple_fonts::HELVETICA_NEUE,
1191                apple_fonts::LUCIDA_GRANDE,
1192            ],
1193            // Bold weights: Helvetica Neue first (System Font has no Bold variant in fontconfig).
1194            Self::UiBold | Self::TitleBold => vec![
1195                apple_fonts::HELVETICA_NEUE,
1196                apple_fonts::LUCIDA_GRANDE,
1197            ],
1198            // Monospace: Menlo (has a Bold variant), then Monaco.
1199            Self::Monospace | Self::MonospaceBold | Self::MonospaceItalic => vec![
1200                apple_fonts::MENLO,
1201                apple_fonts::MONACO,
1202            ],
1203            // Title / Menu / Small: System Font then Helvetica Neue.
1204            Self::Title | Self::Menu | Self::Small => vec![
1205                apple_fonts::SYSTEM_FONT,
1206                apple_fonts::HELVETICA_NEUE,
1207            ],
1208            // Serif fonts - Georgia has bold variant
1209            Self::Serif => vec![
1210                apple_fonts::NEW_YORK,
1211                "Georgia",
1212                "Times New Roman",
1213            ],
1214            Self::SerifBold => vec![
1215                "Georgia", // Georgia Bold exists
1216                "Times New Roman",
1217            ],
1218        }
1219    }
1220    
1221    fn windows_fallback_chain(self) -> Vec<&'static str> {
1222        match self {
1223            Self::Ui | Self::UiBold => vec![
1224                windows_fonts::SEGOE_UI_VARIABLE_TEXT,
1225                windows_fonts::SEGOE_UI,
1226                windows_fonts::TAHOMA,
1227            ],
1228            Self::Monospace | Self::MonospaceBold | Self::MonospaceItalic => vec![
1229                windows_fonts::CASCADIA_MONO,
1230                windows_fonts::CASCADIA_CODE,
1231                windows_fonts::CONSOLAS,
1232                windows_fonts::LUCIDA_CONSOLE,
1233                windows_fonts::COURIER_NEW,
1234            ],
1235            Self::Title | Self::TitleBold => vec![
1236                windows_fonts::SEGOE_UI_VARIABLE_DISPLAY,
1237                windows_fonts::SEGOE_UI,
1238            ],
1239            Self::Menu => vec![
1240                windows_fonts::SEGOE_UI,
1241                windows_fonts::TAHOMA,
1242            ],
1243            Self::Small => vec![
1244                windows_fonts::SEGOE_UI,
1245            ],
1246            Self::Serif | Self::SerifBold => vec![
1247                "Cambria",
1248                "Georgia",
1249                "Times New Roman",
1250            ],
1251        }
1252    }
1253    
1254    fn linux_fallback_chain(self) -> Vec<&'static str> {
1255        match self {
1256            Self::Ui | Self::UiBold => vec![
1257                linux_fonts::CANTARELL,
1258                linux_fonts::UBUNTU,
1259                linux_fonts::NOTO_SANS,
1260                linux_fonts::DEJAVU_SANS,
1261                linux_fonts::LIBERATION_SANS,
1262                linux_fonts::SANS_SERIF,
1263            ],
1264            Self::Monospace | Self::MonospaceBold | Self::MonospaceItalic => vec![
1265                linux_fonts::UBUNTU_MONO,
1266                linux_fonts::HACK,
1267                linux_fonts::NOTO_MONO,
1268                linux_fonts::DEJAVU_SANS_MONO,
1269                linux_fonts::LIBERATION_MONO,
1270                linux_fonts::MONOSPACE,
1271            ],
1272            Self::Title | Self::TitleBold | Self::Menu | Self::Small => vec![
1273                linux_fonts::CANTARELL,
1274                linux_fonts::UBUNTU,
1275                linux_fonts::NOTO_SANS,
1276            ],
1277            Self::Serif | Self::SerifBold => vec![
1278                linux_fonts::NOTO_SERIF,
1279                linux_fonts::DEJAVU_SERIF,
1280                linux_fonts::LIBERATION_SERIF,
1281                linux_fonts::SERIF,
1282            ],
1283        }
1284    }
1285    
1286    fn android_fallback_chain(self) -> Vec<&'static str> {
1287        match self {
1288            Self::Ui | Self::UiBold | Self::Title | Self::TitleBold => vec!["Roboto", "Noto Sans"],
1289            Self::Monospace | Self::MonospaceBold | Self::MonospaceItalic => {
1290                vec!["Roboto Mono", "Droid Sans Mono", "monospace"]
1291            }
1292            Self::Menu | Self::Small => vec!["Roboto"],
1293            Self::Serif | Self::SerifBold => vec!["Noto Serif", "Droid Serif", "serif"],
1294        }
1295    }
1296    
1297    fn generic_fallback_chain(self) -> Vec<&'static str> {
1298        match self {
1299            Self::Ui | Self::UiBold | Self::Title | Self::TitleBold | Self::Menu | Self::Small => {
1300                vec!["sans-serif"]
1301            }
1302            Self::Monospace | Self::MonospaceBold | Self::MonospaceItalic => {
1303                vec!["monospace"]
1304            }
1305            Self::Serif | Self::SerifBold => vec!["serif"],
1306        }
1307    }
1308}
1309
1310impl SystemStyle {
1311
1312    /// Format the `SystemStyle` as a human-readable JSON string for debugging.
1313    ///
1314    /// This does NOT use serde — it manually formats the most important fields
1315    /// so that they can be verified against OS-reported values in a test script.
1316    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
1317    #[must_use] pub fn to_json_string(&self) -> AzString {
1318        use alloc::format;
1319
1320        fn opt_color(c: OptionColorU) -> alloc::string::String {
1321            c.as_ref().map_or_else(
1322                || "null".into(),
1323                |c| format!("\"#{:02x}{:02x}{:02x}{:02x}\"", c.r, c.g, c.b, c.a),
1324            )
1325        }
1326        fn opt_str(s: &OptionString) -> alloc::string::String {
1327            s.as_ref()
1328                .map_or_else(|| "null".into(), |s| format!("\"{}\"", s.as_str()))
1329        }
1330        fn opt_f32(v: OptionF32) -> alloc::string::String {
1331            v.into_option()
1332                .map_or_else(|| "null".into(), |v| format!("{v:.2}"))
1333        }
1334        fn opt_u16(v: OptionU16) -> alloc::string::String {
1335            v.into_option()
1336                .map_or_else(|| "null".into(), |v| format!("{v}"))
1337        }
1338        fn opt_px(v: &OptionPixelValue) -> alloc::string::String {
1339            v.as_ref().map_or_else(
1340                || "null".into(),
1341                |v| format!("{:.1}", v.to_pixels_internal(0.0, 0.0, 0.0)),
1342            )
1343        }
1344
1345        let tm = &self.metrics.titlebar;
1346        let inp = &self.input;
1347        let tr = &self.text_rendering;
1348        let acc = &self.accessibility;
1349        let sp = &self.scrollbar_preferences;
1350        let lnx = &self.linux;
1351        let vh = &self.visual_hints;
1352        let anim = &self.animation;
1353        let audio = &self.audio;
1354
1355        let json = format!(
1356r#"{{
1357  "theme": "{:?}",
1358  "platform": "{:?}",
1359  "os_version": "{:?}:{}",
1360  "language": "{}",
1361  "prefers_reduced_motion": {:?},
1362  "prefers_high_contrast": {:?},
1363  "colors": {{
1364    "text": {},
1365    "secondary_text": {},
1366    "tertiary_text": {},
1367    "background": {},
1368    "accent": {},
1369    "accent_text": {},
1370    "button_face": {},
1371    "button_text": {},
1372    "disabled_text": {},
1373    "window_background": {},
1374    "under_page_background": {},
1375    "selection_background": {},
1376    "selection_text": {},
1377    "selection_background_inactive": {},
1378    "selection_text_inactive": {},
1379    "link": {},
1380    "separator": {},
1381    "grid": {},
1382    "find_highlight": {},
1383    "sidebar_background": {},
1384    "sidebar_selection": {}
1385  }},
1386  "fonts": {{
1387    "ui_font": {},
1388    "ui_font_size": {},
1389    "monospace_font": {},
1390    "title_font": {},
1391    "menu_font": {},
1392    "small_font": {}
1393  }},
1394  "titlebar": {{
1395    "button_side": "{:?}",
1396    "height": {},
1397    "button_area_width": {},
1398    "padding_horizontal": {},
1399    "title_font": {},
1400    "title_font_size": {},
1401    "title_font_weight": {},
1402    "has_close": {},
1403    "has_minimize": {},
1404    "has_maximize": {},
1405    "has_fullscreen": {}
1406  }},
1407  "input": {{
1408    "double_click_time_ms": {},
1409    "double_click_distance_px": {:.1},
1410    "drag_threshold_px": {:.1},
1411    "caret_blink_rate_ms": {},
1412    "caret_width_px": {:.1},
1413    "wheel_scroll_lines": {},
1414    "hover_time_ms": {}
1415  }},
1416  "text_rendering": {{
1417    "font_smoothing_enabled": {},
1418    "subpixel_type": "{:?}",
1419    "font_smoothing_gamma": {},
1420    "increased_contrast": {}
1421  }},
1422  "accessibility": {{
1423    "prefers_bold_text": {},
1424    "prefers_larger_text": {},
1425    "text_scale_factor": {:.2},
1426    "prefers_high_contrast": {},
1427    "prefers_reduced_motion": {},
1428    "prefers_reduced_transparency": {},
1429    "screen_reader_active": {},
1430    "differentiate_without_color": {}
1431  }},
1432  "scrollbar_preferences": {{
1433    "visibility": "{:?}",
1434    "track_click": "{:?}"
1435  }},
1436  "linux": {{
1437    "gtk_theme": {},
1438    "icon_theme": {},
1439    "cursor_theme": {},
1440    "cursor_size": {},
1441    "titlebar_button_layout": {}
1442  }},
1443  "visual_hints": {{
1444    "show_button_images": {},
1445    "show_menu_images": {},
1446    "toolbar_style": "{:?}",
1447    "show_tooltips": {}
1448  }},
1449  "animation": {{
1450    "animations_enabled": {},
1451    "animation_duration_factor": {:.2},
1452    "focus_indicator_behavior": "{:?}"
1453  }},
1454  "audio": {{
1455    "event_sounds_enabled": {},
1456    "input_feedback_sounds_enabled": {}
1457  }}
1458}}"#,
1459            // top-level
1460            self.theme,
1461            self.platform,
1462            self.os_version.os, self.os_version.version_id,
1463            self.language.as_str(),
1464            self.prefers_reduced_motion,
1465            self.prefers_high_contrast,
1466            // colors
1467            opt_color(self.colors.text),
1468            opt_color(self.colors.secondary_text),
1469            opt_color(self.colors.tertiary_text),
1470            opt_color(self.colors.background),
1471            opt_color(self.colors.accent),
1472            opt_color(self.colors.accent_text),
1473            opt_color(self.colors.button_face),
1474            opt_color(self.colors.button_text),
1475            opt_color(self.colors.disabled_text),
1476            opt_color(self.colors.window_background),
1477            opt_color(self.colors.under_page_background),
1478            opt_color(self.colors.selection_background),
1479            opt_color(self.colors.selection_text),
1480            opt_color(self.colors.selection_background_inactive),
1481            opt_color(self.colors.selection_text_inactive),
1482            opt_color(self.colors.link),
1483            opt_color(self.colors.separator),
1484            opt_color(self.colors.grid),
1485            opt_color(self.colors.find_highlight),
1486            opt_color(self.colors.sidebar_background),
1487            opt_color(self.colors.sidebar_selection),
1488            // fonts
1489            opt_str(&self.fonts.ui_font),
1490            opt_f32(self.fonts.ui_font_size),
1491            opt_str(&self.fonts.monospace_font),
1492            opt_str(&self.fonts.title_font),
1493            opt_str(&self.fonts.menu_font),
1494            opt_str(&self.fonts.small_font),
1495            // titlebar
1496            tm.button_side,
1497            opt_px(&tm.height),
1498            opt_px(&tm.button_area_width),
1499            opt_px(&tm.padding_horizontal),
1500            opt_str(&tm.title_font),
1501            opt_f32(tm.title_font_size),
1502            opt_u16(tm.title_font_weight),
1503            tm.buttons.has_close,
1504            tm.buttons.has_minimize,
1505            tm.buttons.has_maximize,
1506            tm.buttons.has_fullscreen,
1507            // input
1508            inp.double_click_time_ms,
1509            inp.double_click_distance_px,
1510            inp.drag_threshold_px,
1511            inp.caret_blink_rate_ms,
1512            inp.caret_width_px,
1513            inp.wheel_scroll_lines,
1514            inp.hover_time_ms,
1515            // text_rendering
1516            tr.font_smoothing_enabled,
1517            tr.subpixel_type,
1518            tr.font_smoothing_gamma,
1519            tr.increased_contrast,
1520            // accessibility
1521            acc.prefers_bold_text,
1522            acc.prefers_larger_text,
1523            acc.text_scale_factor,
1524            acc.prefers_high_contrast,
1525            acc.prefers_reduced_motion,
1526            acc.prefers_reduced_transparency,
1527            acc.screen_reader_active,
1528            acc.differentiate_without_color,
1529            // scrollbar_preferences
1530            sp.visibility,
1531            sp.track_click,
1532            // linux
1533            opt_str(&lnx.gtk_theme),
1534            opt_str(&lnx.icon_theme),
1535            opt_str(&lnx.cursor_theme),
1536            lnx.cursor_size,
1537            opt_str(&lnx.titlebar_button_layout),
1538            // visual_hints
1539            vh.show_button_images,
1540            vh.show_menu_images,
1541            vh.toolbar_style,
1542            vh.show_tooltips,
1543            // animation
1544            anim.animations_enabled,
1545            anim.animation_duration_factor,
1546            anim.focus_indicator_behavior,
1547            // audio
1548            audio.event_sounds_enabled,
1549            audio.input_feedback_sounds_enabled,
1550        );
1551
1552        AzString::from(json)
1553    }
1554
1555    /// Returns a platform-appropriate default system style.
1556    ///
1557    /// This returns hard-coded defaults based on the target OS. For actual
1558    /// runtime detection of the user's theme, colors, and fonts, use the
1559    /// platform discovery in `azul-dll` (called automatically by `App::create()`).
1560    #[must_use] pub fn detect() -> Self {
1561        Self::default_for_platform()
1562    }
1563
1564    /// Returns hard-coded defaults for the current compile-time platform.
1565    #[must_use] pub fn default_for_platform() -> Self {
1566        #[cfg(target_os = "windows")]
1567        { defaults::windows_11_light() }
1568        #[cfg(target_os = "macos")]
1569        { defaults::macos_modern_light() }
1570        #[cfg(target_os = "linux")]
1571        { defaults::gnome_adwaita_light() }
1572        #[cfg(target_os = "android")]
1573        { defaults::android_material_light() }
1574        #[cfg(target_os = "ios")]
1575        { defaults::ios_light() }
1576        #[cfg(not(any(
1577            target_os = "linux",
1578            target_os = "windows",
1579            target_os = "macos",
1580            target_os = "android",
1581            target_os = "ios"
1582        )))]
1583        { Self::default() }
1584    }
1585
1586    /// Alias for `detect` - kept for internal compatibility, not exposed in FFI.
1587    #[inline]
1588    #[must_use] pub fn new() -> Self {
1589        Self::detect()
1590    }
1591
1592    /// Create a CSS stylesheet for CSD (Client-Side Decorations) titlebar
1593    ///
1594    /// This generates CSS rules for the CSD titlebar using system colors,
1595    /// fonts, and metrics to match the native platform look. Returned rules
1596    /// carry `rule_priority::SYSTEM`.
1597    #[must_use] pub fn create_csd_stylesheet(&self) -> Css {
1598        use alloc::format;
1599
1600        use crate::parser2::new_from_str;
1601
1602        // Build CSS string from SystemStyle
1603        let mut css = String::new();
1604
1605        // Get system colors with fallbacks
1606        let bg_color = self
1607            .colors
1608            .window_background
1609            .as_option()
1610            .copied()
1611            .unwrap_or(ColorU::new_rgb(240, 240, 240));
1612        let text_color = self
1613            .colors
1614            .text
1615            .as_option()
1616            .copied()
1617            .unwrap_or(ColorU::new_rgb(0, 0, 0));
1618        let accent_color = self
1619            .colors
1620            .accent
1621            .as_option()
1622            .copied()
1623            .unwrap_or(ColorU::new_rgb(0, 120, 215));
1624        let border_color = match self.theme {
1625            Theme::Dark => ColorU::new_rgb(60, 60, 60),
1626            Theme::Light => ColorU::new_rgb(200, 200, 200),
1627        };
1628
1629        // Get system metrics with fallbacks
1630        let corner_radius = self
1631            .metrics
1632            .corner_radius
1633            .map(|px| {
1634                use crate::props::basic::pixel::DEFAULT_FONT_SIZE;
1635                format!("{}px", px.to_pixels_internal(1.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE))
1636            })
1637            .unwrap_or_else(|| "4px".to_string());
1638
1639        // Titlebar container
1640        let _ = write!(css,
1641            ".csd-titlebar {{ width: 100%; height: 32px; background: rgb({}, {}, {}); \
1642             border-bottom: 1px solid rgb({}, {}, {}); display: flex; flex-direction: row; \
1643             align-items: center; justify-content: space-between; padding: 0 8px; \
1644             cursor: grab; user-select: none; }} ",
1645            bg_color.r, bg_color.g, bg_color.b, border_color.r, border_color.g, border_color.b,
1646        );
1647
1648        // Title text
1649        let _ = write!(css,
1650            ".csd-title {{ color: rgb({}, {}, {}); font-size: 13px; flex-grow: 1; text-align: \
1651             center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; \
1652             user-select: none; }} ",
1653            text_color.r, text_color.g, text_color.b,
1654        );
1655
1656        // Button container
1657        css.push_str(".csd-buttons { display: flex; flex-direction: row; gap: 4px; } ");
1658
1659        // Buttons
1660        let _ = write!(css,
1661            ".csd-button {{ width: 32px; height: 24px; border-radius: {}; background: \
1662             transparent; color: rgb({}, {}, {}); font-size: 16px; line-height: 24px; text-align: \
1663             center; cursor: pointer; user-select: none; }} ",
1664            corner_radius, text_color.r, text_color.g, text_color.b,
1665        );
1666
1667        // Button hover state
1668        let hover_color = match self.theme {
1669            Theme::Dark => ColorU::new_rgb(60, 60, 60),
1670            Theme::Light => ColorU::new_rgb(220, 220, 220),
1671        };
1672        let _ = write!(css,
1673            ".csd-button:hover {{ background: rgb({}, {}, {}); }} ",
1674            hover_color.r, hover_color.g, hover_color.b,
1675        );
1676
1677        // Close button hover (red on all platforms)
1678        css.push_str(
1679            ".csd-close:hover { background: rgb(232, 17, 35); color: rgb(255, 255, 255); } ",
1680        );
1681
1682        // Platform-specific button styling
1683        match self.platform {
1684            Platform::MacOs => {
1685                // macOS traffic light buttons (left side)
1686                css.push_str(".csd-buttons { position: absolute; left: 8px; } ");
1687                css.push_str(
1688                    ".csd-close { background: rgb(255, 95, 86); width: 12px; height: 12px; \
1689                     border-radius: 50%; } ",
1690                );
1691                css.push_str(
1692                    ".csd-minimize { background: rgb(255, 189, 46); width: 12px; height: 12px; \
1693                     border-radius: 50%; } ",
1694                );
1695                css.push_str(
1696                    ".csd-maximize { background: rgb(40, 201, 64); width: 12px; height: 12px; \
1697                     border-radius: 50%; } ",
1698                );
1699            }
1700            Platform::Linux(_) => {
1701                // Linux - title on left, buttons on right
1702                css.push_str(".csd-title { text-align: left; } ");
1703            }
1704            _ => {
1705                // Windows and others - standard layout
1706            }
1707        }
1708
1709        // Parse CSS string into a Css.
1710        let (mut parsed_css, _warnings) = new_from_str(&css);
1711        // Tag every rule as system-level so author CSS overrides win.
1712        for rule in parsed_css.rules.as_mut() {
1713            rule.priority = crate::css::rule_priority::SYSTEM;
1714        }
1715        parsed_css
1716    }
1717}
1718
1719/// Detect the Linux desktop environment from environment variables.
1720///
1721/// Checks `XDG_CURRENT_DESKTOP`, `DESKTOP_SESSION`, and specific env markers
1722/// to identify GNOME, KDE, XFCE, Cinnamon, MATE, Hyprland, Sway, i3, etc.
1723#[must_use] pub fn detect_linux_desktop_env() -> DesktopEnvironment {
1724    // Check XDG_CURRENT_DESKTOP first (most reliable)
1725    if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
1726        let desktop_lower = desktop.to_lowercase();
1727        if desktop_lower.contains("gnome") {
1728            return DesktopEnvironment::Gnome;
1729        }
1730        if desktop_lower.contains("kde") || desktop_lower.contains("plasma") {
1731            return DesktopEnvironment::Kde;
1732        }
1733        if desktop_lower.contains("xfce") {
1734            return DesktopEnvironment::Other(AzString::from_const_str("XFCE"));
1735        }
1736        if desktop_lower.contains("unity") {
1737            return DesktopEnvironment::Other(AzString::from_const_str("Unity"));
1738        }
1739        if desktop_lower.contains("cinnamon") {
1740            return DesktopEnvironment::Other(AzString::from_const_str("Cinnamon"));
1741        }
1742        if desktop_lower.contains("mate") {
1743            return DesktopEnvironment::Other(AzString::from_const_str("MATE"));
1744        }
1745        if desktop_lower.contains("lxde") || desktop_lower.contains("lxqt") {
1746            return DesktopEnvironment::Other(AzString::from(desktop.to_uppercase()));
1747        }
1748        if desktop_lower.contains("budgie") {
1749            return DesktopEnvironment::Other(AzString::from_const_str("Budgie"));
1750        }
1751        if desktop_lower.contains("pantheon") {
1752            return DesktopEnvironment::Other(AzString::from_const_str("Pantheon"));
1753        }
1754        if desktop_lower.contains("deepin") {
1755            return DesktopEnvironment::Other(AzString::from_const_str("Deepin"));
1756        }
1757        if desktop_lower.contains("hyprland") {
1758            return DesktopEnvironment::Other(AzString::from_const_str("Hyprland"));
1759        }
1760        if desktop_lower.contains("sway") {
1761            return DesktopEnvironment::Other(AzString::from_const_str("Sway"));
1762        }
1763        if desktop_lower.contains("i3") {
1764            return DesktopEnvironment::Other(AzString::from_const_str("i3"));
1765        }
1766        return DesktopEnvironment::Other(AzString::from(desktop));
1767    }
1768
1769    // Check DESKTOP_SESSION as fallback
1770    if let Ok(session) = std::env::var("DESKTOP_SESSION") {
1771        let session_lower = session.to_lowercase();
1772        if session_lower.contains("gnome") {
1773            return DesktopEnvironment::Gnome;
1774        }
1775        if session_lower.contains("plasma") || session_lower.contains("kde") {
1776            return DesktopEnvironment::Kde;
1777        }
1778        if session_lower.contains("xfce") {
1779            return DesktopEnvironment::Other(AzString::from_const_str("XFCE"));
1780        }
1781        if session_lower.contains("cinnamon") {
1782            return DesktopEnvironment::Other(AzString::from_const_str("Cinnamon"));
1783        }
1784        return DesktopEnvironment::Other(AzString::from(session));
1785    }
1786
1787    // Check for specific environment markers
1788    if std::env::var("GNOME_DESKTOP_SESSION_ID").is_ok() {
1789        return DesktopEnvironment::Gnome;
1790    }
1791    if std::env::var("KDE_FULL_SESSION").is_ok() {
1792        return DesktopEnvironment::Kde;
1793    }
1794    if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() {
1795        return DesktopEnvironment::Other(AzString::from_const_str("Hyprland"));
1796    }
1797    if std::env::var("SWAYSOCK").is_ok() {
1798        return DesktopEnvironment::Other(AzString::from_const_str("Sway"));
1799    }
1800    if std::env::var("I3SOCK").is_ok() {
1801        return DesktopEnvironment::Other(AzString::from_const_str("i3"));
1802    }
1803
1804    DesktopEnvironment::Other(AzString::from_const_str("Unknown"))
1805}
1806
1807/// Detect the system language as a BCP 47 tag.
1808///
1809/// Checks `LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, and `LANG` in priority order.
1810/// Returns `"en-US"` if detection fails. For runtime detection via native
1811/// OS APIs, the platform discovery in `azul-dll` overrides this.
1812#[must_use] pub fn detect_system_language() -> AzString {
1813    let env_vars = ["LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"];
1814    for var in &env_vars {
1815        if let Ok(value) = std::env::var(var) {
1816            let value = value.trim();
1817            if value.is_empty() || value == "C" || value == "POSIX" {
1818                continue;
1819            }
1820            // Parse locale format: "de_DE.UTF-8" or "de_DE" or "de"
1821            let lang = value
1822                .split('.')  // Remove .UTF-8 suffix
1823                .next()
1824                .unwrap_or(value)
1825                .split(':')  // LANGUAGE can be "de:en_US:en"
1826                .next()
1827                .unwrap_or(value);
1828            if !lang.is_empty() {
1829                return AzString::from(lang.replace('_', "-"));
1830            }
1831        }
1832    }
1833    AzString::from_const_str("en-US")
1834}
1835
1836pub mod defaults {
1837    //! A collection of hard-coded system style defaults that mimic the appearance
1838    //! of various operating systems and desktop environments.
1839    //!
1840    //! These are used as a
1841    //! fallback when the "io" feature is disabled, ensuring deterministic styles
1842    //! for testing and environments where system calls are not desired.
1843
1844    use super::{
1845        AccessibilitySettings, AnimationMetrics, AudioMetrics, FocusVisuals, InputMetrics,
1846        LinuxCustomization, ScrollbarPreferences, TextRenderingHints, VisualHints,
1847    };
1848    use crate::{
1849        corety::{AzString, OptionF32, OptionString},
1850        dynamic_selector::{BoolCondition, OsVersion},
1851        props::{
1852            basic::{
1853                color::{ColorU, OptionColorU},
1854                pixel::{PixelValue, OptionPixelValue},
1855            },
1856            layout::{
1857                dimensions::LayoutWidth,
1858                spacing::{LayoutPaddingLeft, LayoutPaddingRight},
1859            },
1860            style::{
1861                background::StyleBackgroundContent,
1862                scrollbar::{
1863                    ComputedScrollbarStyle, OverflowScrolling, OverscrollBehavior, ScrollBehavior,
1864                    ScrollPhysics, ScrollbarInfo,
1865                    SCROLLBAR_ANDROID_DARK, SCROLLBAR_ANDROID_LIGHT, SCROLLBAR_CLASSIC_DARK,
1866                    SCROLLBAR_CLASSIC_LIGHT, SCROLLBAR_IOS_DARK, SCROLLBAR_IOS_LIGHT,
1867                    SCROLLBAR_MACOS_DARK, SCROLLBAR_MACOS_LIGHT, SCROLLBAR_WINDOWS_DARK,
1868                    SCROLLBAR_WINDOWS_LIGHT,
1869                },
1870            },
1871        },
1872        system::{
1873            DesktopEnvironment, Platform, SystemColors, SystemFonts, SystemMetrics, SystemStyle,
1874            Theme, IconStyleOptions, TitlebarMetrics,
1875        },
1876    };
1877
1878    // --- Custom Scrollbar Style Constants for Nostalgia ---
1879
1880    /// A scrollbar style mimicking the classic Windows 95/98/2000/XP look.
1881    pub const SCROLLBAR_WINDOWS_CLASSIC: ScrollbarInfo = ScrollbarInfo {
1882        width: LayoutWidth::Px(PixelValue::const_px(17)),
1883        padding_left: LayoutPaddingLeft {
1884            inner: PixelValue::const_px(0),
1885        },
1886        padding_right: LayoutPaddingRight {
1887            inner: PixelValue::const_px(0),
1888        },
1889        track: StyleBackgroundContent::Color(ColorU {
1890            r: 223,
1891            g: 223,
1892            b: 223,
1893            a: 255,
1894        }), // Scrollbar trough color
1895        thumb: StyleBackgroundContent::Color(ColorU {
1896            r: 208,
1897            g: 208,
1898            b: 208,
1899            a: 255,
1900        }), // Button face color
1901        button: StyleBackgroundContent::Color(ColorU {
1902            r: 208,
1903            g: 208,
1904            b: 208,
1905            a: 255,
1906        }),
1907        corner: StyleBackgroundContent::Color(ColorU {
1908            r: 223,
1909            g: 223,
1910            b: 223,
1911            a: 255,
1912        }),
1913        resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
1914        clip_to_container_border: false,
1915        scroll_behavior: ScrollBehavior::Auto,
1916        overscroll_behavior_x: OverscrollBehavior::None,
1917        overscroll_behavior_y: OverscrollBehavior::None,
1918        overflow_scrolling: OverflowScrolling::Auto,
1919    };
1920
1921    /// A scrollbar style mimicking the macOS "Aqua" theme from the early 2000s.
1922    pub const SCROLLBAR_MACOS_AQUA: ScrollbarInfo = ScrollbarInfo {
1923        width: LayoutWidth::Px(PixelValue::const_px(15)),
1924        padding_left: LayoutPaddingLeft {
1925            inner: PixelValue::const_px(0),
1926        },
1927        padding_right: LayoutPaddingRight {
1928            inner: PixelValue::const_px(0),
1929        },
1930        track: StyleBackgroundContent::Color(ColorU {
1931            r: 238,
1932            g: 238,
1933            b: 238,
1934            a: 128,
1935        }), // Translucent track
1936        thumb: StyleBackgroundContent::Color(ColorU {
1937            r: 105,
1938            g: 173,
1939            b: 255,
1940            a: 255,
1941        }), // "Gel" blue
1942        button: StyleBackgroundContent::Color(ColorU {
1943            r: 105,
1944            g: 173,
1945            b: 255,
1946            a: 255,
1947        }),
1948        corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
1949        resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
1950        clip_to_container_border: true,
1951        scroll_behavior: ScrollBehavior::Smooth,
1952        overscroll_behavior_x: OverscrollBehavior::Auto,
1953        overscroll_behavior_y: OverscrollBehavior::Auto,
1954        overflow_scrolling: OverflowScrolling::Auto,
1955    };
1956
1957    /// A scrollbar style mimicking the KDE Oxygen theme.
1958    pub const SCROLLBAR_KDE_OXYGEN: ScrollbarInfo = ScrollbarInfo {
1959        width: LayoutWidth::Px(PixelValue::const_px(14)),
1960        padding_left: LayoutPaddingLeft {
1961            inner: PixelValue::const_px(2),
1962        },
1963        padding_right: LayoutPaddingRight {
1964            inner: PixelValue::const_px(2),
1965        },
1966        track: StyleBackgroundContent::Color(ColorU {
1967            r: 242,
1968            g: 242,
1969            b: 242,
1970            a: 255,
1971        }),
1972        thumb: StyleBackgroundContent::Color(ColorU {
1973            r: 177,
1974            g: 177,
1975            b: 177,
1976            a: 255,
1977        }),
1978        button: StyleBackgroundContent::Color(ColorU {
1979            r: 216,
1980            g: 216,
1981            b: 216,
1982            a: 255,
1983        }),
1984        corner: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
1985        resizer: StyleBackgroundContent::Color(ColorU::TRANSPARENT),
1986        clip_to_container_border: false,
1987        scroll_behavior: ScrollBehavior::Auto,
1988        overscroll_behavior_x: OverscrollBehavior::Auto,
1989        overscroll_behavior_y: OverscrollBehavior::Auto,
1990        overflow_scrolling: OverflowScrolling::Auto,
1991    };
1992
1993    /// Helper to convert a detailed `ScrollbarInfo` into the simplified `ComputedScrollbarStyle`.
1994    fn scrollbar_info_to_computed(info: &ScrollbarInfo) -> ComputedScrollbarStyle {
1995        ComputedScrollbarStyle {
1996            width: Some(info.width.clone()),
1997            thumb_color: match info.thumb {
1998                StyleBackgroundContent::Color(c) => Some(c),
1999                _ => None,
2000            },
2001            track_color: match info.track {
2002                StyleBackgroundContent::Color(c) => Some(c),
2003                _ => None,
2004            },
2005        }
2006    }
2007
2008    // --- Windows Styles ---
2009
2010    /// Windows 11 light mode defaults (Segoe UI Variable, `WinUI` 3 colors).
2011    #[must_use] pub fn windows_11_light() -> SystemStyle {
2012        SystemStyle {
2013            theme: Theme::Light,
2014            platform: Platform::Windows,
2015            colors: SystemColors {
2016                text: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2017                background: OptionColorU::Some(ColorU::new_rgb(243, 243, 243)),
2018                accent: OptionColorU::Some(ColorU::new_rgb(0, 95, 184)),
2019                window_background: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2020                selection_background: OptionColorU::Some(ColorU::new_rgb(0, 120, 215)),
2021                selection_text: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2022                ..Default::default()
2023            },
2024            fonts: SystemFonts {
2025                ui_font: OptionString::Some("Segoe UI Variable Text".into()),
2026                ui_font_size: OptionF32::Some(9.0),
2027                monospace_font: OptionString::Some("Consolas".into()),
2028                ..Default::default()
2029            },
2030            metrics: SystemMetrics {
2031                corner_radius: OptionPixelValue::Some(PixelValue::px(4.0)),
2032                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2033                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(12.0)),
2034                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(6.0)),
2035                titlebar: TitlebarMetrics::windows(),
2036            },
2037            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_WINDOWS_LIGHT))),
2038            app_specific_stylesheet: None,
2039            run_destructor: true,
2040            icon_style: IconStyleOptions::default(),
2041            language: AzString::from_const_str("en-US"),
2042            os_version: OsVersion::WIN_11,
2043            prefers_reduced_motion: BoolCondition::False,
2044            prefers_high_contrast: BoolCondition::False,
2045            scroll_physics: ScrollPhysics::windows(),
2046            linux: LinuxCustomization::default(),
2047            focus_visuals: FocusVisuals::default(),
2048            accessibility: AccessibilitySettings::default(),
2049            input: InputMetrics::default(),
2050            text_rendering: TextRenderingHints::default(),
2051            scrollbar_preferences: ScrollbarPreferences::default(),
2052            visual_hints: VisualHints::default(),
2053            animation: AnimationMetrics::default(),
2054            audio: AudioMetrics::default(),
2055        }
2056    }
2057
2058    /// Windows 11 dark mode defaults (Segoe UI Variable, `WinUI` 3 dark colors).
2059    #[must_use] pub fn windows_11_dark() -> SystemStyle {
2060        SystemStyle {
2061            theme: Theme::Dark,
2062            platform: Platform::Windows,
2063            colors: SystemColors {
2064                text: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2065                background: OptionColorU::Some(ColorU::new_rgb(32, 32, 32)),
2066                accent: OptionColorU::Some(ColorU::new_rgb(0, 120, 215)),
2067                window_background: OptionColorU::Some(ColorU::new_rgb(25, 25, 25)),
2068                selection_background: OptionColorU::Some(ColorU::new_rgb(0, 120, 215)),
2069                selection_text: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2070                ..Default::default()
2071            },
2072            fonts: SystemFonts {
2073                ui_font: OptionString::Some("Segoe UI Variable Text".into()),
2074                ui_font_size: OptionF32::Some(9.0),
2075                monospace_font: OptionString::Some("Consolas".into()),
2076                ..Default::default()
2077            },
2078            metrics: SystemMetrics {
2079                corner_radius: OptionPixelValue::Some(PixelValue::px(4.0)),
2080                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2081                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(12.0)),
2082                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(6.0)),
2083                titlebar: TitlebarMetrics::windows(),
2084            },
2085            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_WINDOWS_DARK))),
2086            app_specific_stylesheet: None,
2087            run_destructor: true,
2088            icon_style: IconStyleOptions::default(),
2089            language: AzString::from_const_str("en-US"),
2090            os_version: OsVersion::WIN_11,
2091            prefers_reduced_motion: BoolCondition::False,
2092            prefers_high_contrast: BoolCondition::False,
2093            scroll_physics: ScrollPhysics::windows(),
2094            linux: LinuxCustomization::default(),
2095            focus_visuals: FocusVisuals::default(),
2096            accessibility: AccessibilitySettings::default(),
2097            input: InputMetrics::default(),
2098            text_rendering: TextRenderingHints::default(),
2099            scrollbar_preferences: ScrollbarPreferences::default(),
2100            visual_hints: VisualHints::default(),
2101            animation: AnimationMetrics::default(),
2102            audio: AudioMetrics::default(),
2103        }
2104    }
2105
2106    /// Windows 7 Aero theme defaults (Segoe UI, classic Aero colors).
2107    #[must_use] pub fn windows_7_aero() -> SystemStyle {
2108        SystemStyle {
2109            theme: Theme::Light,
2110            platform: Platform::Windows,
2111            colors: SystemColors {
2112                text: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2113                background: OptionColorU::Some(ColorU::new_rgb(240, 240, 240)),
2114                accent: OptionColorU::Some(ColorU::new_rgb(51, 153, 255)),
2115                window_background: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2116                selection_background: OptionColorU::Some(ColorU::new_rgb(51, 153, 255)),
2117                selection_text: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2118                ..Default::default()
2119            },
2120            fonts: SystemFonts {
2121                ui_font: OptionString::Some("Segoe UI".into()),
2122                ui_font_size: OptionF32::Some(9.0),
2123                monospace_font: OptionString::Some("Consolas".into()),
2124                ..Default::default()
2125            },
2126            metrics: SystemMetrics {
2127                corner_radius: OptionPixelValue::Some(PixelValue::px(6.0)),
2128                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2129                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(10.0)),
2130                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(5.0)),
2131                titlebar: TitlebarMetrics::windows(),
2132            },
2133            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_CLASSIC_LIGHT))),
2134            app_specific_stylesheet: None,
2135            run_destructor: true,
2136            icon_style: IconStyleOptions::default(),
2137            language: AzString::from_const_str("en-US"),
2138            os_version: OsVersion::WIN_7,
2139            prefers_reduced_motion: BoolCondition::False,
2140            prefers_high_contrast: BoolCondition::False,
2141            scroll_physics: ScrollPhysics::windows(),
2142            linux: LinuxCustomization::default(),
2143            focus_visuals: FocusVisuals::default(),
2144            accessibility: AccessibilitySettings::default(),
2145            input: InputMetrics::default(),
2146            text_rendering: TextRenderingHints::default(),
2147            scrollbar_preferences: ScrollbarPreferences::default(),
2148            visual_hints: VisualHints::default(),
2149            animation: AnimationMetrics::default(),
2150            audio: AudioMetrics::default(),
2151        }
2152    }
2153
2154    /// Windows XP Luna theme defaults (Tahoma, classic Luna blue).
2155    #[must_use] pub fn windows_xp_luna() -> SystemStyle {
2156        SystemStyle {
2157            theme: Theme::Light,
2158            platform: Platform::Windows,
2159            colors: SystemColors {
2160                text: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2161                background: OptionColorU::Some(ColorU::new_rgb(236, 233, 216)),
2162                accent: OptionColorU::Some(ColorU::new_rgb(49, 106, 197)),
2163                window_background: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2164                selection_background: OptionColorU::Some(ColorU::new_rgb(49, 106, 197)),
2165                selection_text: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2166                ..Default::default()
2167            },
2168            fonts: SystemFonts {
2169                ui_font: OptionString::Some("Tahoma".into()),
2170                ui_font_size: OptionF32::Some(8.0),
2171                monospace_font: OptionString::Some("Lucida Console".into()),
2172                ..Default::default()
2173            },
2174            metrics: SystemMetrics {
2175                corner_radius: OptionPixelValue::Some(PixelValue::px(3.0)),
2176                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2177                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(8.0)),
2178                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(4.0)),
2179                titlebar: TitlebarMetrics::windows(),
2180            },
2181            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_WINDOWS_CLASSIC))),
2182            app_specific_stylesheet: None,
2183            run_destructor: true,
2184            icon_style: IconStyleOptions::default(),
2185            language: AzString::from_const_str("en-US"),
2186            os_version: OsVersion::WIN_XP,
2187            prefers_reduced_motion: BoolCondition::False,
2188            prefers_high_contrast: BoolCondition::False,
2189            scroll_physics: ScrollPhysics::windows(),
2190            linux: LinuxCustomization::default(),
2191            focus_visuals: FocusVisuals::default(),
2192            accessibility: AccessibilitySettings::default(),
2193            input: InputMetrics::default(),
2194            text_rendering: TextRenderingHints::default(),
2195            scrollbar_preferences: ScrollbarPreferences::default(),
2196            visual_hints: VisualHints::default(),
2197            animation: AnimationMetrics::default(),
2198            audio: AudioMetrics::default(),
2199        }
2200    }
2201
2202    // --- macOS Styles ---
2203
2204    /// Modern macOS light mode defaults (SF Pro, rounded corners).
2205    #[must_use] pub fn macos_modern_light() -> SystemStyle {
2206        SystemStyle {
2207            platform: Platform::MacOs,
2208            theme: Theme::Light,
2209            colors: SystemColors {
2210                text: OptionColorU::Some(ColorU::new(0, 0, 0, 221)),
2211                background: OptionColorU::Some(ColorU::new_rgb(242, 242, 247)),
2212                accent: OptionColorU::Some(ColorU::new_rgb(0, 122, 255)),
2213                window_background: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2214                // Default macOS selection uses accent color with transparency
2215                selection_background: OptionColorU::Some(ColorU::new(0, 122, 255, 128)),
2216                selection_text: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2217                ..Default::default()
2218            },
2219            fonts: SystemFonts {
2220                ui_font: OptionString::Some(".SF NS".into()),
2221                ui_font_size: OptionF32::Some(13.0),
2222                monospace_font: OptionString::Some("Menlo".into()),
2223                ..Default::default()
2224            },
2225            metrics: SystemMetrics {
2226                corner_radius: OptionPixelValue::Some(PixelValue::px(8.0)),
2227                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2228                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(16.0)),
2229                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(6.0)),
2230                titlebar: TitlebarMetrics::macos(),
2231            },
2232            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_MACOS_LIGHT))),
2233            app_specific_stylesheet: None,
2234            run_destructor: true,
2235            icon_style: IconStyleOptions::default(),
2236            language: AzString::from_const_str("en-US"),
2237            os_version: OsVersion::MACOS_SONOMA,
2238            prefers_reduced_motion: BoolCondition::False,
2239            prefers_high_contrast: BoolCondition::False,
2240            scroll_physics: ScrollPhysics::macos(),
2241            linux: LinuxCustomization::default(),
2242            focus_visuals: FocusVisuals::default(),
2243            accessibility: AccessibilitySettings::default(),
2244            input: InputMetrics::default(),
2245            text_rendering: TextRenderingHints::default(),
2246            scrollbar_preferences: ScrollbarPreferences::default(),
2247            visual_hints: VisualHints::default(),
2248            animation: AnimationMetrics::default(),
2249            audio: AudioMetrics::default(),
2250        }
2251    }
2252
2253    /// Modern macOS dark mode defaults (SF Pro, dark background).
2254    #[must_use] pub fn macos_modern_dark() -> SystemStyle {
2255        SystemStyle {
2256            platform: Platform::MacOs,
2257            theme: Theme::Dark,
2258            colors: SystemColors {
2259                text: OptionColorU::Some(ColorU::new(255, 255, 255, 221)),
2260                background: OptionColorU::Some(ColorU::new_rgb(28, 28, 30)),
2261                accent: OptionColorU::Some(ColorU::new_rgb(10, 132, 255)),
2262                window_background: OptionColorU::Some(ColorU::new_rgb(44, 44, 46)),
2263                // Default macOS selection uses accent color with transparency
2264                selection_background: OptionColorU::Some(ColorU::new(10, 132, 255, 128)),
2265                selection_text: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2266                ..Default::default()
2267            },
2268            fonts: SystemFonts {
2269                ui_font: OptionString::Some(".SF NS".into()),
2270                ui_font_size: OptionF32::Some(13.0),
2271                monospace_font: OptionString::Some("SF Mono".into()),
2272                monospace_font_size: OptionF32::Some(12.0),
2273                title_font: OptionString::Some(".SF NS".into()),
2274                title_font_size: OptionF32::Some(13.0),
2275                menu_font: OptionString::Some(".SF NS".into()),
2276                menu_font_size: OptionF32::Some(13.0),
2277                small_font: OptionString::Some(".SF NS".into()),
2278                small_font_size: OptionF32::Some(11.0),
2279                ..Default::default()
2280            },
2281            metrics: SystemMetrics {
2282                corner_radius: OptionPixelValue::Some(PixelValue::px(8.0)),
2283                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2284                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(16.0)),
2285                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(6.0)),
2286                titlebar: TitlebarMetrics::macos(),
2287            },
2288            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_MACOS_DARK))),
2289            app_specific_stylesheet: None,
2290            run_destructor: true,
2291            icon_style: IconStyleOptions::default(),
2292            language: AzString::from_const_str("en-US"),
2293            os_version: OsVersion::MACOS_SONOMA,
2294            prefers_reduced_motion: BoolCondition::False,
2295            prefers_high_contrast: BoolCondition::False,
2296            scroll_physics: ScrollPhysics::macos(),
2297            linux: LinuxCustomization::default(),
2298            focus_visuals: FocusVisuals::default(),
2299            accessibility: AccessibilitySettings::default(),
2300            input: InputMetrics::default(),
2301            text_rendering: TextRenderingHints::default(),
2302            scrollbar_preferences: ScrollbarPreferences::default(),
2303            visual_hints: VisualHints::default(),
2304            animation: AnimationMetrics::default(),
2305            audio: AudioMetrics::default(),
2306        }
2307    }
2308
2309    /// Classic macOS Aqua theme defaults (Lucida Grande, gel scrollbars).
2310    #[must_use] pub fn macos_aqua() -> SystemStyle {
2311        SystemStyle {
2312            platform: Platform::MacOs,
2313            theme: Theme::Light,
2314            colors: SystemColors {
2315                text: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2316                background: OptionColorU::Some(ColorU::new_rgb(229, 229, 229)),
2317                accent: OptionColorU::Some(ColorU::new_rgb(63, 128, 234)),
2318                window_background: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2319                ..Default::default()
2320            },
2321            fonts: SystemFonts {
2322                ui_font: OptionString::Some("Lucida Grande".into()),
2323                ui_font_size: OptionF32::Some(13.0),
2324                monospace_font: OptionString::Some("Monaco".into()),
2325                monospace_font_size: OptionF32::Some(12.0),
2326                ..Default::default()
2327            },
2328            metrics: SystemMetrics {
2329                corner_radius: OptionPixelValue::Some(PixelValue::px(12.0)),
2330                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2331                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(16.0)),
2332                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(6.0)),
2333                titlebar: TitlebarMetrics::macos(),
2334            },
2335            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_MACOS_AQUA))),
2336            app_specific_stylesheet: None,
2337            run_destructor: true,
2338            icon_style: IconStyleOptions::default(),
2339            language: AzString::from_const_str("en-US"),
2340            os_version: OsVersion::MACOS_TIGER,
2341            prefers_reduced_motion: BoolCondition::False,
2342            prefers_high_contrast: BoolCondition::False,
2343            scroll_physics: ScrollPhysics::macos(),
2344            linux: LinuxCustomization::default(),
2345            focus_visuals: FocusVisuals::default(),
2346            accessibility: AccessibilitySettings::default(),
2347            input: InputMetrics::default(),
2348            text_rendering: TextRenderingHints::default(),
2349            scrollbar_preferences: ScrollbarPreferences::default(),
2350            visual_hints: VisualHints::default(),
2351            animation: AnimationMetrics::default(),
2352            audio: AudioMetrics::default(),
2353        }
2354    }
2355
2356    // --- Linux Styles ---
2357
2358    /// GNOME Adwaita light theme defaults (Cantarell font).
2359    #[must_use] pub fn gnome_adwaita_light() -> SystemStyle {
2360        SystemStyle {
2361            platform: Platform::Linux(DesktopEnvironment::Gnome),
2362            theme: Theme::Light,
2363            colors: SystemColors {
2364                text: OptionColorU::Some(ColorU::new_rgb(46, 52, 54)),
2365                background: OptionColorU::Some(ColorU::new_rgb(249, 249, 249)),
2366                accent: OptionColorU::Some(ColorU::new_rgb(53, 132, 228)),
2367                window_background: OptionColorU::Some(ColorU::new_rgb(237, 237, 237)),
2368                ..Default::default()
2369            },
2370            fonts: SystemFonts {
2371                ui_font: OptionString::Some("Cantarell".into()),
2372                ui_font_size: OptionF32::Some(11.0),
2373                monospace_font: OptionString::Some("Monospace".into()),
2374                ..Default::default()
2375            },
2376            metrics: SystemMetrics {
2377                corner_radius: OptionPixelValue::Some(PixelValue::px(4.0)),
2378                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2379                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(12.0)),
2380                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(8.0)),
2381                titlebar: TitlebarMetrics::linux_gnome(),
2382            },
2383            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_CLASSIC_LIGHT))),
2384            app_specific_stylesheet: None,
2385            run_destructor: true,
2386            icon_style: IconStyleOptions::default(),
2387            language: AzString::from_const_str("en-US"),
2388            os_version: OsVersion::LINUX_6_0,
2389            prefers_reduced_motion: BoolCondition::False,
2390            prefers_high_contrast: BoolCondition::False,
2391            scroll_physics: ScrollPhysics::default(),
2392            linux: LinuxCustomization::default(),
2393            focus_visuals: FocusVisuals::default(),
2394            accessibility: AccessibilitySettings::default(),
2395            input: InputMetrics::default(),
2396            text_rendering: TextRenderingHints::default(),
2397            scrollbar_preferences: ScrollbarPreferences::default(),
2398            visual_hints: VisualHints::default(),
2399            animation: AnimationMetrics::default(),
2400            audio: AudioMetrics::default(),
2401        }
2402    }
2403
2404    /// GNOME Adwaita dark theme defaults (Cantarell font, dark background).
2405    #[must_use] pub fn gnome_adwaita_dark() -> SystemStyle {
2406        SystemStyle {
2407            platform: Platform::Linux(DesktopEnvironment::Gnome),
2408            theme: Theme::Dark,
2409            colors: SystemColors {
2410                text: OptionColorU::Some(ColorU::new_rgb(238, 238, 236)),
2411                background: OptionColorU::Some(ColorU::new_rgb(36, 36, 36)),
2412                accent: OptionColorU::Some(ColorU::new_rgb(53, 132, 228)),
2413                window_background: OptionColorU::Some(ColorU::new_rgb(48, 48, 48)),
2414                ..Default::default()
2415            },
2416            fonts: SystemFonts {
2417                ui_font: OptionString::Some("Cantarell".into()),
2418                ui_font_size: OptionF32::Some(11.0),
2419                monospace_font: OptionString::Some("Monospace".into()),
2420                ..Default::default()
2421            },
2422            metrics: SystemMetrics {
2423                corner_radius: OptionPixelValue::Some(PixelValue::px(4.0)),
2424                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2425                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(12.0)),
2426                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(8.0)),
2427                titlebar: TitlebarMetrics::linux_gnome(),
2428            },
2429            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_CLASSIC_DARK))),
2430            app_specific_stylesheet: None,
2431            run_destructor: true,
2432            icon_style: IconStyleOptions::default(),
2433            language: AzString::from_const_str("en-US"),
2434            os_version: OsVersion::LINUX_6_0,
2435            prefers_reduced_motion: BoolCondition::False,
2436            prefers_high_contrast: BoolCondition::False,
2437            scroll_physics: ScrollPhysics::default(),
2438            linux: LinuxCustomization::default(),
2439            focus_visuals: FocusVisuals::default(),
2440            accessibility: AccessibilitySettings::default(),
2441            input: InputMetrics::default(),
2442            text_rendering: TextRenderingHints::default(),
2443            scrollbar_preferences: ScrollbarPreferences::default(),
2444            visual_hints: VisualHints::default(),
2445            animation: AnimationMetrics::default(),
2446            audio: AudioMetrics::default(),
2447        }
2448    }
2449
2450    /// GTK2 Clearlooks theme defaults (`DejaVu` Sans, orange accent).
2451    #[must_use] pub fn gtk2_clearlooks() -> SystemStyle {
2452        SystemStyle {
2453            platform: Platform::Linux(DesktopEnvironment::Gnome),
2454            theme: Theme::Light,
2455            colors: SystemColors {
2456                text: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2457                background: OptionColorU::Some(ColorU::new_rgb(239, 239, 239)),
2458                accent: OptionColorU::Some(ColorU::new_rgb(245, 121, 0)),
2459                ..Default::default()
2460            },
2461            fonts: SystemFonts {
2462                ui_font: OptionString::Some("DejaVu Sans".into()),
2463                ui_font_size: OptionF32::Some(10.0),
2464                monospace_font: OptionString::Some("DejaVu Sans Mono".into()),
2465                ..Default::default()
2466            },
2467            metrics: SystemMetrics {
2468                corner_radius: OptionPixelValue::Some(PixelValue::px(4.0)),
2469                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2470                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(10.0)),
2471                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(6.0)),
2472                titlebar: TitlebarMetrics::linux_gnome(),
2473            },
2474            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_CLASSIC_LIGHT))),
2475            app_specific_stylesheet: None,
2476            run_destructor: true,
2477            icon_style: IconStyleOptions::default(),
2478            language: AzString::from_const_str("en-US"),
2479            os_version: OsVersion::LINUX_2_6,
2480            prefers_reduced_motion: BoolCondition::False,
2481            prefers_high_contrast: BoolCondition::False,
2482            scroll_physics: ScrollPhysics::default(),
2483            linux: LinuxCustomization::default(),
2484            focus_visuals: FocusVisuals::default(),
2485            accessibility: AccessibilitySettings::default(),
2486            input: InputMetrics::default(),
2487            text_rendering: TextRenderingHints::default(),
2488            scrollbar_preferences: ScrollbarPreferences::default(),
2489            visual_hints: VisualHints::default(),
2490            animation: AnimationMetrics::default(),
2491            audio: AudioMetrics::default(),
2492        }
2493    }
2494
2495    /// KDE Breeze light theme defaults (Noto Sans, Oxygen scrollbars).
2496    #[must_use] pub fn kde_breeze_light() -> SystemStyle {
2497        SystemStyle {
2498            platform: Platform::Linux(DesktopEnvironment::Kde),
2499            theme: Theme::Light,
2500            colors: SystemColors {
2501                text: OptionColorU::Some(ColorU::new_rgb(31, 36, 39)),
2502                background: OptionColorU::Some(ColorU::new_rgb(239, 240, 241)),
2503                accent: OptionColorU::Some(ColorU::new_rgb(61, 174, 233)),
2504                ..Default::default()
2505            },
2506            fonts: SystemFonts {
2507                ui_font: OptionString::Some("Noto Sans".into()),
2508                ui_font_size: OptionF32::Some(10.0),
2509                monospace_font: OptionString::Some("Hack".into()),
2510                ..Default::default()
2511            },
2512            metrics: SystemMetrics {
2513                corner_radius: OptionPixelValue::Some(PixelValue::px(4.0)),
2514                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2515                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(12.0)),
2516                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(6.0)),
2517                titlebar: TitlebarMetrics::linux_gnome(),
2518            },
2519            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_KDE_OXYGEN))),
2520            app_specific_stylesheet: None,
2521            run_destructor: true,
2522            icon_style: IconStyleOptions::default(),
2523            language: AzString::from_const_str("en-US"),
2524            os_version: OsVersion::LINUX_6_0,
2525            prefers_reduced_motion: BoolCondition::False,
2526            prefers_high_contrast: BoolCondition::False,
2527            scroll_physics: ScrollPhysics::default(),
2528            linux: LinuxCustomization::default(),
2529            focus_visuals: FocusVisuals::default(),
2530            accessibility: AccessibilitySettings::default(),
2531            input: InputMetrics::default(),
2532            text_rendering: TextRenderingHints::default(),
2533            scrollbar_preferences: ScrollbarPreferences::default(),
2534            visual_hints: VisualHints::default(),
2535            animation: AnimationMetrics::default(),
2536            audio: AudioMetrics::default(),
2537        }
2538    }
2539
2540    // --- Mobile Styles ---
2541
2542    /// Android Material Design light theme defaults (Roboto font).
2543    #[must_use] pub fn android_material_light() -> SystemStyle {
2544        SystemStyle {
2545            platform: Platform::Android,
2546            theme: Theme::Light,
2547            colors: SystemColors {
2548                text: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2549                background: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2550                accent: OptionColorU::Some(ColorU::new_rgb(98, 0, 238)),
2551                ..Default::default()
2552            },
2553            fonts: SystemFonts {
2554                ui_font: OptionString::Some("Roboto".into()),
2555                ui_font_size: OptionF32::Some(14.0),
2556                monospace_font: OptionString::Some("Droid Sans Mono".into()),
2557                ..Default::default()
2558            },
2559            metrics: SystemMetrics {
2560                corner_radius: OptionPixelValue::Some(PixelValue::px(12.0)),
2561                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2562                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(16.0)),
2563                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(10.0)),
2564                titlebar: TitlebarMetrics::android(),
2565            },
2566            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_ANDROID_LIGHT))),
2567            app_specific_stylesheet: None,
2568            run_destructor: true,
2569            icon_style: IconStyleOptions::default(),
2570            language: AzString::from_const_str("en-US"),
2571            os_version: OsVersion::ANDROID_14,
2572            prefers_reduced_motion: BoolCondition::False,
2573            prefers_high_contrast: BoolCondition::False,
2574            scroll_physics: ScrollPhysics::android(),
2575            linux: LinuxCustomization::default(),
2576            focus_visuals: FocusVisuals::default(),
2577            accessibility: AccessibilitySettings::default(),
2578            input: InputMetrics::default(),
2579            text_rendering: TextRenderingHints::default(),
2580            scrollbar_preferences: ScrollbarPreferences::default(),
2581            visual_hints: VisualHints::default(),
2582            animation: AnimationMetrics::default(),
2583            audio: AudioMetrics::default(),
2584        }
2585    }
2586
2587    /// Android Holo dark theme defaults (Roboto font, dark background).
2588    #[must_use] pub fn android_holo_dark() -> SystemStyle {
2589        SystemStyle {
2590            platform: Platform::Android,
2591            theme: Theme::Dark,
2592            colors: SystemColors {
2593                text: OptionColorU::Some(ColorU::new_rgb(255, 255, 255)),
2594                background: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2595                accent: OptionColorU::Some(ColorU::new_rgb(51, 181, 229)),
2596                ..Default::default()
2597            },
2598            fonts: SystemFonts {
2599                ui_font: OptionString::Some("Roboto".into()),
2600                ui_font_size: OptionF32::Some(14.0),
2601                monospace_font: OptionString::Some("Droid Sans Mono".into()),
2602                ..Default::default()
2603            },
2604            metrics: SystemMetrics {
2605                corner_radius: OptionPixelValue::Some(PixelValue::px(2.0)),
2606                border_width: OptionPixelValue::Some(PixelValue::px(1.0)),
2607                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(12.0)),
2608                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(8.0)),
2609                titlebar: TitlebarMetrics::android(),
2610            },
2611            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_ANDROID_DARK))),
2612            app_specific_stylesheet: None,
2613            run_destructor: true,
2614            icon_style: IconStyleOptions::default(),
2615            language: AzString::from_const_str("en-US"),
2616            os_version: OsVersion::ANDROID_ICE_CREAM_SANDWICH,
2617            prefers_reduced_motion: BoolCondition::False,
2618            prefers_high_contrast: BoolCondition::False,
2619            scroll_physics: ScrollPhysics::android(),
2620            linux: LinuxCustomization::default(),
2621            focus_visuals: FocusVisuals::default(),
2622            accessibility: AccessibilitySettings::default(),
2623            input: InputMetrics::default(),
2624            text_rendering: TextRenderingHints::default(),
2625            scrollbar_preferences: ScrollbarPreferences::default(),
2626            visual_hints: VisualHints::default(),
2627            animation: AnimationMetrics::default(),
2628            audio: AudioMetrics::default(),
2629        }
2630    }
2631
2632    /// iOS light theme defaults (SF UI font, rounded corners).
2633    #[must_use] pub fn ios_light() -> SystemStyle {
2634        SystemStyle {
2635            platform: Platform::Ios,
2636            theme: Theme::Light,
2637            colors: SystemColors {
2638                text: OptionColorU::Some(ColorU::new_rgb(0, 0, 0)),
2639                background: OptionColorU::Some(ColorU::new_rgb(242, 242, 247)),
2640                accent: OptionColorU::Some(ColorU::new_rgb(0, 122, 255)),
2641                ..Default::default()
2642            },
2643            fonts: SystemFonts {
2644                ui_font: OptionString::Some(".SFUI-Display-Regular".into()),
2645                ui_font_size: OptionF32::Some(17.0),
2646                monospace_font: OptionString::Some("Menlo".into()),
2647                ..Default::default()
2648            },
2649            metrics: SystemMetrics {
2650                corner_radius: OptionPixelValue::Some(PixelValue::px(10.0)),
2651                border_width: OptionPixelValue::Some(PixelValue::px(0.5)),
2652                button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(20.0)),
2653                button_padding_vertical: OptionPixelValue::Some(PixelValue::px(12.0)),
2654                titlebar: TitlebarMetrics::ios(),
2655            },
2656            scrollbar: Some(Box::new(scrollbar_info_to_computed(&SCROLLBAR_IOS_LIGHT))),
2657            app_specific_stylesheet: None,
2658            run_destructor: true,
2659            icon_style: IconStyleOptions::default(),
2660            language: AzString::from_const_str("en-US"),
2661            os_version: OsVersion::IOS_17,
2662            prefers_reduced_motion: BoolCondition::False,
2663            prefers_high_contrast: BoolCondition::False,
2664            scroll_physics: ScrollPhysics::ios(),
2665            linux: LinuxCustomization::default(),
2666            focus_visuals: FocusVisuals::default(),
2667            accessibility: AccessibilitySettings::default(),
2668            input: InputMetrics::default(),
2669            text_rendering: TextRenderingHints::default(),
2670            scrollbar_preferences: ScrollbarPreferences::default(),
2671            visual_hints: VisualHints::default(),
2672            animation: AnimationMetrics::default(),
2673            audio: AudioMetrics::default(),
2674        }
2675    }
2676}
2677
2678#[cfg(test)]
2679mod autotest_generated {
2680    use super::*;
2681    use crate::css::rule_priority;
2682
2683    const ALL_FONT_TYPES: [SystemFontType; 11] = [
2684        SystemFontType::Ui,
2685        SystemFontType::UiBold,
2686        SystemFontType::Monospace,
2687        SystemFontType::MonospaceBold,
2688        SystemFontType::MonospaceItalic,
2689        SystemFontType::Title,
2690        SystemFontType::TitleBold,
2691        SystemFontType::Menu,
2692        SystemFontType::Small,
2693        SystemFontType::Serif,
2694        SystemFontType::SerifBold,
2695    ];
2696
2697    fn all_platforms() -> Vec<Platform> {
2698        vec![
2699            Platform::Windows,
2700            Platform::MacOs,
2701            Platform::Linux(DesktopEnvironment::Gnome),
2702            Platform::Linux(DesktopEnvironment::Kde),
2703            Platform::Linux(DesktopEnvironment::Other(AzString::from_const_str("Hyprland"))),
2704            Platform::Android,
2705            Platform::Ios,
2706            Platform::Unknown,
2707        ]
2708    }
2709
2710    /// Every hard-coded default style, so the smoke tests can sweep all of them.
2711    fn all_default_styles() -> Vec<(&'static str, SystemStyle)> {
2712        vec![
2713            ("windows_11_light", defaults::windows_11_light()),
2714            ("windows_11_dark", defaults::windows_11_dark()),
2715            ("windows_7_aero", defaults::windows_7_aero()),
2716            ("windows_xp_luna", defaults::windows_xp_luna()),
2717            ("macos_modern_light", defaults::macos_modern_light()),
2718            ("macos_modern_dark", defaults::macos_modern_dark()),
2719            ("macos_aqua", defaults::macos_aqua()),
2720            ("gnome_adwaita_light", defaults::gnome_adwaita_light()),
2721            ("gnome_adwaita_dark", defaults::gnome_adwaita_dark()),
2722            ("gtk2_clearlooks", defaults::gtk2_clearlooks()),
2723            ("kde_breeze_light", defaults::kde_breeze_light()),
2724            ("android_material_light", defaults::android_material_light()),
2725            ("android_holo_dark", defaults::android_holo_dark()),
2726            ("ios_light", defaults::ios_light()),
2727        ]
2728    }
2729
2730    // ── SystemFontType::from_css_str — parser ────────────────────────────
2731
2732    #[test]
2733    fn from_css_str_valid_minimal() {
2734        assert_eq!(SystemFontType::from_css_str("system:ui"), Some(SystemFontType::Ui));
2735        assert_eq!(
2736            SystemFontType::from_css_str("system:monospace:italic"),
2737            Some(SystemFontType::MonospaceItalic)
2738        );
2739    }
2740
2741    #[test]
2742    fn from_css_str_empty_input_returns_none() {
2743        assert_eq!(SystemFontType::from_css_str(""), None);
2744    }
2745
2746    #[test]
2747    fn from_css_str_whitespace_only_returns_none() {
2748        for s in ["   ", "\t\n", "\r\n\r\n", "\t \t \n"] {
2749            assert_eq!(SystemFontType::from_css_str(s), None, "input {s:?}");
2750        }
2751    }
2752
2753    #[test]
2754    fn from_css_str_prefix_only_is_none_and_does_not_panic_on_slice() {
2755        // Exactly 7 bytes: `&s[7..]` slices right at the end of the string.
2756        assert_eq!(SystemFontType::from_css_str("system:"), None);
2757        assert_eq!(SystemFontType::from_css_str("  system:  "), None);
2758        assert_eq!(SystemFontType::from_css_str("system::"), None);
2759    }
2760
2761    #[test]
2762    fn from_css_str_garbage_returns_none() {
2763        for s in [
2764            ";;;",
2765            "{}{}",
2766            "\0\u{1}\u{2}\u{7f}",
2767            "system",
2768            "systemui",
2769            "system;ui",
2770            "system:ui:",
2771            ":system:ui",
2772            "font-family: system:ui;",
2773            "\\system:ui",
2774            "system:ui\0",
2775            "system:\u{0}ui",
2776        ] {
2777            assert_eq!(SystemFontType::from_css_str(s), None, "input {s:?}");
2778        }
2779    }
2780
2781    #[test]
2782    fn from_css_str_leading_trailing_junk() {
2783        // Surrounding ASCII whitespace is trimmed …
2784        assert_eq!(SystemFontType::from_css_str("  system:ui  "), Some(SystemFontType::Ui));
2785        assert_eq!(
2786            SystemFontType::from_css_str("\t\nsystem:monospace\r\n"),
2787            Some(SystemFontType::Monospace)
2788        );
2789        // … but any other junk is rejected outright.
2790        assert_eq!(SystemFontType::from_css_str("system:ui;garbage"), None);
2791        assert_eq!(SystemFontType::from_css_str("garbage system:ui"), None);
2792        assert_eq!(SystemFontType::from_css_str("system:ui system:ui"), None);
2793        assert_eq!(SystemFontType::from_css_str("system: ui"), None);
2794        assert_eq!(SystemFontType::from_css_str("system:ui:bold:extra"), None);
2795    }
2796
2797    #[test]
2798    fn from_css_str_is_case_sensitive() {
2799        // Documents current behaviour: unlike `AZ_RICING`, the font keyword
2800        // is matched case-sensitively, so upper/mixed case is rejected.
2801        for s in ["SYSTEM:UI", "System:Ui", "system:UI", "System:ui", "sYsTeM:ui"] {
2802            assert_eq!(SystemFontType::from_css_str(s), None, "input {s:?}");
2803        }
2804    }
2805
2806    #[test]
2807    fn from_css_str_boundary_numbers() {
2808        for s in [
2809            "0",
2810            "-0",
2811            "9223372036854775807",
2812            "-9223372036854775808",
2813            "NaN",
2814            "inf",
2815            "-inf",
2816            "1e400",
2817            "system:0",
2818            "system:-1",
2819            "system:NaN",
2820            "system:inf",
2821            "system:9223372036854775807",
2822        ] {
2823            assert_eq!(SystemFontType::from_css_str(s), None, "input {s:?}");
2824        }
2825    }
2826
2827    #[test]
2828    fn from_css_str_unicode_does_not_panic() {
2829        for s in [
2830            "\u{1F600}",
2831            "system:\u{1F600}",
2832            "system:ui\u{0301}",          // combining acute accent
2833            "\u{1F600}system:ui",
2834            "systém:ui",                   // non-ASCII inside the prefix
2835            "system:ui",              // fullwidth latin
2836            "system:\u{202E}ui",           // right-to-left override
2837            "system:\u{FFFD}",
2838            "system:ui\u{200B}",           // zero-width space (not trimmed)
2839        ] {
2840            assert_eq!(SystemFontType::from_css_str(s), None, "input {s:?}");
2841        }
2842    }
2843
2844    #[test]
2845    fn from_css_str_extremely_long_input_does_not_hang() {
2846        let long = format!("system:{}", "u".repeat(1_000_000));
2847        assert_eq!(SystemFontType::from_css_str(&long), None);
2848
2849        // A valid keyword with a megabyte of trailing junk is still invalid.
2850        let long_suffix = format!("system:ui{}", "x".repeat(1_000_000));
2851        assert_eq!(SystemFontType::from_css_str(&long_suffix), None);
2852
2853        // A megabyte of whitespace around a valid keyword still parses.
2854        let padded = format!("{}system:ui{}", " ".repeat(100_000), " ".repeat(100_000));
2855        assert_eq!(SystemFontType::from_css_str(&padded), Some(SystemFontType::Ui));
2856    }
2857
2858    #[test]
2859    fn from_css_str_deeply_nested_input_does_not_stack_overflow() {
2860        let nested = format!("system:{}{}", "(".repeat(10_000), ")".repeat(10_000));
2861        assert_eq!(SystemFontType::from_css_str(&nested), None);
2862
2863        let brackets = format!("system:{}", "[".repeat(10_000));
2864        assert_eq!(SystemFontType::from_css_str(&brackets), None);
2865    }
2866
2867    // ── SystemFontType round-trip / getters ──────────────────────────────
2868
2869    #[test]
2870    fn font_type_css_str_round_trips() {
2871        for ty in ALL_FONT_TYPES {
2872            let s = ty.as_css_str();
2873            assert_eq!(SystemFontType::from_css_str(s), Some(ty), "round-trip of {ty:?}");
2874            // Padding must not change the decoded value.
2875            assert_eq!(
2876                SystemFontType::from_css_str(&format!("  {s}\t")),
2877                Some(ty),
2878                "padded round-trip of {ty:?}"
2879            );
2880        }
2881    }
2882
2883    #[test]
2884    fn font_type_css_str_is_well_formed_and_unique() {
2885        let mut seen: Vec<&'static str> = Vec::new();
2886        for ty in ALL_FONT_TYPES {
2887            let s = ty.as_css_str();
2888            assert!(s.starts_with("system:"), "{ty:?} -> {s:?}");
2889            assert!(s.len() > "system:".len(), "{ty:?} has an empty keyword");
2890            assert_eq!(s.trim(), s, "{ty:?} -> {s:?} has surrounding whitespace");
2891            assert!(s.is_ascii(), "{ty:?} -> {s:?} is not ASCII");
2892            seen.push(s);
2893        }
2894        seen.sort_unstable();
2895        assert!(
2896            seen.windows(2).all(|w| w[0] != w[1]),
2897            "as_css_str() is not injective: {seen:?}"
2898        );
2899    }
2900
2901    #[test]
2902    fn font_type_default_is_ui() {
2903        let d = SystemFontType::default();
2904        assert_eq!(d, SystemFontType::Ui);
2905        assert_eq!(d.as_css_str(), "system:ui");
2906        assert!(!d.is_bold());
2907        assert!(!d.is_italic());
2908    }
2909
2910    // ── SystemFontType::is_bold / is_italic — predicates ─────────────────
2911
2912    #[test]
2913    fn is_bold_matches_exactly_the_bold_variants() {
2914        assert!(SystemFontType::UiBold.is_bold());
2915        assert!(SystemFontType::MonospaceBold.is_bold());
2916        assert!(SystemFontType::TitleBold.is_bold());
2917        assert!(SystemFontType::SerifBold.is_bold());
2918
2919        assert!(!SystemFontType::Ui.is_bold());
2920        assert!(!SystemFontType::Monospace.is_bold());
2921        assert!(!SystemFontType::MonospaceItalic.is_bold());
2922        assert!(!SystemFontType::Title.is_bold());
2923        assert!(!SystemFontType::Menu.is_bold());
2924        assert!(!SystemFontType::Small.is_bold());
2925        assert!(!SystemFontType::Serif.is_bold());
2926    }
2927
2928    #[test]
2929    fn is_italic_matches_exactly_the_italic_variant() {
2930        assert!(SystemFontType::MonospaceItalic.is_italic());
2931        for ty in ALL_FONT_TYPES {
2932            if ty != SystemFontType::MonospaceItalic {
2933                assert!(!ty.is_italic(), "{ty:?} must not be italic");
2934            }
2935        }
2936    }
2937
2938    #[test]
2939    fn predicates_agree_with_the_css_keyword() {
2940        for ty in ALL_FONT_TYPES {
2941            let s = ty.as_css_str();
2942            assert_eq!(ty.is_bold(), s.ends_with(":bold"), "{ty:?} -> {s:?}");
2943            assert_eq!(ty.is_italic(), s.ends_with(":italic"), "{ty:?} -> {s:?}");
2944            // No variant is both bold and italic.
2945            assert!(!(ty.is_bold() && ty.is_italic()), "{ty:?} is bold *and* italic");
2946        }
2947    }
2948
2949    // ── SystemFontType::get_fallback_chain (+ private per-OS chains) ─────
2950
2951    #[test]
2952    fn fallback_chains_are_non_empty_and_deduplicated() {
2953        for platform in all_platforms() {
2954            for ty in ALL_FONT_TYPES {
2955                let chain = ty.get_fallback_chain(&platform);
2956                assert!(!chain.is_empty(), "{ty:?} on {platform:?} has an empty chain");
2957                assert!(
2958                    chain.iter().all(|f| !f.trim().is_empty()),
2959                    "{ty:?} on {platform:?} has a blank family: {chain:?}"
2960                );
2961                let mut sorted = chain.clone();
2962                sorted.sort_unstable();
2963                assert!(
2964                    sorted.windows(2).all(|w| w[0] != w[1]),
2965                    "{ty:?} on {platform:?} lists a duplicate family: {chain:?}"
2966                );
2967            }
2968        }
2969    }
2970
2971    #[test]
2972    fn fallback_chain_is_deterministic() {
2973        for platform in all_platforms() {
2974            for ty in ALL_FONT_TYPES {
2975                assert_eq!(
2976                    ty.get_fallback_chain(&platform),
2977                    ty.get_fallback_chain(&platform),
2978                    "{ty:?} on {platform:?} is not deterministic"
2979                );
2980            }
2981        }
2982    }
2983
2984    #[test]
2985    fn ios_shares_the_macos_fallback_chain() {
2986        for ty in ALL_FONT_TYPES {
2987            assert_eq!(
2988                ty.get_fallback_chain(&Platform::Ios),
2989                ty.get_fallback_chain(&Platform::MacOs),
2990                "{ty:?}"
2991            );
2992        }
2993    }
2994
2995    #[test]
2996    fn linux_fallback_chain_ignores_the_desktop_environment() {
2997        let gnome = Platform::Linux(DesktopEnvironment::Gnome);
2998        let kde = Platform::Linux(DesktopEnvironment::Kde);
2999        let other = Platform::Linux(DesktopEnvironment::Other(AzString::from_const_str("")));
3000        for ty in ALL_FONT_TYPES {
3001            let a = ty.get_fallback_chain(&gnome);
3002            assert_eq!(a, ty.get_fallback_chain(&kde), "{ty:?}");
3003            assert_eq!(a, ty.get_fallback_chain(&other), "{ty:?}");
3004        }
3005    }
3006
3007    #[test]
3008    fn unknown_platform_falls_back_to_generic_css_families() {
3009        for ty in ALL_FONT_TYPES {
3010            let chain = ty.get_fallback_chain(&Platform::Unknown);
3011            assert_eq!(chain.len(), 1, "{ty:?} -> {chain:?}");
3012            let expected = if ty.is_italic() || matches!(
3013                ty,
3014                SystemFontType::Monospace | SystemFontType::MonospaceBold
3015            ) {
3016                "monospace"
3017            } else if matches!(ty, SystemFontType::Serif | SystemFontType::SerifBold) {
3018                "serif"
3019            } else {
3020                "sans-serif"
3021            };
3022            assert_eq!(chain[0], expected, "{ty:?}");
3023        }
3024    }
3025
3026    #[test]
3027    fn monospace_variants_share_one_chain_per_platform() {
3028        for platform in all_platforms() {
3029            let base = SystemFontType::Monospace.get_fallback_chain(&platform);
3030            assert_eq!(
3031                SystemFontType::MonospaceBold.get_fallback_chain(&platform),
3032                base,
3033                "{platform:?}"
3034            );
3035            assert_eq!(
3036                SystemFontType::MonospaceItalic.get_fallback_chain(&platform),
3037                base,
3038                "{platform:?}"
3039            );
3040        }
3041    }
3042
3043    // ── Platform::current ────────────────────────────────────────────────
3044
3045    #[test]
3046    fn platform_current_is_deterministic_and_matches_target_os() {
3047        let a = Platform::current();
3048        assert_eq!(a, Platform::current());
3049
3050        #[cfg(target_os = "linux")]
3051        assert!(matches!(a, Platform::Linux(_)), "{a:?}");
3052        #[cfg(target_os = "windows")]
3053        assert_eq!(a, Platform::Windows);
3054        #[cfg(target_os = "macos")]
3055        assert_eq!(a, Platform::MacOs);
3056        #[cfg(target_os = "android")]
3057        assert_eq!(a, Platform::Android);
3058        #[cfg(target_os = "ios")]
3059        assert_eq!(a, Platform::Ios);
3060
3061        // `current()` never reports the fallback on a supported OS.
3062        #[cfg(any(
3063            target_os = "linux",
3064            target_os = "windows",
3065            target_os = "macos",
3066            target_os = "android",
3067            target_os = "ios"
3068        ))]
3069        assert_ne!(a, Platform::Unknown);
3070
3071        // Default is the "we don't know" variant, not the compiled-for one.
3072        assert_eq!(Platform::default(), Platform::Unknown);
3073    }
3074
3075    // ── TitlebarMetrics constructors ─────────────────────────────────────
3076
3077    #[test]
3078    fn titlebar_metrics_have_sane_geometry() {
3079        // NB: TitlebarMetrics::default() is deliberately the "unknown" variant (all-None,
3080        // so SystemMetrics::default() can represent "not detected" and resolve() falls
3081        // back) — it is NOT a rendering profile, so it is excluded here. The platform
3082        // constructors below are the ones that must carry concrete, sane geometry.
3083        let all = [
3084            ("windows", TitlebarMetrics::windows()),
3085            ("macos", TitlebarMetrics::macos()),
3086            ("linux_gnome", TitlebarMetrics::linux_gnome()),
3087            ("ios", TitlebarMetrics::ios()),
3088            ("android", TitlebarMetrics::android()),
3089        ];
3090        for (name, tm) in all {
3091            let height = tm
3092                .height
3093                .as_ref()
3094                .map(|p| p.to_pixels_internal(0.0, 0.0, 0.0))
3095                .expect("titlebar height must be set");
3096            assert!(height.is_finite() && height > 0.0, "{name}: height {height}");
3097
3098            let button_area = tm
3099                .button_area_width
3100                .as_ref()
3101                .map(|p| p.to_pixels_internal(0.0, 0.0, 0.0))
3102                .expect("button area width must be set");
3103            assert!(
3104                button_area.is_finite() && button_area >= 0.0,
3105                "{name}: button_area_width {button_area}"
3106            );
3107
3108            let padding = tm
3109                .padding_horizontal
3110                .as_ref()
3111                .map(|p| p.to_pixels_internal(0.0, 0.0, 0.0))
3112                .expect("padding must be set");
3113            assert!(padding.is_finite() && padding >= 0.0, "{name}: padding {padding}");
3114
3115            let size = tm.title_font_size.into_option().expect("font size must be set");
3116            assert!(size.is_finite() && size > 0.0, "{name}: font size {size}");
3117
3118            let weight = tm.title_font_weight.into_option().expect("font weight must be set");
3119            assert!((100..=900).contains(&weight), "{name}: weight {weight}");
3120        }
3121    }
3122
3123    #[test]
3124    fn titlebar_metrics_match_their_platform_conventions() {
3125        let win = TitlebarMetrics::windows();
3126        assert_eq!(win.button_side, TitlebarButtonSide::Right);
3127        assert!(win.buttons.has_close && win.buttons.has_minimize && win.buttons.has_maximize);
3128        assert!(!win.buttons.has_fullscreen);
3129
3130        // macOS: traffic lights on the left, zoom replaced by fullscreen.
3131        let mac = TitlebarMetrics::macos();
3132        assert_eq!(mac.button_side, TitlebarButtonSide::Left);
3133        assert!(mac.buttons.has_fullscreen);
3134        assert!(!mac.buttons.has_maximize);
3135
3136        assert_eq!(TitlebarMetrics::linux_gnome().button_side, TitlebarButtonSide::Right);
3137
3138        // Mobile: no window controls at all.
3139        for (name, tm) in [("ios", TitlebarMetrics::ios()), ("android", TitlebarMetrics::android())] {
3140            let b = tm.buttons;
3141            assert!(
3142                !b.has_close && !b.has_minimize && !b.has_maximize && !b.has_fullscreen,
3143                "{name} must not expose window controls"
3144            );
3145        }
3146
3147        // Only iOS declares a notch safe area.
3148        let ios = TitlebarMetrics::ios();
3149        assert!(ios.safe_area.top.is_some());
3150        assert!(ios.safe_area.bottom.is_some());
3151        assert_eq!(TitlebarMetrics::windows().safe_area, SafeAreaInsets::default());
3152    }
3153
3154    // ── SystemStyle::new / detect / default_for_platform ─────────────────
3155
3156    #[test]
3157    fn system_style_new_detect_and_default_for_platform_agree() {
3158        let a = SystemStyle::new();
3159        let b = SystemStyle::detect();
3160        let c = SystemStyle::default_for_platform();
3161        assert_eq!(a, b);
3162        assert_eq!(b, c);
3163    }
3164
3165    #[test]
3166    fn system_style_constructors_arm_the_ffi_drop_guard() {
3167        // `run_destructor` is the double-drop guard; every freshly built style
3168        // (and every clone of one) must own its heap pointers.
3169        assert!(SystemStyle::default().run_destructor);
3170        assert!(SystemStyle::new().run_destructor);
3171        assert!(SystemStyle::detect().run_destructor);
3172        for (name, style) in all_default_styles() {
3173            assert!(style.run_destructor, "{name} does not own its heap pointers");
3174            assert!(style.clone().run_destructor, "clone of {name} lost the guard");
3175        }
3176    }
3177
3178    #[test]
3179    fn system_style_default_is_empty_but_valid() {
3180        let d = SystemStyle::default();
3181        assert_eq!(d.platform, Platform::Unknown);
3182        assert_eq!(d.theme, Theme::Light);
3183        assert!(d.app_specific_stylesheet.is_none());
3184        assert!(d.scrollbar.is_none());
3185        assert!(d.language.as_str().is_empty());
3186        assert!(d.colors.text.is_none());
3187    }
3188
3189    // ── defaults::* (+ the private scrollbar_info_to_computed helper) ─────
3190
3191    #[test]
3192    fn default_styles_are_fully_populated() {
3193        for (name, style) in all_default_styles() {
3194            assert!(style.colors.text.is_some(), "{name}: no text color");
3195            assert!(style.colors.background.is_some(), "{name}: no background color");
3196            assert!(style.colors.accent.is_some(), "{name}: no accent color");
3197            assert!(style.fonts.ui_font.is_some(), "{name}: no UI font");
3198            assert!(style.fonts.monospace_font.is_some(), "{name}: no monospace font");
3199            assert!(!style.language.as_str().is_empty(), "{name}: empty language");
3200            assert_ne!(style.platform, Platform::Unknown, "{name}: unknown platform");
3201
3202            let size = style.fonts.ui_font_size.into_option().expect("ui font size");
3203            assert!(size.is_finite() && size > 0.0, "{name}: ui font size {size}");
3204
3205            let radius = style
3206                .metrics
3207                .corner_radius
3208                .as_ref()
3209                .map(|p| p.to_pixels_internal(0.0, 0.0, 0.0))
3210                .expect("corner radius");
3211            assert!(radius.is_finite() && radius >= 0.0, "{name}: corner radius {radius}");
3212        }
3213    }
3214
3215    #[test]
3216    fn default_styles_carry_a_fully_resolved_scrollbar() {
3217        // Exercises the private `scrollbar_info_to_computed` helper: every
3218        // built-in ScrollbarInfo uses solid colors, so nothing may map to None.
3219        for (name, style) in all_default_styles() {
3220            let sb = style.scrollbar.as_ref().unwrap_or_else(|| panic!("{name}: no scrollbar"));
3221            assert!(sb.width.is_some(), "{name}: scrollbar width lost");
3222            assert!(sb.thumb_color.is_some(), "{name}: thumb color lost");
3223            assert!(sb.track_color.is_some(), "{name}: track color lost");
3224        }
3225    }
3226
3227    #[test]
3228    fn light_and_dark_default_styles_differ() {
3229        assert_ne!(defaults::windows_11_light(), defaults::windows_11_dark());
3230        assert_ne!(defaults::macos_modern_light(), defaults::macos_modern_dark());
3231        assert_ne!(defaults::gnome_adwaita_light(), defaults::gnome_adwaita_dark());
3232        assert_ne!(defaults::android_material_light(), defaults::android_holo_dark());
3233
3234        assert_eq!(defaults::windows_11_dark().theme, Theme::Dark);
3235        assert_eq!(defaults::macos_modern_dark().theme, Theme::Dark);
3236        assert_eq!(defaults::gnome_adwaita_dark().theme, Theme::Dark);
3237        assert_eq!(defaults::android_holo_dark().theme, Theme::Dark);
3238
3239        assert_eq!(defaults::kde_breeze_light().platform, Platform::Linux(DesktopEnvironment::Kde));
3240        assert_eq!(defaults::ios_light().platform, Platform::Ios);
3241    }
3242
3243    #[test]
3244    fn default_style_constructors_are_deterministic() {
3245        for _ in 0..3 {
3246            assert_eq!(defaults::windows_xp_luna(), defaults::windows_xp_luna());
3247            assert_eq!(defaults::macos_aqua(), defaults::macos_aqua());
3248            assert_eq!(defaults::gtk2_clearlooks(), defaults::gtk2_clearlooks());
3249            assert_eq!(defaults::windows_7_aero(), defaults::windows_7_aero());
3250        }
3251    }
3252
3253    // ── SystemStyle::to_json_string ──────────────────────────────────────
3254
3255    #[test]
3256    fn to_json_string_has_balanced_braces_for_every_default() {
3257        let mut styles = all_default_styles();
3258        styles.push(("default", SystemStyle::default()));
3259        for (name, style) in styles {
3260            let json = style.to_json_string();
3261            let s = json.as_str();
3262            assert!(s.starts_with('{'), "{name}: does not start with '{{'");
3263            assert!(s.ends_with('}'), "{name}: does not end with '}}'");
3264            let open = s.chars().filter(|c| *c == '{').count();
3265            let close = s.chars().filter(|c| *c == '}').count();
3266            assert_eq!(open, close, "{name}: unbalanced braces");
3267            for key in [
3268                "\"theme\"",
3269                "\"platform\"",
3270                "\"colors\"",
3271                "\"fonts\"",
3272                "\"titlebar\"",
3273                "\"input\"",
3274                "\"accessibility\"",
3275                "\"audio\"",
3276            ] {
3277                assert!(s.contains(key), "{name}: missing {key}");
3278            }
3279        }
3280    }
3281
3282    #[test]
3283    fn to_json_string_reports_known_values() {
3284        let json = defaults::windows_11_light().to_json_string();
3285        let s = json.as_str();
3286        assert!(s.contains("\"theme\": \"Light\""), "{s}");
3287        assert!(s.contains("\"platform\": \"Windows\""), "{s}");
3288        assert!(s.contains("\"language\": \"en-US\""), "{s}");
3289        // text = rgb(0,0,0) -> "#000000ff" (alpha is included)
3290        assert!(s.contains("\"text\": \"#000000ff\""), "{s}");
3291        // Windows titlebar height is 32px, formatted with one decimal.
3292        assert!(s.contains("\"height\": 32.0"), "{s}");
3293        // Unset colors serialize as JSON null, not as an empty string.
3294        assert!(s.contains("\"grid\": null"), "{s}");
3295    }
3296
3297    #[test]
3298    fn to_json_string_survives_nan_and_infinite_metrics() {
3299        let mut style = SystemStyle::default();
3300        style.accessibility.text_scale_factor = f32::NAN;
3301        style.animation.animation_duration_factor = f32::INFINITY;
3302        style.input.double_click_distance_px = f32::NEG_INFINITY;
3303        style.input.drag_threshold_px = f32::MAX;
3304        style.input.caret_width_px = f32::MIN_POSITIVE;
3305        style.input.double_click_time_ms = u32::MAX;
3306        style.input.caret_blink_rate_ms = u32::MAX;
3307        style.input.wheel_scroll_lines = u32::MAX;
3308        style.input.hover_time_ms = u32::MAX;
3309        style.text_rendering.font_smoothing_gamma = u32::MAX;
3310        style.linux.cursor_size = u32::MAX;
3311
3312        // Must not panic; the extreme values are formatted, not truncated away.
3313        let json = style.to_json_string();
3314        let s = json.as_str();
3315        assert!(!s.is_empty());
3316        assert!(s.contains(&format!("\"cursor_size\": {}", u32::MAX)), "{s}");
3317        assert!(s.contains(&format!("\"double_click_time_ms\": {}", u32::MAX)), "{s}");
3318    }
3319
3320    #[test]
3321    fn to_json_string_survives_extreme_pixel_metrics() {
3322        let mut style = SystemStyle::default();
3323        style.metrics.titlebar.height = OptionPixelValue::Some(PixelValue::px(f32::NAN));
3324        style.metrics.titlebar.button_area_width =
3325            OptionPixelValue::Some(PixelValue::px(f32::INFINITY));
3326        style.metrics.titlebar.padding_horizontal =
3327            OptionPixelValue::Some(PixelValue::px(f32::NEG_INFINITY));
3328        style.metrics.titlebar.title_font_size = OptionF32::Some(f32::MAX);
3329        style.metrics.titlebar.title_font_weight = OptionU16::Some(u16::MAX);
3330
3331        let json = style.to_json_string();
3332        assert!(!json.as_str().is_empty());
3333
3334        // PixelValue stores fixed-point isize, so NaN saturates to 0 and the
3335        // infinities saturate to the isize bounds — the JSON stays finite.
3336        let nan_px = PixelValue::px(f32::NAN).to_pixels_internal(0.0, 0.0, 0.0);
3337        assert_eq!(nan_px, 0.0);
3338        assert!(PixelValue::px(f32::INFINITY)
3339            .to_pixels_internal(0.0, 0.0, 0.0)
3340            .is_finite());
3341        assert!(PixelValue::px(f32::NEG_INFINITY)
3342            .to_pixels_internal(0.0, 0.0, 0.0)
3343            .is_finite());
3344    }
3345
3346    #[test]
3347    fn to_json_string_survives_hostile_strings() {
3348        // Quote / backslash / newline / unicode in an OS-reported string must
3349        // not panic the formatter.
3350        let mut style = SystemStyle::default();
3351        style.language = AzString::from("\"\\\n\t\u{1F600}");
3352        style.fonts.ui_font = OptionString::Some(AzString::from("a\"b\\c"));
3353        style.linux.gtk_theme = OptionString::Some(AzString::from("\u{202E}evil"));
3354
3355        let json = style.to_json_string();
3356        let s = json.as_str();
3357        assert!(!s.is_empty());
3358        assert!(s.contains("\"language\":"), "{s}");
3359    }
3360
3361    #[test]
3362    fn to_json_string_is_deterministic() {
3363        let style = defaults::gnome_adwaita_dark();
3364        assert_eq!(style.to_json_string(), style.to_json_string());
3365        assert_ne!(
3366            defaults::gnome_adwaita_dark().to_json_string(),
3367            defaults::gnome_adwaita_light().to_json_string()
3368        );
3369    }
3370
3371    // ── SystemStyle::create_csd_stylesheet ───────────────────────────────
3372
3373    #[test]
3374    fn csd_stylesheet_rules_all_carry_system_priority() {
3375        let mut styles = all_default_styles();
3376        styles.push(("default", SystemStyle::default()));
3377        for (name, style) in styles {
3378            let css = style.create_csd_stylesheet();
3379            let rules = css.rules.as_slice();
3380            assert!(!rules.is_empty(), "{name}: produced no rules");
3381            for rule in rules {
3382                assert_eq!(
3383                    rule.priority,
3384                    rule_priority::SYSTEM,
3385                    "{name}: rule escaped the SYSTEM layer"
3386                );
3387            }
3388            // System rules must lose against author CSS.
3389            const _: () = assert!(rule_priority::SYSTEM < rule_priority::AUTHOR);
3390        }
3391    }
3392
3393    #[test]
3394    fn csd_stylesheet_uses_fallback_colors_when_the_system_reports_none() {
3395        // All colors unset -> the hard-coded fallbacks must still produce CSS.
3396        let css = SystemStyle::default().create_csd_stylesheet();
3397        assert!(!css.rules.as_slice().is_empty());
3398        assert_ne!(css, Css::default());
3399    }
3400
3401    #[test]
3402    fn csd_stylesheet_is_platform_specific() {
3403        let mac = defaults::macos_modern_light().create_csd_stylesheet();
3404        let win = defaults::windows_11_light().create_csd_stylesheet();
3405        let lin = defaults::gnome_adwaita_light().create_csd_stylesheet();
3406        assert_ne!(mac, win);
3407        assert_ne!(win, lin);
3408        assert_ne!(mac, lin);
3409        // macOS appends the traffic-light rules on top of the shared ones.
3410        assert!(mac.rules.as_slice().len() > win.rules.as_slice().len());
3411    }
3412
3413    #[test]
3414    fn csd_stylesheet_survives_extreme_corner_radius() {
3415        for radius in [
3416            PixelValue::px(f32::NAN),
3417            PixelValue::px(f32::INFINITY),
3418            PixelValue::px(f32::NEG_INFINITY),
3419            PixelValue::px(f32::MAX),
3420            PixelValue::px(-1.0),
3421            PixelValue::percent(f32::MAX),
3422            PixelValue::em(f32::MIN),
3423        ] {
3424            let mut style = defaults::windows_11_light();
3425            style.metrics.corner_radius = OptionPixelValue::Some(radius);
3426            let css = style.create_csd_stylesheet();
3427            assert!(
3428                !css.rules.as_slice().is_empty(),
3429                "radius {radius:?} produced no rules"
3430            );
3431            for rule in css.rules.as_slice() {
3432                assert_eq!(rule.priority, rule_priority::SYSTEM);
3433            }
3434        }
3435    }
3436
3437    #[test]
3438    fn csd_stylesheet_is_deterministic() {
3439        let style = defaults::kde_breeze_light();
3440        assert_eq!(style.create_csd_stylesheet(), style.create_csd_stylesheet());
3441    }
3442
3443    // ── AZ_RICING / environment probes ───────────────────────────────────
3444    //
3445    // These functions read process-global environment variables. The tests
3446    // below deliberately do NOT mutate the environment (`set_var` races with
3447    // every other test thread in the same binary), so they assert the
3448    // invariants that must hold for *any* ambient environment.
3449
3450    #[test]
3451    fn ricing_mode_is_deterministic_and_total() {
3452        let mode = ricing_mode();
3453        assert_eq!(mode, ricing_mode(), "ricing_mode() is not deterministic");
3454        assert!(
3455            matches!(mode, RicingMode::Off | RicingMode::Default | RicingMode::Force),
3456            "{mode:?}"
3457        );
3458        assert_eq!(RicingMode::default(), RicingMode::Default);
3459    }
3460
3461    #[test]
3462    fn ricing_enabled_is_the_inverse_of_off() {
3463        assert_eq!(ricing_enabled(), ricing_mode() != RicingMode::Off);
3464        assert_eq!(ricing_enabled(), ricing_enabled());
3465    }
3466
3467    #[test]
3468    fn detect_linux_desktop_env_is_deterministic() {
3469        let a = detect_linux_desktop_env();
3470        assert_eq!(a, detect_linux_desktop_env());
3471
3472        // Unless the ambient env explicitly sets an *empty* desktop string
3473        // (which the function forwards verbatim), the `Other` label is either
3474        // a const name or the non-empty env value.
3475        let blank_env = |k: &str| std::env::var(k).map(|v| v.is_empty()).unwrap_or(false);
3476        if !blank_env("XDG_CURRENT_DESKTOP") && !blank_env("DESKTOP_SESSION") {
3477            if let DesktopEnvironment::Other(ref name) = a {
3478                assert!(!name.as_str().is_empty(), "empty desktop-environment label");
3479            }
3480        }
3481    }
3482
3483    #[test]
3484    fn detect_system_language_is_a_normalized_tag() {
3485        let lang = detect_system_language();
3486        let s = lang.as_str();
3487        assert!(!s.is_empty(), "language tag must never be empty");
3488        // The encoding suffix (".UTF-8"), the LANGUAGE list separator (':')
3489        // and the POSIX underscore must all be normalized away.
3490        assert!(!s.contains('.'), "{s:?} still carries an encoding suffix");
3491        assert!(!s.contains(':'), "{s:?} still carries a locale list");
3492        assert!(!s.contains('_'), "{s:?} is not BCP 47 (underscore)");
3493        assert_eq!(lang, detect_system_language(), "not deterministic");
3494    }
3495}