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