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