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