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}
19
20impl PseudoStateFlags {
21    /// Check if a specific pseudo-state is active
22    pub fn has_state(&self, state: PseudoStateType) -> bool {
23        match state {
24            PseudoStateType::Normal => true,
25            PseudoStateType::Hover => self.hover,
26            PseudoStateType::Active => self.active,
27            PseudoStateType::Focus => self.focused,
28            PseudoStateType::Disabled => self.disabled,
29            PseudoStateType::Checked => self.checked,
30            PseudoStateType::FocusWithin => self.focus_within,
31            PseudoStateType::Visited => self.visited,
32        }
33    }
34}
35
36/// Dynamic selector that is evaluated at runtime
37/// C-compatible: Tagged union with single field
38#[repr(C, u8)]
39#[derive(Debug, Clone, PartialEq)]
40pub enum DynamicSelector {
41    /// Operating system condition
42    Os(OsCondition) = 0,
43    /// Operating system version (e.g. macOS 14.0, Windows 11)
44    OsVersion(OsVersionCondition) = 1,
45    /// Media query (print/screen)
46    Media(MediaType) = 2,
47    /// Viewport width min/max (for @media)
48    ViewportWidth(MinMaxRange) = 3,
49    /// Viewport height min/max (for @media)
50    ViewportHeight(MinMaxRange) = 4,
51    /// Container width min/max (for @container)
52    ContainerWidth(MinMaxRange) = 5,
53    /// Container height min/max (for @container)
54    ContainerHeight(MinMaxRange) = 6,
55    /// Container name (for named @container queries)
56    ContainerName(AzString) = 7,
57    /// Theme (dark/light/custom)
58    Theme(ThemeCondition) = 8,
59    /// Aspect Ratio (min/max for @media and @container)
60    AspectRatio(MinMaxRange) = 9,
61    /// Orientation (portrait/landscape)
62    Orientation(OrientationType) = 10,
63    /// Reduced Motion (accessibility)
64    PrefersReducedMotion(BoolCondition) = 11,
65    /// High Contrast (accessibility)
66    PrefersHighContrast(BoolCondition) = 12,
67    /// Pseudo-State (hover, active, focus, etc.)
68    PseudoState(PseudoStateType) = 13,
69    /// Language/Locale (for @lang("de-DE"))
70    /// Matches BCP 47 language tags (e.g., "de", "de-DE", "en-US")
71    Language(LanguageCondition) = 14,
72}
73
74impl_vec!(
75    DynamicSelector,
76    DynamicSelectorVec,
77    DynamicSelectorVecDestructor,
78    DynamicSelectorVecDestructorType
79);
80impl_vec_clone!(
81    DynamicSelector,
82    DynamicSelectorVec,
83    DynamicSelectorVecDestructor
84);
85impl_vec_debug!(DynamicSelector, DynamicSelectorVec);
86impl_vec_partialeq!(DynamicSelector, DynamicSelectorVec);
87
88/// Min/Max Range for numeric conditions (C-compatible)
89#[repr(C)]
90#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
91pub struct MinMaxRange {
92    /// Minimum value (NaN = no minimum limit)
93    pub min: f32,
94    /// Maximum value (NaN = no maximum limit)
95    pub max: f32,
96}
97
98impl MinMaxRange {
99    pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
100        Self {
101            min: if let Some(m) = min { m } else { f32::NAN },
102            max: if let Some(m) = max { m } else { f32::NAN },
103        }
104    }
105
106    pub fn min(&self) -> Option<f32> {
107        if self.min.is_nan() {
108            None
109        } else {
110            Some(self.min)
111        }
112    }
113
114    pub fn max(&self) -> Option<f32> {
115        if self.max.is_nan() {
116            None
117        } else {
118            Some(self.max)
119        }
120    }
121
122    pub fn matches(&self, value: f32) -> bool {
123        let min_ok = self.min.is_nan() || value >= self.min;
124        let max_ok = self.max.is_nan() || value <= self.max;
125        min_ok && max_ok
126    }
127}
128
129/// Boolean condition (C-compatible)
130#[repr(C)]
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132pub enum BoolCondition {
133    False,
134    True,
135}
136
137impl From<bool> for BoolCondition {
138    fn from(b: bool) -> Self {
139        if b {
140            Self::True
141        } else {
142            Self::False
143        }
144    }
145}
146
147impl From<BoolCondition> for bool {
148    fn from(b: BoolCondition) -> Self {
149        matches!(b, BoolCondition::True)
150    }
151}
152
153#[repr(C)]
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
155pub enum OsCondition {
156    Any,
157    Apple, // macOS + iOS
158    MacOS,
159    IOS,
160    Linux,
161    Windows,
162    Android,
163    Web, // WASM
164}
165
166impl OsCondition {
167    /// Convert from css::system::Platform
168    pub fn from_system_platform(platform: &crate::system::Platform) -> Self {
169        use crate::system::Platform;
170        match platform {
171            Platform::Windows => OsCondition::Windows,
172            Platform::MacOs => OsCondition::MacOS,
173            Platform::Linux(_) => OsCondition::Linux,
174            Platform::Android => OsCondition::Android,
175            Platform::Ios => OsCondition::IOS,
176            Platform::Unknown => OsCondition::Any,
177        }
178    }
179}
180
181#[repr(C, u8)]
182#[derive(Debug, Clone, PartialEq, Eq, Hash)]
183pub enum OsVersionCondition {
184    /// Semantic version: "14.0", "11.0.22000"
185    Exact(AzString),
186    /// Minimum version: >= "14.0"
187    Min(AzString),
188    /// Maximum version: <= "14.0"
189    Max(AzString),
190    /// Desktop environment (Linux)
191    DesktopEnvironment(LinuxDesktopEnv),
192}
193
194#[repr(C)]
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
196pub enum LinuxDesktopEnv {
197    Gnome,
198    KDE,
199    XFCE,
200    Unity,
201    Cinnamon,
202    MATE,
203    Other,
204}
205
206impl LinuxDesktopEnv {
207    /// Convert from css::system::DesktopEnvironment
208    pub fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
209        use crate::system::DesktopEnvironment;
210        match de {
211            DesktopEnvironment::Gnome => LinuxDesktopEnv::Gnome,
212            DesktopEnvironment::Kde => LinuxDesktopEnv::KDE,
213            DesktopEnvironment::Other(_) => LinuxDesktopEnv::Other,
214        }
215    }
216}
217
218#[repr(C)]
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220pub enum MediaType {
221    Screen,
222    Print,
223    All,
224}
225
226#[repr(C, u8)]
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
228pub enum ThemeCondition {
229    Light,
230    Dark,
231    Custom(AzString),
232    /// System preference
233    SystemPreferred,
234}
235
236impl ThemeCondition {
237    /// Convert from css::system::Theme
238    pub fn from_system_theme(theme: crate::system::Theme) -> Self {
239        use crate::system::Theme;
240        match theme {
241            Theme::Light => ThemeCondition::Light,
242            Theme::Dark => ThemeCondition::Dark,
243        }
244    }
245}
246
247#[repr(C)]
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub enum OrientationType {
250    Portrait,
251    Landscape,
252}
253
254/// Language/Locale condition for @lang() CSS selector
255/// Matches BCP 47 language tags with prefix matching
256#[repr(C, u8)]
257#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258pub enum LanguageCondition {
259    /// Exact match (e.g., "de-DE" matches only "de-DE")
260    Exact(AzString),
261    /// Prefix match (e.g., "de" matches "de", "de-DE", "de-AT", etc.)
262    Prefix(AzString),
263}
264
265impl LanguageCondition {
266    /// Check if this condition matches the given language tag
267    pub fn matches(&self, language: &str) -> bool {
268        match self {
269            LanguageCondition::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
270            LanguageCondition::Prefix(prefix) => {
271                let prefix_str = prefix.as_str();
272                if language.len() < prefix_str.len() {
273                    return false;
274                }
275                // Check if language starts with prefix (case-insensitive)
276                let lang_prefix = &language[..prefix_str.len()];
277                if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
278                    return false;
279                }
280                // Must be exact match or followed by '-'
281                language.len() == prefix_str.len()
282                    || language.as_bytes().get(prefix_str.len()) == Some(&b'-')
283            }
284        }
285    }
286}
287
288#[repr(C)]
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
290pub enum PseudoStateType {
291    /// No special state (corresponds to "Normal" in NodeDataInlineCssProperty)
292    Normal,
293    /// Element is being hovered (:hover)
294    Hover,
295    /// Element is active/being clicked (:active)
296    Active,
297    /// Element has focus (:focus)
298    Focus,
299    /// Element is disabled (:disabled)
300    Disabled,
301    /// Element is checked/selected (:checked)
302    Checked,
303    /// Element or child has focus (:focus-within)
304    FocusWithin,
305    /// Link has been visited (:visited)
306    Visited,
307}
308
309impl_option!(
310    LinuxDesktopEnv,
311    OptionLinuxDesktopEnv,
312    [Debug, Clone, Copy, PartialEq, Eq, Hash]
313);
314
315/// Context for evaluating dynamic selectors
316#[repr(C)]
317#[derive(Debug, Clone)]
318pub struct DynamicSelectorContext {
319    /// Operating system info
320    pub os: OsCondition,
321    pub os_version: AzString,
322    pub desktop_env: OptionLinuxDesktopEnv,
323
324    /// Theme info
325    pub theme: ThemeCondition,
326
327    /// Media info (from WindowState)
328    pub media_type: MediaType,
329    pub viewport_width: f32,
330    pub viewport_height: f32,
331
332    /// Container info (from parent node)
333    /// NaN = no container
334    pub container_width: f32,
335    pub container_height: f32,
336    pub container_name: OptionString,
337
338    /// Accessibility preferences
339    pub prefers_reduced_motion: BoolCondition,
340    pub prefers_high_contrast: BoolCondition,
341
342    /// Orientation
343    pub orientation: OrientationType,
344
345    /// Node state (hover, active, focus, disabled, checked, focus_within, visited)
346    pub pseudo_state: PseudoStateFlags,
347
348    /// Language/Locale (BCP 47 tag, e.g., "en-US", "de-DE")
349    pub language: AzString,
350}
351
352impl Default for DynamicSelectorContext {
353    fn default() -> Self {
354        Self {
355            os: OsCondition::Any,
356            os_version: AzString::from_const_str("0.0"),
357            desktop_env: OptionLinuxDesktopEnv::None,
358            theme: ThemeCondition::Light,
359            media_type: MediaType::Screen,
360            viewport_width: 800.0,
361            viewport_height: 600.0,
362            container_width: f32::NAN,
363            container_height: f32::NAN,
364            container_name: OptionString::None,
365            prefers_reduced_motion: BoolCondition::False,
366            prefers_high_contrast: BoolCondition::False,
367            orientation: OrientationType::Landscape,
368            pseudo_state: PseudoStateFlags::default(),
369            language: AzString::from_const_str("en-US"),
370        }
371    }
372}
373
374impl DynamicSelectorContext {
375    /// Create a context from SystemStyle
376    pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
377        let os = OsCondition::from_system_platform(&system_style.platform);
378        let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
379            OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
380        } else {
381            OptionLinuxDesktopEnv::None
382        };
383        let theme = ThemeCondition::from_system_theme(system_style.theme);
384
385        Self {
386            os,
387            os_version: AzString::from_const_str("0.0"), // TODO: Version detection
388            desktop_env,
389            theme,
390            media_type: MediaType::Screen,
391            viewport_width: 800.0, // Will be updated with window size
392            viewport_height: 600.0,
393            container_width: f32::NAN,
394            container_height: f32::NAN,
395            container_name: OptionString::None,
396            prefers_reduced_motion: BoolCondition::False, // TODO: Accessibility
397            prefers_high_contrast: BoolCondition::False,
398            orientation: OrientationType::Landscape,
399            pseudo_state: PseudoStateFlags::default(),
400            language: system_style.language.clone(),
401        }
402    }
403
404    /// Update viewport dimensions (e.g., on window resize)
405    pub fn with_viewport(&self, width: f32, height: f32) -> Self {
406        let mut ctx = self.clone();
407        ctx.viewport_width = width;
408        ctx.viewport_height = height;
409        ctx.orientation = if width > height {
410            OrientationType::Landscape
411        } else {
412            OrientationType::Portrait
413        };
414        ctx
415    }
416
417    /// Update container dimensions (for @container queries)
418    pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
419        let mut ctx = self.clone();
420        ctx.container_width = width;
421        ctx.container_height = height;
422        ctx.container_name = name.into();
423        ctx
424    }
425
426    /// Update pseudo-state (hover, active, focus, etc.)
427    pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
428        let mut ctx = self.clone();
429        ctx.pseudo_state = state;
430        ctx
431    }
432
433    /// Check if viewport changed significantly (for breakpoint detection)
434    pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
435        for bp in breakpoints {
436            let self_above = self.viewport_width >= *bp;
437            let other_above = other.viewport_width >= *bp;
438            if self_above != other_above {
439                return true;
440            }
441        }
442        false
443    }
444}
445
446impl DynamicSelector {
447    /// Check if this selector matches in the given context
448    pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
449        match self {
450            Self::Os(os) => Self::match_os(*os, ctx.os),
451            Self::OsVersion(ver) => Self::match_os_version(ver, &ctx.os_version, &ctx.desktop_env),
452            Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
453            Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
454            Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
455            Self::ContainerWidth(range) => {
456                !ctx.container_width.is_nan() && range.matches(ctx.container_width)
457            }
458            Self::ContainerHeight(range) => {
459                !ctx.container_height.is_nan() && range.matches(ctx.container_height)
460            }
461            Self::ContainerName(name) => ctx.container_name.as_ref().map_or(false, |n| n == name),
462            Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
463            Self::AspectRatio(range) => {
464                let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
465                range.matches(ratio)
466            }
467            Self::Orientation(orient) => *orient == ctx.orientation,
468            Self::PrefersReducedMotion(pref) => {
469                bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
470            }
471            Self::PrefersHighContrast(pref) => {
472                bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
473            }
474            Self::PseudoState(state) => Self::match_pseudo_state(*state, &ctx.pseudo_state),
475            Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
476        }
477    }
478
479    fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
480        match condition {
481            OsCondition::Any => true,
482            OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
483            _ => condition == actual,
484        }
485    }
486
487    fn match_os_version(
488        condition: &OsVersionCondition,
489        actual: &AzString,
490        desktop_env: &OptionLinuxDesktopEnv,
491    ) -> bool {
492        match condition {
493            OsVersionCondition::Exact(ver) => ver == actual,
494            OsVersionCondition::Min(ver) => Self::compare_version(actual, ver) >= 0,
495            OsVersionCondition::Max(ver) => Self::compare_version(actual, ver) <= 0,
496            OsVersionCondition::DesktopEnvironment(env) => {
497                desktop_env.as_ref().map_or(false, |de| de == env)
498            }
499        }
500    }
501
502    fn compare_version(a: &AzString, b: &AzString) -> i32 {
503        // Simple string comparison for now
504        // TODO: Proper semantic version comparison
505        a.as_str().cmp(b.as_str()) as i32
506    }
507
508    fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
509        match (condition, actual) {
510            (ThemeCondition::SystemPreferred, _) => true,
511            _ => condition == actual,
512        }
513    }
514
515    fn match_pseudo_state(state: PseudoStateType, node_state: &PseudoStateFlags) -> bool {
516        match state {
517            PseudoStateType::Normal => true, // Normal is always active (base state)
518            PseudoStateType::Hover => node_state.hover,
519            PseudoStateType::Active => node_state.active,
520            PseudoStateType::Focus => node_state.focused,
521            PseudoStateType::Disabled => node_state.disabled,
522            PseudoStateType::Checked => node_state.checked,
523            PseudoStateType::FocusWithin => node_state.focus_within,
524            PseudoStateType::Visited => node_state.visited,
525        }
526    }
527}
528
529// ============================================================================
530// CssPropertyWithConditions - Replacement for NodeDataInlineCssProperty
531// ============================================================================
532
533/// A CSS property with optional conditions for when it should be applied.
534/// This replaces `NodeDataInlineCssProperty` with a more flexible system.
535///
536/// If `apply_if` is empty, the property always applies.
537/// If `apply_if` contains conditions, ALL conditions must be satisfied for the property to apply.
538#[repr(C)]
539#[derive(Debug, Clone, PartialEq)]
540pub struct CssPropertyWithConditions {
541    /// The actual CSS property value
542    pub property: CssProperty,
543    /// Conditions that must all be satisfied for this property to apply.
544    /// Empty means unconditional (always apply).
545    pub apply_if: DynamicSelectorVec,
546}
547
548impl Eq for CssPropertyWithConditions {}
549
550impl PartialOrd for CssPropertyWithConditions {
551    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
552        Some(self.cmp(other))
553    }
554}
555
556impl Ord for CssPropertyWithConditions {
557    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
558        // Compare by condition count only (simple stable ordering)
559        self.apply_if
560            .as_slice()
561            .len()
562            .cmp(&other.apply_if.as_slice().len())
563    }
564}
565
566impl CssPropertyWithConditions {
567    /// Create an unconditional property (always applies) - const version
568    pub const fn simple(property: CssProperty) -> Self {
569        Self {
570            property,
571            apply_if: DynamicSelectorVec::from_const_slice(&[]),
572        }
573    }
574
575    /// Create a property with a single condition (const version using slice reference)
576    pub const fn with_single_condition(
577        property: CssProperty,
578        conditions: &'static [DynamicSelector],
579    ) -> Self {
580        Self {
581            property,
582            apply_if: DynamicSelectorVec::from_const_slice(conditions),
583        }
584    }
585
586    /// Create a property with a single condition (non-const, allocates)
587    pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
588        Self {
589            property,
590            apply_if: DynamicSelectorVec::from_vec(vec![condition]),
591        }
592    }
593
594    /// Create a property with multiple conditions (all must match)
595    pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
596        Self {
597            property,
598            apply_if: conditions,
599        }
600    }
601
602    /// Create a property that applies only on hover (const version)
603    pub const fn on_hover(property: CssProperty) -> Self {
604        Self::with_single_condition(
605            property,
606            &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
607        )
608    }
609
610    /// Create a property that applies only when active (const version)
611    pub const fn on_active(property: CssProperty) -> Self {
612        Self::with_single_condition(
613            property,
614            &[DynamicSelector::PseudoState(PseudoStateType::Active)],
615        )
616    }
617
618    /// Create a property that applies only when focused (const version)
619    pub const fn on_focus(property: CssProperty) -> Self {
620        Self::with_single_condition(
621            property,
622            &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
623        )
624    }
625
626    /// Create a property that applies only when disabled (const version)
627    pub const fn when_disabled(property: CssProperty) -> Self {
628        Self::with_single_condition(
629            property,
630            &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
631        )
632    }
633
634    /// Create a property that applies only on a specific OS (non-const, needs runtime value)
635    pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
636        Self::with_condition(property, DynamicSelector::Os(os))
637    }
638
639    /// Create a property that applies only in dark theme (const version)
640    pub const fn dark_theme(property: CssProperty) -> Self {
641        Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
642    }
643
644    /// Create a property that applies only in light theme (const version)
645    pub const fn light_theme(property: CssProperty) -> Self {
646        Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
647    }
648
649    /// Create a property for Windows only (const version)
650    pub const fn on_windows(property: CssProperty) -> Self {
651        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
652    }
653
654    /// Create a property for macOS only (const version)
655    pub const fn on_macos(property: CssProperty) -> Self {
656        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
657    }
658
659    /// Create a property for Linux only (const version)
660    pub const fn on_linux(property: CssProperty) -> Self {
661        Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
662    }
663
664    /// Check if this property matches in the given context
665    pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
666        // Empty conditions = always matches
667        if self.apply_if.as_slice().is_empty() {
668            return true;
669        }
670
671        // All conditions must match
672        self.apply_if
673            .as_slice()
674            .iter()
675            .all(|selector| selector.matches(ctx))
676    }
677
678    /// Check if this property has any conditions
679    pub fn is_conditional(&self) -> bool {
680        !self.apply_if.as_slice().is_empty()
681    }
682
683    /// Check if this property is a pseudo-state conditional only
684    /// (hover, active, focus, etc.)
685    pub fn is_pseudo_state_only(&self) -> bool {
686        let conditions = self.apply_if.as_slice();
687        !conditions.is_empty()
688            && conditions
689                .iter()
690                .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
691    }
692
693    /// Check if this property affects layout (width, height, margin, etc.)
694    /// TODO: Implement when CssProperty has this method
695    pub fn is_layout_affecting(&self) -> bool {
696        // For now, assume all properties might affect layout
697        // This should be implemented properly when property categories are available
698        true
699    }
700}
701
702impl_vec!(
703    CssPropertyWithConditions,
704    CssPropertyWithConditionsVec,
705    CssPropertyWithConditionsVecDestructor,
706    CssPropertyWithConditionsVecDestructorType
707);
708impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
709impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
710impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
711impl_vec_clone!(
712    CssPropertyWithConditions,
713    CssPropertyWithConditionsVec,
714    CssPropertyWithConditionsVecDestructor
715);
716
717// Manual implementations for Eq and Ord (required for NodeData derives)
718impl Eq for CssPropertyWithConditionsVec {}
719
720impl Ord for CssPropertyWithConditionsVec {
721    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
722        self.as_slice().len().cmp(&other.as_slice().len())
723    }
724}
725
726impl core::hash::Hash for CssPropertyWithConditions {
727    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
728        self.property.hash(state);
729        // DynamicSelectorVec doesn't implement Hash, so we hash the length
730        self.apply_if.as_slice().len().hash(state);
731    }
732}
733
734impl core::hash::Hash for CssPropertyWithConditionsVec {
735    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
736        for item in self.as_slice() {
737            item.hash(state);
738        }
739    }
740}
741
742impl CssPropertyWithConditionsVec {
743    /// Parse CSS properties from a string, all with "normal" (unconditional) state
744    #[cfg(feature = "parser")]
745    pub fn parse_normal(style: &str) -> Self {
746        use crate::props::property::{
747            parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
748            CssPropertyType,
749        };
750
751        let mut props = Vec::new();
752        let key_map = CssKeyMap::get();
753
754        // Simple CSS parsing: split by semicolons and parse key:value pairs
755        for pair in style.split(';') {
756            let pair = pair.trim();
757            if pair.is_empty() {
758                continue;
759            }
760            if let Some((key, value)) = pair.split_once(':') {
761                let key = key.trim();
762                let value = value.trim();
763                // First, try to parse as a regular (non-shorthand) property
764                if let Some(prop_type) = CssPropertyType::from_str(key, &key_map) {
765                    if let Ok(prop) = parse_css_property(prop_type, value) {
766                        props.push(CssPropertyWithConditions::simple(prop));
767                        continue;
768                    }
769                }
770                // If not found, try as a shorthand (combined) property (e.g., overflow, margin, padding)
771                if let Some(combined_type) = CombinedCssPropertyType::from_str(key, &key_map) {
772                    if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
773                        for prop in expanded_props {
774                            props.push(CssPropertyWithConditions::simple(prop));
775                        }
776                    }
777                }
778            }
779        }
780
781        CssPropertyWithConditionsVec::from_vec(props)
782    }
783
784    /// Parse CSS properties from a string, all with hover condition
785    #[cfg(feature = "parser")]
786    pub fn parse_hover(style: &str) -> Self {
787        use crate::props::property::{
788            parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
789            CssPropertyType,
790        };
791
792        let mut props = Vec::new();
793        let key_map = CssKeyMap::get();
794
795        for pair in style.split(';') {
796            let pair = pair.trim();
797            if pair.is_empty() {
798                continue;
799            }
800            if let Some((key, value)) = pair.split_once(':') {
801                let key = key.trim();
802                let value = value.trim();
803                // First, try to parse as a regular (non-shorthand) property
804                if let Some(prop_type) = CssPropertyType::from_str(key, &key_map) {
805                    if let Ok(prop) = parse_css_property(prop_type, value) {
806                        props.push(CssPropertyWithConditions::on_hover(prop));
807                        continue;
808                    }
809                }
810                // If not found, try as a shorthand (combined) property
811                if let Some(combined_type) = CombinedCssPropertyType::from_str(key, &key_map) {
812                    if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
813                        for prop in expanded_props {
814                            props.push(CssPropertyWithConditions::on_hover(prop));
815                        }
816                    }
817                }
818            }
819        }
820
821        CssPropertyWithConditionsVec::from_vec(props)
822    }
823
824    /// Parse CSS properties from a string, all with active condition
825    #[cfg(feature = "parser")]
826    pub fn parse_active(style: &str) -> Self {
827        use crate::props::property::{
828            parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
829            CssPropertyType,
830        };
831
832        let mut props = Vec::new();
833        let key_map = CssKeyMap::get();
834
835        for pair in style.split(';') {
836            let pair = pair.trim();
837            if pair.is_empty() {
838                continue;
839            }
840            if let Some((key, value)) = pair.split_once(':') {
841                let key = key.trim();
842                let value = value.trim();
843                // First, try to parse as a regular (non-shorthand) property
844                if let Some(prop_type) = CssPropertyType::from_str(key, &key_map) {
845                    if let Ok(prop) = parse_css_property(prop_type, value) {
846                        props.push(CssPropertyWithConditions::on_active(prop));
847                        continue;
848                    }
849                }
850                // If not found, try as a shorthand (combined) property
851                if let Some(combined_type) = CombinedCssPropertyType::from_str(key, &key_map) {
852                    if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
853                        for prop in expanded_props {
854                            props.push(CssPropertyWithConditions::on_active(prop));
855                        }
856                    }
857                }
858            }
859        }
860
861        CssPropertyWithConditionsVec::from_vec(props)
862    }
863
864    /// Parse CSS properties from a string, all with focus condition
865    #[cfg(feature = "parser")]
866    pub fn parse_focus(style: &str) -> Self {
867        use crate::props::property::{
868            parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
869            CssPropertyType,
870        };
871
872        let mut props = Vec::new();
873        let key_map = CssKeyMap::get();
874
875        for pair in style.split(';') {
876            let pair = pair.trim();
877            if pair.is_empty() {
878                continue;
879            }
880            if let Some((key, value)) = pair.split_once(':') {
881                let key = key.trim();
882                let value = value.trim();
883                // First, try to parse as a regular (non-shorthand) property
884                if let Some(prop_type) = CssPropertyType::from_str(key, &key_map) {
885                    if let Ok(prop) = parse_css_property(prop_type, value) {
886                        props.push(CssPropertyWithConditions::on_focus(prop));
887                        continue;
888                    }
889                }
890                // If not found, try as a shorthand (combined) property
891                if let Some(combined_type) = CombinedCssPropertyType::from_str(key, &key_map) {
892                    if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
893                        for prop in expanded_props {
894                            props.push(CssPropertyWithConditions::on_focus(prop));
895                        }
896                    }
897                }
898            }
899        }
900
901        CssPropertyWithConditionsVec::from_vec(props)
902    }
903}