Skip to main content

azul_css/
dynamic_selector.rs

1//! Dynamic CSS selectors for runtime evaluation based on OS, media queries, container queries, etc.
2
3use crate::corety::{AzString, OptionString};
4use crate::props::property::CssProperty;
5
6/// State flags for pseudo-classes (used in DynamicSelectorContext)
7/// Note: This is a CSS-only version. See azul_core::styled_dom::StyledNodeState for the main type.
8#[repr(C)]
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
10pub struct PseudoStateFlags {
11    pub hover: bool,
12    pub active: bool,
13    pub focused: bool,
14    pub disabled: bool,
15    pub checked: bool,
16    pub focus_within: bool,
17    pub visited: bool,
18    /// Window is not focused (equivalent to GTK :backdrop)
19    pub backdrop: bool,
20    /// Element is currently being dragged (:dragging)
21    pub dragging: bool,
22    /// A dragged element is over this drop target (:drag-over)
23    pub drag_over: bool,
24}
25
26impl PseudoStateFlags {
27    /// Check if a specific pseudo-state is active
28    pub fn has_state(&self, state: PseudoStateType) -> bool {
29        match state {
30            PseudoStateType::Normal => true,
31            PseudoStateType::Hover => self.hover,
32            PseudoStateType::Active => self.active,
33            PseudoStateType::Focus => self.focused,
34            PseudoStateType::Disabled => self.disabled,
35            PseudoStateType::Checked => self.checked,
36            PseudoStateType::FocusWithin => self.focus_within,
37            PseudoStateType::Visited => self.visited,
38            PseudoStateType::Backdrop => self.backdrop,
39            PseudoStateType::Dragging => self.dragging,
40            PseudoStateType::DragOver => self.drag_over,
41        }
42    }
43}
44
45/// Dynamic selector that is evaluated at runtime
46/// C-compatible: Tagged union with single field
47#[repr(C, u8)]
48#[derive(Debug, Clone, PartialEq)]
49pub enum DynamicSelector {
50    /// Operating system condition
51    Os(OsCondition) = 0,
52    /// Operating system version (e.g. macOS 14.0, Windows 11)
53    OsVersion(OsVersionCondition) = 1,
54    /// Media query (print/screen)
55    Media(MediaType) = 2,
56    /// Viewport width min/max (for @media)
57    ViewportWidth(MinMaxRange) = 3,
58    /// Viewport height min/max (for @media)
59    ViewportHeight(MinMaxRange) = 4,
60    /// Container width min/max (for @container)
61    ContainerWidth(MinMaxRange) = 5,
62    /// Container height min/max (for @container)
63    ContainerHeight(MinMaxRange) = 6,
64    /// Container name (for named @container queries)
65    ContainerName(AzString) = 7,
66    /// Theme (dark/light/custom)
67    Theme(ThemeCondition) = 8,
68    /// Aspect Ratio (min/max for @media and @container)
69    AspectRatio(MinMaxRange) = 9,
70    /// Orientation (portrait/landscape)
71    Orientation(OrientationType) = 10,
72    /// Reduced Motion (accessibility)
73    PrefersReducedMotion(BoolCondition) = 11,
74    /// High Contrast (accessibility)
75    PrefersHighContrast(BoolCondition) = 12,
76    /// Pseudo-State (hover, active, focus, etc.)
77    PseudoState(PseudoStateType) = 13,
78    /// Language/Locale (for @lang("de-DE"))
79    /// Matches BCP 47 language tags (e.g., "de", "de-DE", "en-US")
80    Language(LanguageCondition) = 14,
81}
82
83impl_option!(
84    DynamicSelector,
85    OptionDynamicSelector,
86    copy = false,
87    [Debug, Clone, PartialEq]
88);
89
90impl_vec!(DynamicSelector, DynamicSelectorVec, DynamicSelectorVecDestructor, DynamicSelectorVecDestructorType, DynamicSelectorVecSlice, OptionDynamicSelector);
91impl_vec_clone!(
92    DynamicSelector,
93    DynamicSelectorVec,
94    DynamicSelectorVecDestructor
95);
96impl_vec_debug!(DynamicSelector, DynamicSelectorVec);
97impl_vec_partialeq!(DynamicSelector, DynamicSelectorVec);
98
99/// Min/Max Range for numeric conditions (C-compatible)
100#[repr(C)]
101#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
102pub struct MinMaxRange {
103    /// Minimum value (NaN = no minimum limit)
104    pub min: f32,
105    /// Maximum value (NaN = no maximum limit)
106    pub max: f32,
107}
108
109impl MinMaxRange {
110    pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
111        Self {
112            min: if let Some(m) = min { m } else { f32::NAN },
113            max: if let Some(m) = max { m } else { f32::NAN },
114        }
115    }
116    
117    /// Create a range with only a minimum value (>= min)
118    pub const fn with_min(min_val: f32) -> Self {
119        Self {
120            min: min_val,
121            max: f32::NAN,
122        }
123    }
124    
125    /// Create a range with only a maximum value (<= max)
126    pub const fn with_max(max_val: f32) -> Self {
127        Self {
128            min: f32::NAN,
129            max: max_val,
130        }
131    }
132
133    pub fn min(&self) -> Option<f32> {
134        if self.min.is_nan() {
135            None
136        } else {
137            Some(self.min)
138        }
139    }
140
141    pub fn max(&self) -> Option<f32> {
142        if self.max.is_nan() {
143            None
144        } else {
145            Some(self.max)
146        }
147    }
148
149    pub fn matches(&self, value: f32) -> bool {
150        let min_ok = self.min.is_nan() || value >= self.min;
151        let max_ok = self.max.is_nan() || value <= self.max;
152        min_ok && max_ok
153    }
154}
155
156/// Boolean condition (C-compatible)
157#[repr(C)]
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
159pub enum BoolCondition {
160    #[default]
161    False,
162    True,
163}
164
165impl From<bool> for BoolCondition {
166    fn from(b: bool) -> Self {
167        if b {
168            Self::True
169        } else {
170            Self::False
171        }
172    }
173}
174
175impl From<BoolCondition> for bool {
176    fn from(b: BoolCondition) -> Self {
177        matches!(b, BoolCondition::True)
178    }
179}
180
181#[repr(C)]
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183pub enum OsCondition {
184    Any,
185    Apple, // macOS + iOS
186    MacOS,
187    IOS,
188    Linux,
189    Windows,
190    Android,
191    Web, // WASM
192}
193
194impl_option!(
195    OsCondition,
196    OptionOsCondition,
197    [Debug, Clone, Copy, PartialEq, Eq, Hash]
198);
199
200impl OsCondition {
201    /// Convert from css::system::Platform
202    pub fn from_system_platform(platform: &crate::system::Platform) -> Self {
203        use crate::system::Platform;
204        match platform {
205            Platform::Windows => OsCondition::Windows,
206            Platform::MacOs => OsCondition::MacOS,
207            Platform::Linux(_) => OsCondition::Linux,
208            Platform::Android => OsCondition::Android,
209            Platform::Ios => OsCondition::IOS,
210            Platform::Unknown => OsCondition::Any,
211        }
212    }
213}
214
215#[repr(C, u8)]
216#[derive(Debug, Clone, PartialEq, Eq, Hash)]
217pub enum OsVersionCondition {
218    /// Minimum version: >= specified version
219    /// Format: OsVersion { os, version_id }
220    Min(OsVersion),
221    /// Maximum version: <= specified version
222    Max(OsVersion),
223    /// Exact version match
224    Exact(OsVersion),
225    /// Desktop environment (Linux only)
226    DesktopEnvironment(LinuxDesktopEnv),
227}
228
229/// OS version with ordering - only comparable within the same OS family
230/// 
231/// Each OS has its own version numbering system with named versions.
232/// Comparisons between different OS families always return false.
233#[repr(C)]
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235pub struct OsVersion {
236    /// Which OS family this version belongs to
237    pub os: OsFamily,
238    /// Numeric version ID for ordering (higher = newer)
239    /// Each OS has its own numbering scheme starting from 0
240    pub version_id: u32,
241}
242
243impl Default for OsVersion {
244    fn default() -> Self {
245        Self::unknown()
246    }
247}
248
249impl OsVersion {
250    pub const fn new(os: OsFamily, version_id: u32) -> Self {
251        Self { os, version_id }
252    }
253    
254    /// Compare two versions - only meaningful within the same OS family
255    /// Returns None if OS families don't match (comparison not meaningful)
256    pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
257        if self.os != other.os {
258            None // Cross-OS comparison not meaningful
259        } else {
260            Some(self.version_id.cmp(&other.version_id))
261        }
262    }
263    
264    /// Check if self >= other (for Min conditions)
265    pub fn is_at_least(&self, other: &Self) -> bool {
266        self.compare(other).map_or(false, |o| o != core::cmp::Ordering::Less)
267    }
268    
269    /// Check if self <= other (for Max conditions)
270    pub fn is_at_most(&self, other: &Self) -> bool {
271        self.compare(other).map_or(false, |o| o != core::cmp::Ordering::Greater)
272    }
273}
274
275impl_option!(
276    OsVersion,
277    OptionOsVersion,
278    [Debug, Clone, Copy, PartialEq, Eq, Hash]
279);
280
281impl OsVersion {
282    
283    /// Check if self == other
284    pub fn is_exactly(&self, other: &Self) -> bool {
285        self.compare(other) == Some(core::cmp::Ordering::Equal)
286    }
287}
288
289/// OS family for version comparisons
290#[repr(C)]
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
292pub enum OsFamily {
293    Windows,
294    MacOS,
295    IOS,
296    Linux,
297    Android,
298}
299
300// ============================================================================
301// Windows Version IDs (chronological order)
302// ============================================================================
303
304/// Windows version constants - use these in CSS like `@os-version(>= win-xp)`
305impl OsVersion {
306    // Windows versions (version_id = NT version * 100 + minor)
307    pub const WIN_2000: Self = Self::new(OsFamily::Windows, 500);       // NT 5.0
308    pub const WIN_XP: Self = Self::new(OsFamily::Windows, 501);         // NT 5.1
309    pub const WIN_XP_64: Self = Self::new(OsFamily::Windows, 502);      // NT 5.2
310    pub const WIN_VISTA: Self = Self::new(OsFamily::Windows, 600);      // NT 6.0
311    pub const WIN_7: Self = Self::new(OsFamily::Windows, 601);          // NT 6.1
312    pub const WIN_8: Self = Self::new(OsFamily::Windows, 602);          // NT 6.2
313    pub const WIN_8_1: Self = Self::new(OsFamily::Windows, 603);        // NT 6.3
314    pub const WIN_10: Self = Self::new(OsFamily::Windows, 1000);        // NT 10.0
315    pub const WIN_10_1507: Self = Self::new(OsFamily::Windows, 1000);   // Initial release
316    pub const WIN_10_1511: Self = Self::new(OsFamily::Windows, 1001);   // November Update
317    pub const WIN_10_1607: Self = Self::new(OsFamily::Windows, 1002);   // Anniversary Update
318    pub const WIN_10_1703: Self = Self::new(OsFamily::Windows, 1003);   // Creators Update
319    pub const WIN_10_1709: Self = Self::new(OsFamily::Windows, 1004);   // Fall Creators Update
320    pub const WIN_10_1803: Self = Self::new(OsFamily::Windows, 1005);   // April 2018 Update
321    pub const WIN_10_1809: Self = Self::new(OsFamily::Windows, 1006);   // October 2018 Update
322    pub const WIN_10_1903: Self = Self::new(OsFamily::Windows, 1007);   // May 2019 Update
323    pub const WIN_10_1909: Self = Self::new(OsFamily::Windows, 1008);   // November 2019 Update
324    pub const WIN_10_2004: Self = Self::new(OsFamily::Windows, 1009);   // May 2020 Update
325    pub const WIN_10_20H2: Self = Self::new(OsFamily::Windows, 1010);   // October 2020 Update
326    pub const WIN_10_21H1: Self = Self::new(OsFamily::Windows, 1011);   // May 2021 Update
327    pub const WIN_10_21H2: Self = Self::new(OsFamily::Windows, 1012);   // November 2021 Update
328    pub const WIN_10_22H2: Self = Self::new(OsFamily::Windows, 1013);   // 2022 Update
329    pub const WIN_11: Self = Self::new(OsFamily::Windows, 1100);        // Windows 11 base
330    pub const WIN_11_21H2: Self = Self::new(OsFamily::Windows, 1100);   // Initial release
331    pub const WIN_11_22H2: Self = Self::new(OsFamily::Windows, 1101);   // 2022 Update
332    pub const WIN_11_23H2: Self = Self::new(OsFamily::Windows, 1102);   // 2023 Update
333    pub const WIN_11_24H2: Self = Self::new(OsFamily::Windows, 1103);   // 2024 Update
334    
335    // macOS versions (version_id = major * 100 + minor)
336    pub const MACOS_CHEETAH: Self = Self::new(OsFamily::MacOS, 1000);       // 10.0
337    pub const MACOS_PUMA: Self = Self::new(OsFamily::MacOS, 1001);          // 10.1
338    pub const MACOS_JAGUAR: Self = Self::new(OsFamily::MacOS, 1002);        // 10.2
339    pub const MACOS_PANTHER: Self = Self::new(OsFamily::MacOS, 1003);       // 10.3
340    pub const MACOS_TIGER: Self = Self::new(OsFamily::MacOS, 1004);         // 10.4
341    pub const MACOS_LEOPARD: Self = Self::new(OsFamily::MacOS, 1005);       // 10.5
342    pub const MACOS_SNOW_LEOPARD: Self = Self::new(OsFamily::MacOS, 1006);  // 10.6
343    pub const MACOS_LION: Self = Self::new(OsFamily::MacOS, 1007);          // 10.7
344    pub const MACOS_MOUNTAIN_LION: Self = Self::new(OsFamily::MacOS, 1008); // 10.8
345    pub const MACOS_MAVERICKS: Self = Self::new(OsFamily::MacOS, 1009);     // 10.9
346    pub const MACOS_YOSEMITE: Self = Self::new(OsFamily::MacOS, 1010);      // 10.10
347    pub const MACOS_EL_CAPITAN: Self = Self::new(OsFamily::MacOS, 1011);    // 10.11
348    pub const MACOS_SIERRA: Self = Self::new(OsFamily::MacOS, 1012);        // 10.12
349    pub const MACOS_HIGH_SIERRA: Self = Self::new(OsFamily::MacOS, 1013);   // 10.13
350    pub const MACOS_MOJAVE: Self = Self::new(OsFamily::MacOS, 1014);        // 10.14
351    pub const MACOS_CATALINA: Self = Self::new(OsFamily::MacOS, 1015);      // 10.15
352    pub const MACOS_BIG_SUR: Self = Self::new(OsFamily::MacOS, 1100);       // 11.0
353    pub const MACOS_MONTEREY: Self = Self::new(OsFamily::MacOS, 1200);      // 12.0
354    pub const MACOS_VENTURA: Self = Self::new(OsFamily::MacOS, 1300);       // 13.0
355    pub const MACOS_SONOMA: Self = Self::new(OsFamily::MacOS, 1400);        // 14.0
356    pub const MACOS_SEQUOIA: Self = Self::new(OsFamily::MacOS, 1500);       // 15.0
357    pub const MACOS_TAHOE: Self = Self::new(OsFamily::MacOS, 2600);         // 26.0
358    
359    // iOS versions (version_id = major * 100 + minor)
360    pub const IOS_1: Self = Self::new(OsFamily::IOS, 100);
361    pub const IOS_2: Self = Self::new(OsFamily::IOS, 200);
362    pub const IOS_3: Self = Self::new(OsFamily::IOS, 300);
363    pub const IOS_4: Self = Self::new(OsFamily::IOS, 400);
364    pub const IOS_5: Self = Self::new(OsFamily::IOS, 500);
365    pub const IOS_6: Self = Self::new(OsFamily::IOS, 600);
366    pub const IOS_7: Self = Self::new(OsFamily::IOS, 700);
367    pub const IOS_8: Self = Self::new(OsFamily::IOS, 800);
368    pub const IOS_9: Self = Self::new(OsFamily::IOS, 900);
369    pub const IOS_10: Self = Self::new(OsFamily::IOS, 1000);
370    pub const IOS_11: Self = Self::new(OsFamily::IOS, 1100);
371    pub const IOS_12: Self = Self::new(OsFamily::IOS, 1200);
372    pub const IOS_13: Self = Self::new(OsFamily::IOS, 1300);
373    pub const IOS_14: Self = Self::new(OsFamily::IOS, 1400);
374    pub const IOS_15: Self = Self::new(OsFamily::IOS, 1500);
375    pub const IOS_16: Self = Self::new(OsFamily::IOS, 1600);
376    pub const IOS_17: Self = Self::new(OsFamily::IOS, 1700);
377    pub const IOS_18: Self = Self::new(OsFamily::IOS, 1800);
378    
379    // Android versions (API level as version_id)
380    pub const ANDROID_CUPCAKE: Self = Self::new(OsFamily::Android, 3);      // 1.5
381    pub const ANDROID_DONUT: Self = Self::new(OsFamily::Android, 4);        // 1.6
382    pub const ANDROID_ECLAIR: Self = Self::new(OsFamily::Android, 7);       // 2.1
383    pub const ANDROID_FROYO: Self = Self::new(OsFamily::Android, 8);        // 2.2
384    pub const ANDROID_GINGERBREAD: Self = Self::new(OsFamily::Android, 10); // 2.3
385    pub const ANDROID_HONEYCOMB: Self = Self::new(OsFamily::Android, 13);   // 3.2
386    pub const ANDROID_ICE_CREAM_SANDWICH: Self = Self::new(OsFamily::Android, 15); // 4.0
387    pub const ANDROID_JELLY_BEAN: Self = Self::new(OsFamily::Android, 18);  // 4.3
388    pub const ANDROID_KITKAT: Self = Self::new(OsFamily::Android, 19);      // 4.4
389    pub const ANDROID_LOLLIPOP: Self = Self::new(OsFamily::Android, 22);    // 5.1
390    pub const ANDROID_MARSHMALLOW: Self = Self::new(OsFamily::Android, 23); // 6.0
391    pub const ANDROID_NOUGAT: Self = Self::new(OsFamily::Android, 25);      // 7.1
392    pub const ANDROID_OREO: Self = Self::new(OsFamily::Android, 27);        // 8.1
393    pub const ANDROID_PIE: Self = Self::new(OsFamily::Android, 28);         // 9.0
394    pub const ANDROID_10: Self = Self::new(OsFamily::Android, 29);          // 10
395    pub const ANDROID_11: Self = Self::new(OsFamily::Android, 30);          // 11
396    pub const ANDROID_12: Self = Self::new(OsFamily::Android, 31);          // 12
397    pub const ANDROID_12L: Self = Self::new(OsFamily::Android, 32);         // 12L
398    pub const ANDROID_13: Self = Self::new(OsFamily::Android, 33);          // 13
399    pub const ANDROID_14: Self = Self::new(OsFamily::Android, 34);          // 14
400    pub const ANDROID_15: Self = Self::new(OsFamily::Android, 35);          // 15
401    
402    // Linux kernel versions (major * 1000 + minor * 10 + patch)
403    pub const LINUX_2_6: Self = Self::new(OsFamily::Linux, 2060);
404    pub const LINUX_3_0: Self = Self::new(OsFamily::Linux, 3000);
405    pub const LINUX_4_0: Self = Self::new(OsFamily::Linux, 4000);
406    pub const LINUX_5_0: Self = Self::new(OsFamily::Linux, 5000);
407    pub const LINUX_6_0: Self = Self::new(OsFamily::Linux, 6000);
408    
409    /// Unknown OS version (for when detection fails or OS is unknown)
410    pub const fn unknown() -> Self {
411        Self {
412            os: OsFamily::Linux, // Fallback, but version_id 0 means "unknown"
413            version_id: 0,
414        }
415    }
416}
417
418/// Parse a named or numeric OS version string
419/// Returns None if the version string is not recognized
420pub fn parse_os_version(os: OsFamily, version_str: &str) -> Option<OsVersion> {
421    let version_str = version_str.trim().to_lowercase();
422    let version_str = version_str.as_str();
423    
424    match os {
425        OsFamily::Windows => parse_windows_version(version_str),
426        OsFamily::MacOS => parse_macos_version(version_str),
427        OsFamily::IOS => parse_ios_version(version_str),
428        OsFamily::Android => parse_android_version(version_str),
429        OsFamily::Linux => parse_linux_version(version_str),
430    }
431}
432
433fn parse_windows_version(s: &str) -> Option<OsVersion> {
434    // Named versions
435    match s {
436        "2000" | "win2000" | "win-2000" => Some(OsVersion::WIN_2000),
437        "xp" | "winxp" | "win-xp" => Some(OsVersion::WIN_XP),
438        "vista" | "winvista" | "win-vista" => Some(OsVersion::WIN_VISTA),
439        "7" | "win7" | "win-7" => Some(OsVersion::WIN_7),
440        "8" | "win8" | "win-8" => Some(OsVersion::WIN_8),
441        "8.1" | "win8.1" | "win-8.1" | "win-8-1" => Some(OsVersion::WIN_8_1),
442        "10" | "win10" | "win-10" => Some(OsVersion::WIN_10),
443        "11" | "win11" | "win-11" => Some(OsVersion::WIN_11),
444        // Numeric NT versions
445        "5.0" | "nt5.0" => Some(OsVersion::WIN_2000),
446        "5.1" | "nt5.1" => Some(OsVersion::WIN_XP),
447        "6.0" | "nt6.0" => Some(OsVersion::WIN_VISTA),
448        "6.1" | "nt6.1" => Some(OsVersion::WIN_7),
449        "6.2" | "nt6.2" => Some(OsVersion::WIN_8),
450        "6.3" | "nt6.3" => Some(OsVersion::WIN_8_1),
451        "10.0" | "nt10.0" => Some(OsVersion::WIN_10),
452        _ => None,
453    }
454}
455
456fn parse_macos_version(s: &str) -> Option<OsVersion> {
457    match s {
458        "cheetah" | "10.0" => Some(OsVersion::MACOS_CHEETAH),
459        "puma" | "10.1" => Some(OsVersion::MACOS_PUMA),
460        "jaguar" | "10.2" => Some(OsVersion::MACOS_JAGUAR),
461        "panther" | "10.3" => Some(OsVersion::MACOS_PANTHER),
462        "tiger" | "10.4" => Some(OsVersion::MACOS_TIGER),
463        "leopard" | "10.5" => Some(OsVersion::MACOS_LEOPARD),
464        "snow-leopard" | "snowleopard" | "10.6" => Some(OsVersion::MACOS_SNOW_LEOPARD),
465        "lion" | "10.7" => Some(OsVersion::MACOS_LION),
466        "mountain-lion" | "mountainlion" | "10.8" => Some(OsVersion::MACOS_MOUNTAIN_LION),
467        "mavericks" | "10.9" => Some(OsVersion::MACOS_MAVERICKS),
468        "yosemite" | "10.10" => Some(OsVersion::MACOS_YOSEMITE),
469        "el-capitan" | "elcapitan" | "10.11" => Some(OsVersion::MACOS_EL_CAPITAN),
470        "sierra" | "10.12" => Some(OsVersion::MACOS_SIERRA),
471        "high-sierra" | "highsierra" | "10.13" => Some(OsVersion::MACOS_HIGH_SIERRA),
472        "mojave" | "10.14" => Some(OsVersion::MACOS_MOJAVE),
473        "catalina" | "10.15" => Some(OsVersion::MACOS_CATALINA),
474        "big-sur" | "bigsur" | "11" | "11.0" => Some(OsVersion::MACOS_BIG_SUR),
475        "monterey" | "12" | "12.0" => Some(OsVersion::MACOS_MONTEREY),
476        "ventura" | "13" | "13.0" => Some(OsVersion::MACOS_VENTURA),
477        "sonoma" | "14" | "14.0" => Some(OsVersion::MACOS_SONOMA),
478        "sequoia" | "15" | "15.0" => Some(OsVersion::MACOS_SEQUOIA),
479        "tahoe" | "26" | "26.0" => Some(OsVersion::MACOS_TAHOE),
480        _ => None,
481    }
482}
483
484fn parse_ios_version(s: &str) -> Option<OsVersion> {
485    match s {
486        "1" | "1.0" => Some(OsVersion::IOS_1),
487        "2" | "2.0" => Some(OsVersion::IOS_2),
488        "3" | "3.0" => Some(OsVersion::IOS_3),
489        "4" | "4.0" => Some(OsVersion::IOS_4),
490        "5" | "5.0" => Some(OsVersion::IOS_5),
491        "6" | "6.0" => Some(OsVersion::IOS_6),
492        "7" | "7.0" => Some(OsVersion::IOS_7),
493        "8" | "8.0" => Some(OsVersion::IOS_8),
494        "9" | "9.0" => Some(OsVersion::IOS_9),
495        "10" | "10.0" => Some(OsVersion::IOS_10),
496        "11" | "11.0" => Some(OsVersion::IOS_11),
497        "12" | "12.0" => Some(OsVersion::IOS_12),
498        "13" | "13.0" => Some(OsVersion::IOS_13),
499        "14" | "14.0" => Some(OsVersion::IOS_14),
500        "15" | "15.0" => Some(OsVersion::IOS_15),
501        "16" | "16.0" => Some(OsVersion::IOS_16),
502        "17" | "17.0" => Some(OsVersion::IOS_17),
503        "18" | "18.0" => Some(OsVersion::IOS_18),
504        _ => None,
505    }
506}
507
508fn parse_android_version(s: &str) -> Option<OsVersion> {
509    match s {
510        "cupcake" | "1.5" => Some(OsVersion::ANDROID_CUPCAKE),
511        "donut" | "1.6" => Some(OsVersion::ANDROID_DONUT),
512        "eclair" | "2.1" => Some(OsVersion::ANDROID_ECLAIR),
513        "froyo" | "2.2" => Some(OsVersion::ANDROID_FROYO),
514        "gingerbread" | "2.3" => Some(OsVersion::ANDROID_GINGERBREAD),
515        "honeycomb" | "3.0" | "3.2" => Some(OsVersion::ANDROID_HONEYCOMB),
516        "ice-cream-sandwich" | "ics" | "4.0" => Some(OsVersion::ANDROID_ICE_CREAM_SANDWICH),
517        "jelly-bean" | "jellybean" | "4.3" => Some(OsVersion::ANDROID_JELLY_BEAN),
518        "kitkat" | "4.4" => Some(OsVersion::ANDROID_KITKAT),
519        "lollipop" | "5.0" | "5.1" => Some(OsVersion::ANDROID_LOLLIPOP),
520        "marshmallow" | "6.0" => Some(OsVersion::ANDROID_MARSHMALLOW),
521        "nougat" | "7.0" | "7.1" => Some(OsVersion::ANDROID_NOUGAT),
522        "oreo" | "8.0" | "8.1" => Some(OsVersion::ANDROID_OREO),
523        "pie" | "9" | "9.0" => Some(OsVersion::ANDROID_PIE),
524        "10" | "q" => Some(OsVersion::ANDROID_10),
525        "11" | "r" => Some(OsVersion::ANDROID_11),
526        "12" | "s" => Some(OsVersion::ANDROID_12),
527        "12l" | "12L" => Some(OsVersion::ANDROID_12L),
528        "13" | "t" | "tiramisu" => Some(OsVersion::ANDROID_13),
529        "14" | "u" | "upside-down-cake" => Some(OsVersion::ANDROID_14),
530        "15" | "v" | "vanilla-ice-cream" => Some(OsVersion::ANDROID_15),
531        _ => {
532            // Try parsing as API level
533            if let Some(api) = s.strip_prefix("api") {
534                if let Ok(level) = api.trim().parse::<u32>() {
535                    return Some(OsVersion::new(OsFamily::Android, level));
536                }
537            }
538            None
539        }
540    }
541}
542
543fn parse_linux_version(s: &str) -> Option<OsVersion> {
544    // Parse kernel version like "5.4", "6.0"
545    let parts: Vec<&str> = s.split('.').collect();
546    if parts.len() >= 2 {
547        if let (Ok(major), Ok(minor)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
548            let patch = parts.get(2).and_then(|p| p.parse::<u32>().ok()).unwrap_or(0);
549            return Some(OsVersion::new(OsFamily::Linux, major * 1000 + minor * 10 + patch));
550        }
551    }
552    None
553}
554
555#[repr(C)]
556#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
557pub enum LinuxDesktopEnv {
558    Gnome,
559    KDE,
560    XFCE,
561    Unity,
562    Cinnamon,
563    MATE,
564    Other,
565}
566
567impl LinuxDesktopEnv {
568    /// Convert from css::system::DesktopEnvironment
569    pub fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
570        use crate::system::DesktopEnvironment;
571        match de {
572            DesktopEnvironment::Gnome => LinuxDesktopEnv::Gnome,
573            DesktopEnvironment::Kde => LinuxDesktopEnv::KDE,
574            DesktopEnvironment::Other(_) => LinuxDesktopEnv::Other,
575        }
576    }
577}
578
579#[repr(C)]
580#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
581pub enum MediaType {
582    Screen,
583    Print,
584    All,
585}
586
587#[repr(C, u8)]
588#[derive(Debug, Clone, PartialEq, Eq, Hash)]
589pub enum ThemeCondition {
590    Light,
591    Dark,
592    Custom(AzString),
593    /// System preference
594    SystemPreferred,
595}
596
597impl_option!(
598    ThemeCondition,
599    OptionThemeCondition,
600    copy = false,
601    [Debug, Clone, PartialEq, Eq, Hash]
602);
603
604impl ThemeCondition {
605    /// Convert from css::system::Theme
606    pub fn from_system_theme(theme: crate::system::Theme) -> Self {
607        use crate::system::Theme;
608        match theme {
609            Theme::Light => ThemeCondition::Light,
610            Theme::Dark => ThemeCondition::Dark,
611        }
612    }
613}
614
615#[repr(C)]
616#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
617pub enum OrientationType {
618    Portrait,
619    Landscape,
620}
621
622/// Language/Locale condition for @lang() CSS selector
623/// Matches BCP 47 language tags with prefix matching
624#[repr(C, u8)]
625#[derive(Debug, Clone, PartialEq, Eq, Hash)]
626pub enum LanguageCondition {
627    /// Exact match (e.g., "de-DE" matches only "de-DE")
628    Exact(AzString),
629    /// Prefix match (e.g., "de" matches "de", "de-DE", "de-AT", etc.)
630    Prefix(AzString),
631}
632
633impl LanguageCondition {
634    /// Check if this condition matches the given language tag
635    pub fn matches(&self, language: &str) -> bool {
636        match self {
637            LanguageCondition::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
638            LanguageCondition::Prefix(prefix) => {
639                let prefix_str = prefix.as_str();
640                if language.len() < prefix_str.len() {
641                    return false;
642                }
643                // Check if language starts with prefix (case-insensitive)
644                let lang_prefix = &language[..prefix_str.len()];
645                if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
646                    return false;
647                }
648                // Must be exact match or followed by '-'
649                language.len() == prefix_str.len()
650                    || language.as_bytes().get(prefix_str.len()) == Some(&b'-')
651            }
652        }
653    }
654}
655
656#[repr(C)]
657#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
658pub enum PseudoStateType {
659    /// No special state (corresponds to "Normal" in NodeDataInlineCssProperty)
660    Normal,
661    /// Element is being hovered (:hover)
662    Hover,
663    /// Element is active/being clicked (:active)
664    Active,
665    /// Element has focus (:focus)
666    Focus,
667    /// Element is disabled (:disabled)
668    Disabled,
669    /// Element is checked/selected (:checked)
670    Checked,
671    /// Element or child has focus (:focus-within)
672    FocusWithin,
673    /// Link has been visited (:visited)
674    Visited,
675    /// Window is not focused (:backdrop) - GTK compatibility
676    Backdrop,
677    /// Element is currently being dragged (:dragging)
678    Dragging,
679    /// A dragged element is over this drop target (:drag-over)
680    DragOver,
681}
682
683impl_option!(
684    LinuxDesktopEnv,
685    OptionLinuxDesktopEnv,
686    [Debug, Clone, Copy, PartialEq, Eq, Hash]
687);
688
689/// Context for evaluating dynamic selectors
690#[repr(C)]
691#[derive(Debug, Clone)]
692pub struct DynamicSelectorContext {
693    /// Operating system info
694    pub os: OsCondition,
695    pub os_version: OsVersion,
696    pub desktop_env: OptionLinuxDesktopEnv,
697
698    /// Theme info
699    pub theme: ThemeCondition,
700
701    /// Media info (from WindowState)
702    pub media_type: MediaType,
703    pub viewport_width: f32,
704    pub viewport_height: f32,
705
706    /// Container info (from parent node)
707    /// NaN = no container
708    pub container_width: f32,
709    pub container_height: f32,
710    pub container_name: OptionString,
711
712    /// Accessibility preferences
713    pub prefers_reduced_motion: BoolCondition,
714    pub prefers_high_contrast: BoolCondition,
715
716    /// Orientation
717    pub orientation: OrientationType,
718
719    /// Node state (hover, active, focus, disabled, checked, focus_within, visited)
720    pub pseudo_state: PseudoStateFlags,
721
722    /// Language/Locale (BCP 47 tag, e.g., "en-US", "de-DE")
723    pub language: AzString,
724
725    /// Whether the window currently has focus (for :backdrop pseudo-class)
726    /// When false, :backdrop styles should be applied
727    pub window_focused: bool,
728}
729
730impl Default for DynamicSelectorContext {
731    fn default() -> Self {
732        Self {
733            os: OsCondition::Any,
734            os_version: OsVersion::unknown(),
735            desktop_env: OptionLinuxDesktopEnv::None,
736            theme: ThemeCondition::Light,
737            media_type: MediaType::Screen,
738            viewport_width: 800.0,
739            viewport_height: 600.0,
740            container_width: f32::NAN,
741            container_height: f32::NAN,
742            container_name: OptionString::None,
743            prefers_reduced_motion: BoolCondition::False,
744            prefers_high_contrast: BoolCondition::False,
745            orientation: OrientationType::Landscape,
746            pseudo_state: PseudoStateFlags::default(),
747            language: AzString::from_const_str("en-US"),
748            window_focused: true,
749        }
750    }
751}
752
753impl DynamicSelectorContext {
754    /// Create a context from SystemStyle
755    pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
756        let os = OsCondition::from_system_platform(&system_style.platform);
757        let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
758            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
759        } else {
760            OptionLinuxDesktopEnv::None
761        };
762        let theme = ThemeCondition::from_system_theme(system_style.theme);
763
764        Self {
765            os,
766            os_version: system_style.os_version, // Use version from SystemStyle
767            desktop_env,
768            theme,
769            media_type: MediaType::Screen,
770            viewport_width: 800.0, // Will be updated with window size
771            viewport_height: 600.0,
772            container_width: f32::NAN,
773            container_height: f32::NAN,
774            container_name: OptionString::None,
775            prefers_reduced_motion: system_style.prefers_reduced_motion,
776            prefers_high_contrast: system_style.prefers_high_contrast,
777            orientation: OrientationType::Landscape,
778            pseudo_state: PseudoStateFlags::default(),
779            language: system_style.language.clone(),
780            window_focused: true,
781        }
782    }
783
784    /// Update viewport dimensions (e.g., on window resize)
785    pub fn with_viewport(&self, width: f32, height: f32) -> Self {
786        let mut ctx = self.clone();
787        ctx.viewport_width = width;
788        ctx.viewport_height = height;
789        ctx.orientation = if width > height {
790            OrientationType::Landscape
791        } else {
792            OrientationType::Portrait
793        };
794        ctx
795    }
796
797    /// Update container dimensions (for @container queries)
798    pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
799        let mut ctx = self.clone();
800        ctx.container_width = width;
801        ctx.container_height = height;
802        ctx.container_name = name.into();
803        ctx
804    }
805
806    /// Update pseudo-state (hover, active, focus, etc.)
807    pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
808        let mut ctx = self.clone();
809        ctx.pseudo_state = state;
810        ctx
811    }
812
813    /// Check if viewport changed significantly (for breakpoint detection)
814    pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
815        for bp in breakpoints {
816            let self_above = self.viewport_width >= *bp;
817            let other_above = other.viewport_width >= *bp;
818            if self_above != other_above {
819                return true;
820            }
821        }
822        false
823    }
824}
825
826impl DynamicSelector {
827    /// Check if this selector matches in the given context
828    pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
829        match self {
830            Self::Os(os) => Self::match_os(*os, ctx.os),
831            Self::OsVersion(ver) => Self::match_os_version(ver, &ctx.os_version, &ctx.desktop_env),
832            Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
833            Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
834            Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
835            Self::ContainerWidth(range) => {
836                !ctx.container_width.is_nan() && range.matches(ctx.container_width)
837            }
838            Self::ContainerHeight(range) => {
839                !ctx.container_height.is_nan() && range.matches(ctx.container_height)
840            }
841            Self::ContainerName(name) => ctx.container_name.as_ref().map_or(false, |n| n == name),
842            Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
843            Self::AspectRatio(range) => {
844                let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
845                range.matches(ratio)
846            }
847            Self::Orientation(orient) => *orient == ctx.orientation,
848            Self::PrefersReducedMotion(pref) => {
849                bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
850            }
851            Self::PrefersHighContrast(pref) => {
852                bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
853            }
854            Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
855            Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
856        }
857    }
858
859    fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
860        match condition {
861            OsCondition::Any => true,
862            OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
863            _ => condition == actual,
864        }
865    }
866
867    fn match_os_version(
868        condition: &OsVersionCondition,
869        actual: &OsVersion,
870        desktop_env: &OptionLinuxDesktopEnv,
871    ) -> bool {
872        match condition {
873            OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
874            OsVersionCondition::Min(ver) => actual.is_at_least(ver),
875            OsVersionCondition::Max(ver) => actual.is_at_most(ver),
876            OsVersionCondition::DesktopEnvironment(env) => {
877                desktop_env.as_ref().map_or(false, |de| de == env)
878            }
879        }
880    }
881
882    fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
883        match (condition, actual) {
884            (ThemeCondition::SystemPreferred, _) => true,
885            _ => condition == actual,
886        }
887    }
888
889    fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
890        let node_state = &ctx.pseudo_state;
891        match state {
892            PseudoStateType::Normal => true, // Normal is always active (base state)
893            PseudoStateType::Hover => node_state.hover,
894            PseudoStateType::Active => node_state.active,
895            PseudoStateType::Focus => node_state.focused,
896            PseudoStateType::Disabled => node_state.disabled,
897            PseudoStateType::Checked => node_state.checked,
898            PseudoStateType::FocusWithin => node_state.focus_within,
899            PseudoStateType::Visited => node_state.visited,
900            // :backdrop is true when window is NOT focused (opposite of window_focused)
901            PseudoStateType::Backdrop => !ctx.window_focused,
902            PseudoStateType::Dragging => node_state.dragging,
903            PseudoStateType::DragOver => node_state.drag_over,
904        }
905    }
906}
907
908// ============================================================================
909// CssPropertyWithConditions - Replacement for NodeDataInlineCssProperty
910// ============================================================================
911
912/// A CSS property with optional conditions for when it should be applied.
913/// This replaces `NodeDataInlineCssProperty` with a more flexible system.
914///
915/// If `apply_if` is empty, the property always applies.
916/// If `apply_if` contains conditions, ALL conditions must be satisfied for the property to apply.
917#[repr(C)]
918#[derive(Debug, Clone, PartialEq)]
919pub struct CssPropertyWithConditions {
920    /// The actual CSS property value
921    pub property: CssProperty,
922    /// Conditions that must all be satisfied for this property to apply.
923    /// Empty means unconditional (always apply).
924    pub apply_if: DynamicSelectorVec,
925}
926
927impl_option!(
928    CssPropertyWithConditions,
929    OptionCssPropertyWithConditions,
930    copy = false,
931    [Debug, Clone, PartialEq, PartialOrd]
932);
933
934impl Eq for CssPropertyWithConditions {}
935
936impl PartialOrd for CssPropertyWithConditions {
937    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
938        Some(self.cmp(other))
939    }
940}
941
942impl Ord for CssPropertyWithConditions {
943    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
944        // Compare by condition count only (simple stable ordering)
945        self.apply_if
946            .as_slice()
947            .len()
948            .cmp(&other.apply_if.as_slice().len())
949    }
950}
951
952impl CssPropertyWithConditions {
953    /// Create an unconditional property (always applies) - const version
954    pub const fn simple(property: CssProperty) -> Self {
955        Self {
956            property,
957            apply_if: DynamicSelectorVec::from_const_slice(&[]),
958        }
959    }
960
961    /// Create a property with a single condition (const version using slice reference)
962    pub const fn with_single_condition(
963        property: CssProperty,
964        conditions: &'static [DynamicSelector],
965    ) -> Self {
966        Self {
967            property,
968            apply_if: DynamicSelectorVec::from_const_slice(conditions),
969        }
970    }
971
972    /// Create a property with a single condition (non-const, allocates)
973    pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
974        Self {
975            property,
976            apply_if: DynamicSelectorVec::from_vec(vec![condition]),
977        }
978    }
979
980    /// Create a property with multiple conditions (all must match)
981    pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
982        Self {
983            property,
984            apply_if: conditions,
985        }
986    }
987
988    /// Create a property that applies only on hover (const version)
989    pub const fn on_hover(property: CssProperty) -> Self {
990        Self::with_single_condition(
991            property,
992            &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
993        )
994    }
995
996    /// Create a property that applies only when active (const version)
997    pub const fn on_active(property: CssProperty) -> Self {
998        Self::with_single_condition(
999            property,
1000            &[DynamicSelector::PseudoState(PseudoStateType::Active)],
1001        )
1002    }
1003
1004    /// Create a property that applies only when focused (const version)
1005    pub const fn on_focus(property: CssProperty) -> Self {
1006        Self::with_single_condition(
1007            property,
1008            &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
1009        )
1010    }
1011
1012    /// Create a property that applies only when disabled (const version)
1013    pub const fn when_disabled(property: CssProperty) -> Self {
1014        Self::with_single_condition(
1015            property,
1016            &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
1017        )
1018    }
1019
1020    /// Create a property that applies only on a specific OS (non-const, needs runtime value)
1021    pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
1022        Self::with_condition(property, DynamicSelector::Os(os))
1023    }
1024
1025    /// Create a property that applies only in dark theme (const version)
1026    pub const fn dark_theme(property: CssProperty) -> Self {
1027        Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
1028    }
1029
1030    /// Create a property that applies only in light theme (const version)
1031    pub const fn light_theme(property: CssProperty) -> Self {
1032        Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
1033    }
1034
1035    /// Create a property for Windows only (const version)
1036    pub const fn on_windows(property: CssProperty) -> Self {
1037        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
1038    }
1039
1040    /// Create a property for macOS only (const version)
1041    pub const fn on_macos(property: CssProperty) -> Self {
1042        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
1043    }
1044
1045    /// Create a property for Linux only (const version)
1046    pub const fn on_linux(property: CssProperty) -> Self {
1047        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
1048    }
1049
1050    /// Check if this property matches in the given context
1051    pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1052        // Empty conditions = always matches
1053        if self.apply_if.as_slice().is_empty() {
1054            return true;
1055        }
1056
1057        // All conditions must match
1058        self.apply_if
1059            .as_slice()
1060            .iter()
1061            .all(|selector| selector.matches(ctx))
1062    }
1063
1064    /// Check if this property has any conditions
1065    pub fn is_conditional(&self) -> bool {
1066        !self.apply_if.as_slice().is_empty()
1067    }
1068
1069    /// Check if this property is a pseudo-state conditional only
1070    /// (hover, active, focus, etc.)
1071    pub fn is_pseudo_state_only(&self) -> bool {
1072        let conditions = self.apply_if.as_slice();
1073        !conditions.is_empty()
1074            && conditions
1075                .iter()
1076                .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
1077    }
1078
1079    /// Check if this property affects layout (width, height, margin, etc.)
1080    /// 
1081    /// Returns `true` for layout-affecting properties like width, height, margin, padding,
1082    /// font-size, etc. Returns `false` for paint-only properties like color, background,
1083    /// box-shadow, opacity, transform, etc.
1084    pub fn is_layout_affecting(&self) -> bool {
1085        self.property.get_type().can_trigger_relayout()
1086    }
1087}
1088
1089impl_vec!(CssPropertyWithConditions, CssPropertyWithConditionsVec, CssPropertyWithConditionsVecDestructor, CssPropertyWithConditionsVecDestructorType, CssPropertyWithConditionsVecSlice, OptionCssPropertyWithConditions);
1090impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1091impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1092impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1093impl_vec_clone!(
1094    CssPropertyWithConditions,
1095    CssPropertyWithConditionsVec,
1096    CssPropertyWithConditionsVecDestructor
1097);
1098
1099// Manual implementations for Eq and Ord (required for NodeData derives)
1100impl Eq for CssPropertyWithConditionsVec {}
1101
1102impl Ord for CssPropertyWithConditionsVec {
1103    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1104        self.as_slice().len().cmp(&other.as_slice().len())
1105    }
1106}
1107
1108impl core::hash::Hash for CssPropertyWithConditions {
1109    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1110        self.property.hash(state);
1111        // DynamicSelectorVec doesn't implement Hash, so we hash the length
1112        self.apply_if.as_slice().len().hash(state);
1113    }
1114}
1115
1116impl core::hash::Hash for CssPropertyWithConditionsVec {
1117    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1118        for item in self.as_slice() {
1119            item.hash(state);
1120        }
1121    }
1122}
1123
1124impl CssPropertyWithConditionsVec {
1125    /// Parse CSS with support for selectors and nesting.
1126    /// 
1127    /// Supports:
1128    /// - Simple properties: `color: red;`
1129    /// - Pseudo-selectors: `:hover { background: blue; }`
1130    /// - @-rules: `@os linux { font-size: 14px; }`
1131    /// - Nesting: `@os linux { font-size: 14px; :hover { color: red; }}`
1132    /// 
1133    /// Examples:
1134    /// ```ignore
1135    /// // Simple inline styles
1136    /// CssPropertyWithConditionsVec::parse("color: red; font-size: 14px;")
1137    /// 
1138    /// // With hover state
1139    /// CssPropertyWithConditionsVec::parse(":hover { background: blue; }")
1140    /// 
1141    /// // OS-specific with nested hover
1142    /// CssPropertyWithConditionsVec::parse("@os linux { font-size: 14px; :hover { color: red; }}")
1143    /// ```
1144    #[cfg(feature = "parser")]
1145    pub fn parse(style: &str) -> Self {
1146        Self::parse_with_conditions(style, Vec::new())
1147    }
1148    
1149    /// Internal recursive parser with inherited conditions
1150    #[cfg(feature = "parser")]
1151    fn parse_with_conditions(style: &str, inherited_conditions: Vec<DynamicSelector>) -> Self {
1152        use crate::props::property::{
1153            parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
1154            CssPropertyType,
1155        };
1156
1157        let mut props = Vec::new();
1158        let key_map = CssKeyMap::get();
1159        let style = style.trim();
1160        
1161        if style.is_empty() {
1162            return CssPropertyWithConditionsVec::from_vec(props);
1163        }
1164
1165        // Tokenize into segments: properties, pseudo-selectors, and @-rules
1166        let mut chars = style.chars().peekable();
1167        let mut current_segment = String::new();
1168        let mut brace_depth = 0;
1169        
1170        while let Some(c) = chars.next() {
1171            match c {
1172                '{' => {
1173                    brace_depth += 1;
1174                    current_segment.push(c);
1175                }
1176                '}' => {
1177                    brace_depth -= 1;
1178                    current_segment.push(c);
1179                    
1180                    if brace_depth == 0 {
1181                        // End of a block - process it
1182                        let segment = current_segment.trim().to_string();
1183                        current_segment.clear();
1184                        
1185                        if let Some(parsed) = Self::parse_block_segment(&segment, &inherited_conditions, &key_map) {
1186                            props.extend(parsed);
1187                        }
1188                    }
1189                }
1190                ';' if brace_depth == 0 => {
1191                    // End of a simple property
1192                    let segment = current_segment.trim().to_string();
1193                    current_segment.clear();
1194                    
1195                    if !segment.is_empty() {
1196                        if let Some(parsed) = Self::parse_property_segment(&segment, &inherited_conditions, &key_map) {
1197                            props.extend(parsed);
1198                        }
1199                    }
1200                }
1201                _ => {
1202                    current_segment.push(c);
1203                }
1204            }
1205        }
1206        
1207        // Handle any remaining segment (property without trailing semicolon)
1208        let remaining = current_segment.trim();
1209        if !remaining.is_empty() && !remaining.contains('{') {
1210            if let Some(parsed) = Self::parse_property_segment(remaining, &inherited_conditions, &key_map) {
1211                props.extend(parsed);
1212            }
1213        }
1214
1215        CssPropertyWithConditionsVec::from_vec(props)
1216    }
1217    
1218    /// Parse a block segment like `:hover { ... }` or `@os linux { ... }`
1219    #[cfg(feature = "parser")]
1220    fn parse_block_segment(
1221        segment: &str,
1222        inherited_conditions: &[DynamicSelector],
1223        key_map: &crate::props::property::CssKeyMap,
1224    ) -> Option<Vec<CssPropertyWithConditions>> {
1225        // Find the opening brace
1226        let brace_pos = segment.find('{')?;
1227        let selector = segment[..brace_pos].trim();
1228        
1229        // Extract content between braces (excluding the braces themselves)
1230        let content_start = brace_pos + 1;
1231        let content_end = segment.rfind('}')?;
1232        if content_end <= content_start {
1233            return None;
1234        }
1235        let content = &segment[content_start..content_end];
1236        
1237        // Parse selector to get conditions
1238        let mut conditions = inherited_conditions.to_vec();
1239        
1240        if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
1241            conditions.extend(new_conditions);
1242        } else {
1243            // Unknown selector, skip this block
1244            return None;
1245        }
1246        
1247        // Recursively parse the content with the new conditions
1248        let parsed = Self::parse_with_conditions(content, conditions);
1249        Some(parsed.into_library_owned_vec())
1250    }
1251    
1252    /// Parse a selector string into DynamicSelector conditions
1253    #[cfg(feature = "parser")]
1254    fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
1255        let selector = selector.trim();
1256        
1257        // Handle pseudo-selectors
1258        if selector.starts_with(':') {
1259            let pseudo = &selector[1..];
1260            match pseudo {
1261                "hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
1262                "active" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)]),
1263                "focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
1264                "focus-within" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::FocusWithin)]),
1265                "disabled" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Disabled)]),
1266                "checked" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Checked)]),
1267                "visited" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)]),
1268                "backdrop" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Backdrop)]),
1269                "dragging" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Dragging)]),
1270                "drag-over" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::DragOver)]),
1271                _ => return None,
1272            }
1273        }
1274        
1275        // Handle @-rules
1276        if selector.starts_with('@') {
1277            let rule_content = &selector[1..];
1278            
1279            // @os linux, @os windows, etc.
1280            if rule_content.starts_with("os ") {
1281                let os_name = rule_content[3..].trim();
1282                if let Some(os_cond) = Self::parse_os_name(os_name) {
1283                    return Some(vec![DynamicSelector::Os(os_cond)]);
1284                }
1285            }
1286            
1287            // @media (min-width: 800px), etc.
1288            if rule_content.starts_with("media ") {
1289                let media_query = rule_content[6..].trim();
1290                if let Some(media_conds) = Self::parse_media_query(media_query) {
1291                    return Some(media_conds);
1292                }
1293            }
1294            
1295            // @theme dark, @theme light
1296            if rule_content.starts_with("theme ") {
1297                let theme = rule_content[6..].trim();
1298                match theme {
1299                    "dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
1300                    "light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
1301                    _ => return None,
1302                }
1303            }
1304            
1305            return None;
1306        }
1307        
1308        // Handle universal selector * (treat as unconditional)
1309        if selector == "*" {
1310            return Some(vec![]);
1311        }
1312        
1313        // Empty selector means unconditional
1314        if selector.is_empty() {
1315            return Some(vec![]);
1316        }
1317        
1318        None
1319    }
1320    
1321    /// Parse OS name to OsCondition
1322    #[cfg(feature = "parser")]
1323    fn parse_os_name(name: &str) -> Option<OsCondition> {
1324        match name.to_lowercase().as_str() {
1325            "linux" => Some(OsCondition::Linux),
1326            "windows" | "win" => Some(OsCondition::Windows),
1327            "macos" | "mac" | "osx" => Some(OsCondition::MacOS),
1328            "ios" => Some(OsCondition::IOS),
1329            "android" => Some(OsCondition::Android),
1330            "apple" => Some(OsCondition::Apple),
1331            "web" | "wasm" => Some(OsCondition::Web),
1332            "any" | "*" => Some(OsCondition::Any),
1333            _ => None,
1334        }
1335    }
1336    
1337    /// Parse simple media query
1338    #[cfg(feature = "parser")]
1339    fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
1340        let query = query.trim();
1341        
1342        // Handle (min-width: XXXpx)
1343        if query.starts_with('(') && query.ends_with(')') {
1344            let inner = &query[1..query.len()-1];
1345            if let Some((key, value)) = inner.split_once(':') {
1346                let key = key.trim();
1347                let value = value.trim();
1348                
1349                // Parse pixel value
1350                let px_value = value.strip_suffix("px")
1351                    .and_then(|v| v.trim().parse::<f32>().ok());
1352                
1353                match key {
1354                    "min-width" => {
1355                        if let Some(px) = px_value {
1356                            return Some(vec![DynamicSelector::ViewportWidth(
1357                                MinMaxRange::with_min(px)
1358                            )]);
1359                        }
1360                    }
1361                    "max-width" => {
1362                        if let Some(px) = px_value {
1363                            return Some(vec![DynamicSelector::ViewportWidth(
1364                                MinMaxRange::with_max(px)
1365                            )]);
1366                        }
1367                    }
1368                    "min-height" => {
1369                        if let Some(px) = px_value {
1370                            return Some(vec![DynamicSelector::ViewportHeight(
1371                                MinMaxRange::with_min(px)
1372                            )]);
1373                        }
1374                    }
1375                    "max-height" => {
1376                        if let Some(px) = px_value {
1377                            return Some(vec![DynamicSelector::ViewportHeight(
1378                                MinMaxRange::with_max(px)
1379                            )]);
1380                        }
1381                    }
1382                    _ => {}
1383                }
1384            }
1385        }
1386        
1387        // Handle screen, print, all
1388        match query {
1389            "screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
1390            "print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
1391            "all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
1392            _ => None,
1393        }
1394    }
1395    
1396    /// Parse a simple property like "color: red"
1397    #[cfg(feature = "parser")]
1398    fn parse_property_segment(
1399        segment: &str,
1400        inherited_conditions: &[DynamicSelector],
1401        key_map: &crate::props::property::CssKeyMap,
1402    ) -> Option<Vec<CssPropertyWithConditions>> {
1403        use crate::props::property::{
1404            parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
1405            CssPropertyType,
1406        };
1407
1408        let segment = segment.trim();
1409        if segment.is_empty() {
1410            return None;
1411        }
1412        
1413        let (key, value) = segment.split_once(':')?;
1414        let key = key.trim();
1415        let value = value.trim();
1416        
1417        let mut props = Vec::new();
1418        let conditions = if inherited_conditions.is_empty() {
1419            DynamicSelectorVec::from_const_slice(&[])
1420        } else {
1421            DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
1422        };
1423        
1424        // First, try to parse as a regular (non-shorthand) property
1425        if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
1426            if let Ok(prop) = parse_css_property(prop_type, value) {
1427                props.push(CssPropertyWithConditions {
1428                    property: prop,
1429                    apply_if: conditions.clone(),
1430                });
1431                return Some(props);
1432            }
1433        }
1434        
1435        // If not found, try as a shorthand (combined) property
1436        if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
1437            if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
1438                for prop in expanded_props {
1439                    props.push(CssPropertyWithConditions {
1440                        property: prop,
1441                        apply_if: conditions.clone(),
1442                    });
1443                }
1444                return Some(props);
1445            }
1446        }
1447        
1448        None
1449    }
1450
1451    /// Parse CSS properties from a string, all with "normal" (unconditional) state
1452    /// 
1453    /// Deprecated: Use `parse()` instead which supports selectors and nesting
1454    #[cfg(feature = "parser")]
1455    pub fn parse_normal(style: &str) -> Self {
1456        Self::parse(style)
1457    }
1458
1459    /// Parse CSS properties from a string, all with hover condition
1460    /// 
1461    /// Deprecated: Use `parse(":hover { ... }")` instead
1462    #[cfg(feature = "parser")]
1463    pub fn parse_hover(style: &str) -> Self {
1464        // Wrap in :hover { } and parse
1465        let wrapped = format!(":hover {{ {} }}", style);
1466        Self::parse(&wrapped)
1467    }
1468
1469    /// Parse CSS properties from a string, all with active condition
1470    /// 
1471    /// Deprecated: Use `parse(":active { ... }")` instead
1472    #[cfg(feature = "parser")]
1473    pub fn parse_active(style: &str) -> Self {
1474        let wrapped = format!(":active {{ {} }}", style);
1475        Self::parse(&wrapped)
1476    }
1477
1478    /// Parse CSS properties from a string, all with focus condition
1479    /// 
1480    /// Deprecated: Use `parse(":focus { ... }")` instead
1481    #[cfg(feature = "parser")]
1482    pub fn parse_focus(style: &str) -> Self {
1483        let wrapped = format!(":focus {{ {} }}", style);
1484        Self::parse(&wrapped)
1485    }
1486}
1487
1488#[cfg(test)]
1489mod tests {
1490    use super::*;
1491
1492    #[test]
1493    fn test_inline_overflow_parse() {
1494        let style = "overflow: scroll;";
1495        let parsed = CssPropertyWithConditionsVec::parse(style);
1496        let props = parsed.into_library_owned_vec();
1497        eprintln!("Parsed {} properties from '{}'", props.len(), style);
1498        for prop in &props {
1499            eprintln!("  {:?}", prop.property);
1500        }
1501        assert!(props.len() > 0, "Expected overflow to parse into at least 1 property");
1502    }
1503
1504    #[test]
1505    fn test_inline_overflow_y_parse() {
1506        let style = "overflow-y: scroll;";
1507        let parsed = CssPropertyWithConditionsVec::parse(style);
1508        let props = parsed.into_library_owned_vec();
1509        eprintln!("Parsed {} properties from '{}'", props.len(), style);
1510        for prop in &props {
1511            eprintln!("  {:?}", prop.property);
1512        }
1513        assert!(props.len() > 0, "Expected overflow-y to parse into at least 1 property");
1514    }
1515
1516    #[test]
1517    fn test_inline_combined_style_with_overflow() {
1518        let style = "padding: 20px; background-color: #f0f0f0; font-size: 14px; color: #222;overflow: scroll;";
1519        let parsed = CssPropertyWithConditionsVec::parse(style);
1520        let props = parsed.into_library_owned_vec();
1521        eprintln!("Parsed {} properties from combined style", props.len());
1522        for prop in &props {
1523            eprintln!("  {:?}", prop.property);
1524        }
1525        // padding:20px expands to 4, background:1, font-size:1, color:1, overflow:2 = 10
1526        assert!(props.len() >= 9, "Expected at least 9 properties, got {}", props.len());
1527    }
1528
1529    #[test]
1530    fn test_inline_grid_template_columns_parse() {
1531        use crate::props::layout::grid::GridTrackSizing;
1532        let style = "display: grid; grid-template-columns: repeat(4, 160px); gap: 16px; padding: 10px;";
1533        let parsed = CssPropertyWithConditionsVec::parse(style);
1534        let props = parsed.into_library_owned_vec();
1535        eprintln!("Parsed {} properties from grid style", props.len());
1536        for prop in &props {
1537            eprintln!("  {:?}", prop.property);
1538        }
1539        // Find grid-template-columns property
1540        let grid_cols = props.iter().find(|p| {
1541            matches!(p.property, CssProperty::GridTemplateColumns(_))
1542        }).expect("Expected GridTemplateColumns property");
1543        
1544        if let CssProperty::GridTemplateColumns(ref value) = grid_cols.property {
1545            let template = value.get_property().expect("Expected Exact value");
1546            let tracks = template.tracks.as_ref();
1547            assert_eq!(tracks.len(), 4, "Expected 4 tracks");
1548            for (i, track) in tracks.iter().enumerate() {
1549                eprintln!("  Track {}: {:?} (is_fixed={})", i, track, matches!(track, GridTrackSizing::Fixed(_)));
1550                assert!(matches!(track, GridTrackSizing::Fixed(_)), 
1551                    "Track {} should be Fixed(160px), got {:?}", i, track);
1552            }
1553        } else {
1554            panic!("Expected CssProperty::GridTemplateColumns");
1555        }
1556    }
1557}