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