1use crate::corety::{AzString, OptionString};
4use crate::props::property::CssProperty;
5
6#[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 pub backdrop: bool,
20 pub dragging: bool,
22 pub drag_over: bool,
24}
25
26impl PseudoStateFlags {
27 pub fn has_state(&self, state: PseudoStateType) -> bool {
29 match state {
30 PseudoStateType::Normal => true,
31 PseudoStateType::Hover => self.hover,
32 PseudoStateType::Active => self.active,
33 PseudoStateType::Focus => self.focused,
34 PseudoStateType::Disabled => self.disabled,
35 PseudoStateType::Checked => self.checked,
36 PseudoStateType::FocusWithin => self.focus_within,
37 PseudoStateType::Visited => self.visited,
38 PseudoStateType::Backdrop => self.backdrop,
39 PseudoStateType::Dragging => self.dragging,
40 PseudoStateType::DragOver => self.drag_over,
41 }
42 }
43}
44
45#[repr(C, u8)]
48#[derive(Debug, Clone, PartialEq)]
49pub enum DynamicSelector {
50 Os(OsCondition) = 0,
52 OsVersion(OsVersionCondition) = 1,
54 Media(MediaType) = 2,
56 ViewportWidth(MinMaxRange) = 3,
58 ViewportHeight(MinMaxRange) = 4,
60 ContainerWidth(MinMaxRange) = 5,
62 ContainerHeight(MinMaxRange) = 6,
64 ContainerName(AzString) = 7,
66 Theme(ThemeCondition) = 8,
68 AspectRatio(MinMaxRange) = 9,
70 Orientation(OrientationType) = 10,
72 PrefersReducedMotion(BoolCondition) = 11,
74 PrefersHighContrast(BoolCondition) = 12,
76 PseudoState(PseudoStateType) = 13,
78 Language(LanguageCondition) = 14,
81}
82
83impl_option!(
84 DynamicSelector,
85 OptionDynamicSelector,
86 copy = false,
87 [Debug, Clone, PartialEq]
88);
89
90impl_vec!(DynamicSelector, DynamicSelectorVec, DynamicSelectorVecDestructor, DynamicSelectorVecDestructorType, DynamicSelectorVecSlice, OptionDynamicSelector);
91impl_vec_clone!(
92 DynamicSelector,
93 DynamicSelectorVec,
94 DynamicSelectorVecDestructor
95);
96impl_vec_debug!(DynamicSelector, DynamicSelectorVec);
97impl_vec_partialeq!(DynamicSelector, DynamicSelectorVec);
98
99#[repr(C)]
101#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
102pub struct MinMaxRange {
103 pub min: f32,
105 pub max: f32,
107}
108
109impl MinMaxRange {
110 pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
111 Self {
112 min: if let Some(m) = min { m } else { f32::NAN },
113 max: if let Some(m) = max { m } else { f32::NAN },
114 }
115 }
116
117 pub const fn with_min(min_val: f32) -> Self {
119 Self {
120 min: min_val,
121 max: f32::NAN,
122 }
123 }
124
125 pub const fn with_max(max_val: f32) -> Self {
127 Self {
128 min: f32::NAN,
129 max: max_val,
130 }
131 }
132
133 pub fn min(&self) -> Option<f32> {
134 if self.min.is_nan() {
135 None
136 } else {
137 Some(self.min)
138 }
139 }
140
141 pub fn max(&self) -> Option<f32> {
142 if self.max.is_nan() {
143 None
144 } else {
145 Some(self.max)
146 }
147 }
148
149 pub fn matches(&self, value: f32) -> bool {
150 let min_ok = self.min.is_nan() || value >= self.min;
151 let max_ok = self.max.is_nan() || value <= self.max;
152 min_ok && max_ok
153 }
154}
155
156#[repr(C)]
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
159pub enum BoolCondition {
160 #[default]
161 False,
162 True,
163}
164
165impl From<bool> for BoolCondition {
166 fn from(b: bool) -> Self {
167 if b {
168 Self::True
169 } else {
170 Self::False
171 }
172 }
173}
174
175impl From<BoolCondition> for bool {
176 fn from(b: BoolCondition) -> Self {
177 matches!(b, BoolCondition::True)
178 }
179}
180
181#[repr(C)]
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183pub enum OsCondition {
184 Any,
185 Apple, MacOS,
187 IOS,
188 Linux,
189 Windows,
190 Android,
191 Web, }
193
194impl_option!(
195 OsCondition,
196 OptionOsCondition,
197 [Debug, Clone, Copy, PartialEq, Eq, Hash]
198);
199
200impl OsCondition {
201 pub fn from_system_platform(platform: &crate::system::Platform) -> Self {
203 use crate::system::Platform;
204 match platform {
205 Platform::Windows => OsCondition::Windows,
206 Platform::MacOs => OsCondition::MacOS,
207 Platform::Linux(_) => OsCondition::Linux,
208 Platform::Android => OsCondition::Android,
209 Platform::Ios => OsCondition::IOS,
210 Platform::Unknown => OsCondition::Any,
211 }
212 }
213}
214
215#[repr(C, u8)]
216#[derive(Debug, Clone, PartialEq, Eq, Hash)]
217pub enum OsVersionCondition {
218 Min(OsVersion),
221 Max(OsVersion),
223 Exact(OsVersion),
225 DesktopEnvironment(LinuxDesktopEnv),
227}
228
229#[repr(C)]
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235pub struct OsVersion {
236 pub os: OsFamily,
238 pub version_id: u32,
241}
242
243impl Default for OsVersion {
244 fn default() -> Self {
245 Self::unknown()
246 }
247}
248
249impl OsVersion {
250 pub const fn new(os: OsFamily, version_id: u32) -> Self {
251 Self { os, version_id }
252 }
253
254 pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
257 if self.os != other.os {
258 None } else {
260 Some(self.version_id.cmp(&other.version_id))
261 }
262 }
263
264 pub fn is_at_least(&self, other: &Self) -> bool {
266 self.compare(other).map_or(false, |o| o != core::cmp::Ordering::Less)
267 }
268
269 pub fn is_at_most(&self, other: &Self) -> bool {
271 self.compare(other).map_or(false, |o| o != core::cmp::Ordering::Greater)
272 }
273}
274
275impl_option!(
276 OsVersion,
277 OptionOsVersion,
278 [Debug, Clone, Copy, PartialEq, Eq, Hash]
279);
280
281impl OsVersion {
282
283 pub fn is_exactly(&self, other: &Self) -> bool {
285 self.compare(other) == Some(core::cmp::Ordering::Equal)
286 }
287}
288
289#[repr(C)]
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
292pub enum OsFamily {
293 Windows,
294 MacOS,
295 IOS,
296 Linux,
297 Android,
298}
299
300impl OsVersion {
306 pub const WIN_2000: Self = Self::new(OsFamily::Windows, 500); pub const WIN_XP: Self = Self::new(OsFamily::Windows, 501); pub const WIN_XP_64: Self = Self::new(OsFamily::Windows, 502); pub const WIN_VISTA: Self = Self::new(OsFamily::Windows, 600); pub const WIN_7: Self = Self::new(OsFamily::Windows, 601); pub const WIN_8: Self = Self::new(OsFamily::Windows, 602); pub const WIN_8_1: Self = Self::new(OsFamily::Windows, 603); pub const WIN_10: Self = Self::new(OsFamily::Windows, 1000); pub const WIN_10_1507: Self = Self::new(OsFamily::Windows, 1000); pub const WIN_10_1511: Self = Self::new(OsFamily::Windows, 1001); pub const WIN_10_1607: Self = Self::new(OsFamily::Windows, 1002); pub const WIN_10_1703: Self = Self::new(OsFamily::Windows, 1003); pub const WIN_10_1709: Self = Self::new(OsFamily::Windows, 1004); pub const WIN_10_1803: Self = Self::new(OsFamily::Windows, 1005); pub const WIN_10_1809: Self = Self::new(OsFamily::Windows, 1006); pub const WIN_10_1903: Self = Self::new(OsFamily::Windows, 1007); pub const WIN_10_1909: Self = Self::new(OsFamily::Windows, 1008); pub const WIN_10_2004: Self = Self::new(OsFamily::Windows, 1009); pub const WIN_10_20H2: Self = Self::new(OsFamily::Windows, 1010); pub const WIN_10_21H1: Self = Self::new(OsFamily::Windows, 1011); pub const WIN_10_21H2: Self = Self::new(OsFamily::Windows, 1012); pub const WIN_10_22H2: Self = Self::new(OsFamily::Windows, 1013); pub const WIN_11: Self = Self::new(OsFamily::Windows, 1100); pub const WIN_11_21H2: Self = Self::new(OsFamily::Windows, 1100); pub const WIN_11_22H2: Self = Self::new(OsFamily::Windows, 1101); pub const WIN_11_23H2: Self = Self::new(OsFamily::Windows, 1102); pub const WIN_11_24H2: Self = Self::new(OsFamily::Windows, 1103); pub const MACOS_CHEETAH: Self = Self::new(OsFamily::MacOS, 1000); pub const MACOS_PUMA: Self = Self::new(OsFamily::MacOS, 1001); pub const MACOS_JAGUAR: Self = Self::new(OsFamily::MacOS, 1002); pub const MACOS_PANTHER: Self = Self::new(OsFamily::MacOS, 1003); pub const MACOS_TIGER: Self = Self::new(OsFamily::MacOS, 1004); pub const MACOS_LEOPARD: Self = Self::new(OsFamily::MacOS, 1005); pub const MACOS_SNOW_LEOPARD: Self = Self::new(OsFamily::MacOS, 1006); pub const MACOS_LION: Self = Self::new(OsFamily::MacOS, 1007); pub const MACOS_MOUNTAIN_LION: Self = Self::new(OsFamily::MacOS, 1008); pub const MACOS_MAVERICKS: Self = Self::new(OsFamily::MacOS, 1009); pub const MACOS_YOSEMITE: Self = Self::new(OsFamily::MacOS, 1010); pub const MACOS_EL_CAPITAN: Self = Self::new(OsFamily::MacOS, 1011); pub const MACOS_SIERRA: Self = Self::new(OsFamily::MacOS, 1012); pub const MACOS_HIGH_SIERRA: Self = Self::new(OsFamily::MacOS, 1013); pub const MACOS_MOJAVE: Self = Self::new(OsFamily::MacOS, 1014); pub const MACOS_CATALINA: Self = Self::new(OsFamily::MacOS, 1015); pub const MACOS_BIG_SUR: Self = Self::new(OsFamily::MacOS, 1100); pub const MACOS_MONTEREY: Self = Self::new(OsFamily::MacOS, 1200); pub const MACOS_VENTURA: Self = Self::new(OsFamily::MacOS, 1300); pub const MACOS_SONOMA: Self = Self::new(OsFamily::MacOS, 1400); pub const MACOS_SEQUOIA: Self = Self::new(OsFamily::MacOS, 1500); pub const MACOS_TAHOE: Self = Self::new(OsFamily::MacOS, 2600); pub const IOS_1: Self = Self::new(OsFamily::IOS, 100);
361 pub const IOS_2: Self = Self::new(OsFamily::IOS, 200);
362 pub const IOS_3: Self = Self::new(OsFamily::IOS, 300);
363 pub const IOS_4: Self = Self::new(OsFamily::IOS, 400);
364 pub const IOS_5: Self = Self::new(OsFamily::IOS, 500);
365 pub const IOS_6: Self = Self::new(OsFamily::IOS, 600);
366 pub const IOS_7: Self = Self::new(OsFamily::IOS, 700);
367 pub const IOS_8: Self = Self::new(OsFamily::IOS, 800);
368 pub const IOS_9: Self = Self::new(OsFamily::IOS, 900);
369 pub const IOS_10: Self = Self::new(OsFamily::IOS, 1000);
370 pub const IOS_11: Self = Self::new(OsFamily::IOS, 1100);
371 pub const IOS_12: Self = Self::new(OsFamily::IOS, 1200);
372 pub const IOS_13: Self = Self::new(OsFamily::IOS, 1300);
373 pub const IOS_14: Self = Self::new(OsFamily::IOS, 1400);
374 pub const IOS_15: Self = Self::new(OsFamily::IOS, 1500);
375 pub const IOS_16: Self = Self::new(OsFamily::IOS, 1600);
376 pub const IOS_17: Self = Self::new(OsFamily::IOS, 1700);
377 pub const IOS_18: Self = Self::new(OsFamily::IOS, 1800);
378
379 pub const ANDROID_CUPCAKE: Self = Self::new(OsFamily::Android, 3); pub const ANDROID_DONUT: Self = Self::new(OsFamily::Android, 4); pub const ANDROID_ECLAIR: Self = Self::new(OsFamily::Android, 7); pub const ANDROID_FROYO: Self = Self::new(OsFamily::Android, 8); pub const ANDROID_GINGERBREAD: Self = Self::new(OsFamily::Android, 10); pub const ANDROID_HONEYCOMB: Self = Self::new(OsFamily::Android, 13); pub const ANDROID_ICE_CREAM_SANDWICH: Self = Self::new(OsFamily::Android, 15); pub const ANDROID_JELLY_BEAN: Self = Self::new(OsFamily::Android, 18); pub const ANDROID_KITKAT: Self = Self::new(OsFamily::Android, 19); pub const ANDROID_LOLLIPOP: Self = Self::new(OsFamily::Android, 22); pub const ANDROID_MARSHMALLOW: Self = Self::new(OsFamily::Android, 23); pub const ANDROID_NOUGAT: Self = Self::new(OsFamily::Android, 25); pub const ANDROID_OREO: Self = Self::new(OsFamily::Android, 27); pub const ANDROID_PIE: Self = Self::new(OsFamily::Android, 28); pub const ANDROID_10: Self = Self::new(OsFamily::Android, 29); pub const ANDROID_11: Self = Self::new(OsFamily::Android, 30); pub const ANDROID_12: Self = Self::new(OsFamily::Android, 31); pub const ANDROID_12L: Self = Self::new(OsFamily::Android, 32); pub const ANDROID_13: Self = Self::new(OsFamily::Android, 33); pub const ANDROID_14: Self = Self::new(OsFamily::Android, 34); pub const ANDROID_15: Self = Self::new(OsFamily::Android, 35); pub const LINUX_2_6: Self = Self::new(OsFamily::Linux, 2060);
404 pub const LINUX_3_0: Self = Self::new(OsFamily::Linux, 3000);
405 pub const LINUX_4_0: Self = Self::new(OsFamily::Linux, 4000);
406 pub const LINUX_5_0: Self = Self::new(OsFamily::Linux, 5000);
407 pub const LINUX_6_0: Self = Self::new(OsFamily::Linux, 6000);
408
409 pub const fn unknown() -> Self {
411 Self {
412 os: OsFamily::Linux, version_id: 0,
414 }
415 }
416}
417
418pub fn parse_os_version(os: OsFamily, version_str: &str) -> Option<OsVersion> {
421 let version_str = version_str.trim().to_lowercase();
422 let version_str = version_str.as_str();
423
424 match os {
425 OsFamily::Windows => parse_windows_version(version_str),
426 OsFamily::MacOS => parse_macos_version(version_str),
427 OsFamily::IOS => parse_ios_version(version_str),
428 OsFamily::Android => parse_android_version(version_str),
429 OsFamily::Linux => parse_linux_version(version_str),
430 }
431}
432
433fn parse_windows_version(s: &str) -> Option<OsVersion> {
434 match s {
436 "2000" | "win2000" | "win-2000" => Some(OsVersion::WIN_2000),
437 "xp" | "winxp" | "win-xp" => Some(OsVersion::WIN_XP),
438 "vista" | "winvista" | "win-vista" => Some(OsVersion::WIN_VISTA),
439 "7" | "win7" | "win-7" => Some(OsVersion::WIN_7),
440 "8" | "win8" | "win-8" => Some(OsVersion::WIN_8),
441 "8.1" | "win8.1" | "win-8.1" | "win-8-1" => Some(OsVersion::WIN_8_1),
442 "10" | "win10" | "win-10" => Some(OsVersion::WIN_10),
443 "11" | "win11" | "win-11" => Some(OsVersion::WIN_11),
444 "5.0" | "nt5.0" => Some(OsVersion::WIN_2000),
446 "5.1" | "nt5.1" => Some(OsVersion::WIN_XP),
447 "6.0" | "nt6.0" => Some(OsVersion::WIN_VISTA),
448 "6.1" | "nt6.1" => Some(OsVersion::WIN_7),
449 "6.2" | "nt6.2" => Some(OsVersion::WIN_8),
450 "6.3" | "nt6.3" => Some(OsVersion::WIN_8_1),
451 "10.0" | "nt10.0" => Some(OsVersion::WIN_10),
452 _ => None,
453 }
454}
455
456fn parse_macos_version(s: &str) -> Option<OsVersion> {
457 match s {
458 "cheetah" | "10.0" => Some(OsVersion::MACOS_CHEETAH),
459 "puma" | "10.1" => Some(OsVersion::MACOS_PUMA),
460 "jaguar" | "10.2" => Some(OsVersion::MACOS_JAGUAR),
461 "panther" | "10.3" => Some(OsVersion::MACOS_PANTHER),
462 "tiger" | "10.4" => Some(OsVersion::MACOS_TIGER),
463 "leopard" | "10.5" => Some(OsVersion::MACOS_LEOPARD),
464 "snow-leopard" | "snowleopard" | "10.6" => Some(OsVersion::MACOS_SNOW_LEOPARD),
465 "lion" | "10.7" => Some(OsVersion::MACOS_LION),
466 "mountain-lion" | "mountainlion" | "10.8" => Some(OsVersion::MACOS_MOUNTAIN_LION),
467 "mavericks" | "10.9" => Some(OsVersion::MACOS_MAVERICKS),
468 "yosemite" | "10.10" => Some(OsVersion::MACOS_YOSEMITE),
469 "el-capitan" | "elcapitan" | "10.11" => Some(OsVersion::MACOS_EL_CAPITAN),
470 "sierra" | "10.12" => Some(OsVersion::MACOS_SIERRA),
471 "high-sierra" | "highsierra" | "10.13" => Some(OsVersion::MACOS_HIGH_SIERRA),
472 "mojave" | "10.14" => Some(OsVersion::MACOS_MOJAVE),
473 "catalina" | "10.15" => Some(OsVersion::MACOS_CATALINA),
474 "big-sur" | "bigsur" | "11" | "11.0" => Some(OsVersion::MACOS_BIG_SUR),
475 "monterey" | "12" | "12.0" => Some(OsVersion::MACOS_MONTEREY),
476 "ventura" | "13" | "13.0" => Some(OsVersion::MACOS_VENTURA),
477 "sonoma" | "14" | "14.0" => Some(OsVersion::MACOS_SONOMA),
478 "sequoia" | "15" | "15.0" => Some(OsVersion::MACOS_SEQUOIA),
479 "tahoe" | "26" | "26.0" => Some(OsVersion::MACOS_TAHOE),
480 _ => None,
481 }
482}
483
484fn parse_ios_version(s: &str) -> Option<OsVersion> {
485 match s {
486 "1" | "1.0" => Some(OsVersion::IOS_1),
487 "2" | "2.0" => Some(OsVersion::IOS_2),
488 "3" | "3.0" => Some(OsVersion::IOS_3),
489 "4" | "4.0" => Some(OsVersion::IOS_4),
490 "5" | "5.0" => Some(OsVersion::IOS_5),
491 "6" | "6.0" => Some(OsVersion::IOS_6),
492 "7" | "7.0" => Some(OsVersion::IOS_7),
493 "8" | "8.0" => Some(OsVersion::IOS_8),
494 "9" | "9.0" => Some(OsVersion::IOS_9),
495 "10" | "10.0" => Some(OsVersion::IOS_10),
496 "11" | "11.0" => Some(OsVersion::IOS_11),
497 "12" | "12.0" => Some(OsVersion::IOS_12),
498 "13" | "13.0" => Some(OsVersion::IOS_13),
499 "14" | "14.0" => Some(OsVersion::IOS_14),
500 "15" | "15.0" => Some(OsVersion::IOS_15),
501 "16" | "16.0" => Some(OsVersion::IOS_16),
502 "17" | "17.0" => Some(OsVersion::IOS_17),
503 "18" | "18.0" => Some(OsVersion::IOS_18),
504 _ => None,
505 }
506}
507
508fn parse_android_version(s: &str) -> Option<OsVersion> {
509 match s {
510 "cupcake" | "1.5" => Some(OsVersion::ANDROID_CUPCAKE),
511 "donut" | "1.6" => Some(OsVersion::ANDROID_DONUT),
512 "eclair" | "2.1" => Some(OsVersion::ANDROID_ECLAIR),
513 "froyo" | "2.2" => Some(OsVersion::ANDROID_FROYO),
514 "gingerbread" | "2.3" => Some(OsVersion::ANDROID_GINGERBREAD),
515 "honeycomb" | "3.0" | "3.2" => Some(OsVersion::ANDROID_HONEYCOMB),
516 "ice-cream-sandwich" | "ics" | "4.0" => Some(OsVersion::ANDROID_ICE_CREAM_SANDWICH),
517 "jelly-bean" | "jellybean" | "4.3" => Some(OsVersion::ANDROID_JELLY_BEAN),
518 "kitkat" | "4.4" => Some(OsVersion::ANDROID_KITKAT),
519 "lollipop" | "5.0" | "5.1" => Some(OsVersion::ANDROID_LOLLIPOP),
520 "marshmallow" | "6.0" => Some(OsVersion::ANDROID_MARSHMALLOW),
521 "nougat" | "7.0" | "7.1" => Some(OsVersion::ANDROID_NOUGAT),
522 "oreo" | "8.0" | "8.1" => Some(OsVersion::ANDROID_OREO),
523 "pie" | "9" | "9.0" => Some(OsVersion::ANDROID_PIE),
524 "10" | "q" => Some(OsVersion::ANDROID_10),
525 "11" | "r" => Some(OsVersion::ANDROID_11),
526 "12" | "s" => Some(OsVersion::ANDROID_12),
527 "12l" | "12L" => Some(OsVersion::ANDROID_12L),
528 "13" | "t" | "tiramisu" => Some(OsVersion::ANDROID_13),
529 "14" | "u" | "upside-down-cake" => Some(OsVersion::ANDROID_14),
530 "15" | "v" | "vanilla-ice-cream" => Some(OsVersion::ANDROID_15),
531 _ => {
532 if let Some(api) = s.strip_prefix("api") {
534 if let Ok(level) = api.trim().parse::<u32>() {
535 return Some(OsVersion::new(OsFamily::Android, level));
536 }
537 }
538 None
539 }
540 }
541}
542
543fn parse_linux_version(s: &str) -> Option<OsVersion> {
544 let parts: Vec<&str> = s.split('.').collect();
546 if parts.len() >= 2 {
547 if let (Ok(major), Ok(minor)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
548 let patch = parts.get(2).and_then(|p| p.parse::<u32>().ok()).unwrap_or(0);
549 return Some(OsVersion::new(OsFamily::Linux, major * 1000 + minor * 10 + patch));
550 }
551 }
552 None
553}
554
555#[repr(C)]
556#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
557pub enum LinuxDesktopEnv {
558 Gnome,
559 KDE,
560 XFCE,
561 Unity,
562 Cinnamon,
563 MATE,
564 Other,
565}
566
567impl LinuxDesktopEnv {
568 pub fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
570 use crate::system::DesktopEnvironment;
571 match de {
572 DesktopEnvironment::Gnome => LinuxDesktopEnv::Gnome,
573 DesktopEnvironment::Kde => LinuxDesktopEnv::KDE,
574 DesktopEnvironment::Other(_) => LinuxDesktopEnv::Other,
575 }
576 }
577}
578
579#[repr(C)]
580#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
581pub enum MediaType {
582 Screen,
583 Print,
584 All,
585}
586
587#[repr(C, u8)]
588#[derive(Debug, Clone, PartialEq, Eq, Hash)]
589pub enum ThemeCondition {
590 Light,
591 Dark,
592 Custom(AzString),
593 SystemPreferred,
595}
596
597impl_option!(
598 ThemeCondition,
599 OptionThemeCondition,
600 copy = false,
601 [Debug, Clone, PartialEq, Eq, Hash]
602);
603
604impl ThemeCondition {
605 pub fn from_system_theme(theme: crate::system::Theme) -> Self {
607 use crate::system::Theme;
608 match theme {
609 Theme::Light => ThemeCondition::Light,
610 Theme::Dark => ThemeCondition::Dark,
611 }
612 }
613}
614
615#[repr(C)]
616#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
617pub enum OrientationType {
618 Portrait,
619 Landscape,
620}
621
622#[repr(C, u8)]
625#[derive(Debug, Clone, PartialEq, Eq, Hash)]
626pub enum LanguageCondition {
627 Exact(AzString),
629 Prefix(AzString),
631}
632
633impl LanguageCondition {
634 pub fn matches(&self, language: &str) -> bool {
636 match self {
637 LanguageCondition::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
638 LanguageCondition::Prefix(prefix) => {
639 let prefix_str = prefix.as_str();
640 if language.len() < prefix_str.len() {
641 return false;
642 }
643 let lang_prefix = &language[..prefix_str.len()];
645 if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
646 return false;
647 }
648 language.len() == prefix_str.len()
650 || language.as_bytes().get(prefix_str.len()) == Some(&b'-')
651 }
652 }
653 }
654}
655
656#[repr(C)]
657#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
658pub enum PseudoStateType {
659 Normal,
661 Hover,
663 Active,
665 Focus,
667 Disabled,
669 Checked,
671 FocusWithin,
673 Visited,
675 Backdrop,
677 Dragging,
679 DragOver,
681}
682
683impl_option!(
684 LinuxDesktopEnv,
685 OptionLinuxDesktopEnv,
686 [Debug, Clone, Copy, PartialEq, Eq, Hash]
687);
688
689#[repr(C)]
691#[derive(Debug, Clone)]
692pub struct DynamicSelectorContext {
693 pub os: OsCondition,
695 pub os_version: OsVersion,
696 pub desktop_env: OptionLinuxDesktopEnv,
697
698 pub theme: ThemeCondition,
700
701 pub media_type: MediaType,
703 pub viewport_width: f32,
704 pub viewport_height: f32,
705
706 pub container_width: f32,
709 pub container_height: f32,
710 pub container_name: OptionString,
711
712 pub prefers_reduced_motion: BoolCondition,
714 pub prefers_high_contrast: BoolCondition,
715
716 pub orientation: OrientationType,
718
719 pub pseudo_state: PseudoStateFlags,
721
722 pub language: AzString,
724
725 pub window_focused: bool,
728}
729
730impl Default for DynamicSelectorContext {
731 fn default() -> Self {
732 Self {
733 os: OsCondition::Any,
734 os_version: OsVersion::unknown(),
735 desktop_env: OptionLinuxDesktopEnv::None,
736 theme: ThemeCondition::Light,
737 media_type: MediaType::Screen,
738 viewport_width: 800.0,
739 viewport_height: 600.0,
740 container_width: f32::NAN,
741 container_height: f32::NAN,
742 container_name: OptionString::None,
743 prefers_reduced_motion: BoolCondition::False,
744 prefers_high_contrast: BoolCondition::False,
745 orientation: OrientationType::Landscape,
746 pseudo_state: PseudoStateFlags::default(),
747 language: AzString::from_const_str("en-US"),
748 window_focused: true,
749 }
750 }
751}
752
753impl DynamicSelectorContext {
754 pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
756 let os = OsCondition::from_system_platform(&system_style.platform);
757 let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
758 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
759 } else {
760 OptionLinuxDesktopEnv::None
761 };
762 let theme = ThemeCondition::from_system_theme(system_style.theme);
763
764 Self {
765 os,
766 os_version: system_style.os_version, desktop_env,
768 theme,
769 media_type: MediaType::Screen,
770 viewport_width: 800.0, viewport_height: 600.0,
772 container_width: f32::NAN,
773 container_height: f32::NAN,
774 container_name: OptionString::None,
775 prefers_reduced_motion: system_style.prefers_reduced_motion,
776 prefers_high_contrast: system_style.prefers_high_contrast,
777 orientation: OrientationType::Landscape,
778 pseudo_state: PseudoStateFlags::default(),
779 language: system_style.language.clone(),
780 window_focused: true,
781 }
782 }
783
784 pub fn with_viewport(&self, width: f32, height: f32) -> Self {
786 let mut ctx = self.clone();
787 ctx.viewport_width = width;
788 ctx.viewport_height = height;
789 ctx.orientation = if width > height {
790 OrientationType::Landscape
791 } else {
792 OrientationType::Portrait
793 };
794 ctx
795 }
796
797 pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
799 let mut ctx = self.clone();
800 ctx.container_width = width;
801 ctx.container_height = height;
802 ctx.container_name = name.into();
803 ctx
804 }
805
806 pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
808 let mut ctx = self.clone();
809 ctx.pseudo_state = state;
810 ctx
811 }
812
813 pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
815 for bp in breakpoints {
816 let self_above = self.viewport_width >= *bp;
817 let other_above = other.viewport_width >= *bp;
818 if self_above != other_above {
819 return true;
820 }
821 }
822 false
823 }
824}
825
826impl DynamicSelector {
827 pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
829 match self {
830 Self::Os(os) => Self::match_os(*os, ctx.os),
831 Self::OsVersion(ver) => Self::match_os_version(ver, &ctx.os_version, &ctx.desktop_env),
832 Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
833 Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
834 Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
835 Self::ContainerWidth(range) => {
836 !ctx.container_width.is_nan() && range.matches(ctx.container_width)
837 }
838 Self::ContainerHeight(range) => {
839 !ctx.container_height.is_nan() && range.matches(ctx.container_height)
840 }
841 Self::ContainerName(name) => ctx.container_name.as_ref().map_or(false, |n| n == name),
842 Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
843 Self::AspectRatio(range) => {
844 let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
845 range.matches(ratio)
846 }
847 Self::Orientation(orient) => *orient == ctx.orientation,
848 Self::PrefersReducedMotion(pref) => {
849 bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
850 }
851 Self::PrefersHighContrast(pref) => {
852 bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
853 }
854 Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
855 Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
856 }
857 }
858
859 fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
860 match condition {
861 OsCondition::Any => true,
862 OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
863 _ => condition == actual,
864 }
865 }
866
867 fn match_os_version(
868 condition: &OsVersionCondition,
869 actual: &OsVersion,
870 desktop_env: &OptionLinuxDesktopEnv,
871 ) -> bool {
872 match condition {
873 OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
874 OsVersionCondition::Min(ver) => actual.is_at_least(ver),
875 OsVersionCondition::Max(ver) => actual.is_at_most(ver),
876 OsVersionCondition::DesktopEnvironment(env) => {
877 desktop_env.as_ref().map_or(false, |de| de == env)
878 }
879 }
880 }
881
882 fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
883 match (condition, actual) {
884 (ThemeCondition::SystemPreferred, _) => true,
885 _ => condition == actual,
886 }
887 }
888
889 fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
890 let node_state = &ctx.pseudo_state;
891 match state {
892 PseudoStateType::Normal => true, PseudoStateType::Hover => node_state.hover,
894 PseudoStateType::Active => node_state.active,
895 PseudoStateType::Focus => node_state.focused,
896 PseudoStateType::Disabled => node_state.disabled,
897 PseudoStateType::Checked => node_state.checked,
898 PseudoStateType::FocusWithin => node_state.focus_within,
899 PseudoStateType::Visited => node_state.visited,
900 PseudoStateType::Backdrop => !ctx.window_focused,
902 PseudoStateType::Dragging => node_state.dragging,
903 PseudoStateType::DragOver => node_state.drag_over,
904 }
905 }
906}
907
908#[repr(C)]
918#[derive(Debug, Clone, PartialEq)]
919pub struct CssPropertyWithConditions {
920 pub property: CssProperty,
922 pub apply_if: DynamicSelectorVec,
925}
926
927impl_option!(
928 CssPropertyWithConditions,
929 OptionCssPropertyWithConditions,
930 copy = false,
931 [Debug, Clone, PartialEq, PartialOrd]
932);
933
934impl Eq for CssPropertyWithConditions {}
935
936impl PartialOrd for CssPropertyWithConditions {
937 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
938 Some(self.cmp(other))
939 }
940}
941
942impl Ord for CssPropertyWithConditions {
943 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
944 self.apply_if
946 .as_slice()
947 .len()
948 .cmp(&other.apply_if.as_slice().len())
949 }
950}
951
952impl CssPropertyWithConditions {
953 pub const fn simple(property: CssProperty) -> Self {
955 Self {
956 property,
957 apply_if: DynamicSelectorVec::from_const_slice(&[]),
958 }
959 }
960
961 pub const fn with_single_condition(
963 property: CssProperty,
964 conditions: &'static [DynamicSelector],
965 ) -> Self {
966 Self {
967 property,
968 apply_if: DynamicSelectorVec::from_const_slice(conditions),
969 }
970 }
971
972 pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
974 Self {
975 property,
976 apply_if: DynamicSelectorVec::from_vec(vec![condition]),
977 }
978 }
979
980 pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
982 Self {
983 property,
984 apply_if: conditions,
985 }
986 }
987
988 pub const fn on_hover(property: CssProperty) -> Self {
990 Self::with_single_condition(
991 property,
992 &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
993 )
994 }
995
996 pub const fn on_active(property: CssProperty) -> Self {
998 Self::with_single_condition(
999 property,
1000 &[DynamicSelector::PseudoState(PseudoStateType::Active)],
1001 )
1002 }
1003
1004 pub const fn on_focus(property: CssProperty) -> Self {
1006 Self::with_single_condition(
1007 property,
1008 &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
1009 )
1010 }
1011
1012 pub const fn when_disabled(property: CssProperty) -> Self {
1014 Self::with_single_condition(
1015 property,
1016 &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
1017 )
1018 }
1019
1020 pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
1022 Self::with_condition(property, DynamicSelector::Os(os))
1023 }
1024
1025 pub const fn dark_theme(property: CssProperty) -> Self {
1027 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
1028 }
1029
1030 pub const fn light_theme(property: CssProperty) -> Self {
1032 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
1033 }
1034
1035 pub const fn on_windows(property: CssProperty) -> Self {
1037 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
1038 }
1039
1040 pub const fn on_macos(property: CssProperty) -> Self {
1042 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
1043 }
1044
1045 pub const fn on_linux(property: CssProperty) -> Self {
1047 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
1048 }
1049
1050 pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1052 if self.apply_if.as_slice().is_empty() {
1054 return true;
1055 }
1056
1057 self.apply_if
1059 .as_slice()
1060 .iter()
1061 .all(|selector| selector.matches(ctx))
1062 }
1063
1064 pub fn is_conditional(&self) -> bool {
1066 !self.apply_if.as_slice().is_empty()
1067 }
1068
1069 pub fn is_pseudo_state_only(&self) -> bool {
1072 let conditions = self.apply_if.as_slice();
1073 !conditions.is_empty()
1074 && conditions
1075 .iter()
1076 .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
1077 }
1078
1079 pub fn is_layout_affecting(&self) -> bool {
1085 self.property.get_type().can_trigger_relayout()
1086 }
1087}
1088
1089impl_vec!(CssPropertyWithConditions, CssPropertyWithConditionsVec, CssPropertyWithConditionsVecDestructor, CssPropertyWithConditionsVecDestructorType, CssPropertyWithConditionsVecSlice, OptionCssPropertyWithConditions);
1090impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1091impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1092impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1093impl_vec_clone!(
1094 CssPropertyWithConditions,
1095 CssPropertyWithConditionsVec,
1096 CssPropertyWithConditionsVecDestructor
1097);
1098
1099impl Eq for CssPropertyWithConditionsVec {}
1101
1102impl Ord for CssPropertyWithConditionsVec {
1103 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1104 self.as_slice().len().cmp(&other.as_slice().len())
1105 }
1106}
1107
1108impl core::hash::Hash for CssPropertyWithConditions {
1109 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1110 self.property.hash(state);
1111 self.apply_if.as_slice().len().hash(state);
1113 }
1114}
1115
1116impl core::hash::Hash for CssPropertyWithConditionsVec {
1117 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1118 for item in self.as_slice() {
1119 item.hash(state);
1120 }
1121 }
1122}
1123
1124impl CssPropertyWithConditionsVec {
1125 #[cfg(feature = "parser")]
1145 pub fn parse(style: &str) -> Self {
1146 Self::parse_with_conditions(style, Vec::new())
1147 }
1148
1149 #[cfg(feature = "parser")]
1151 fn parse_with_conditions(style: &str, inherited_conditions: Vec<DynamicSelector>) -> Self {
1152 use crate::props::property::{
1153 parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
1154 CssPropertyType,
1155 };
1156
1157 let mut props = Vec::new();
1158 let key_map = CssKeyMap::get();
1159 let style = style.trim();
1160
1161 if style.is_empty() {
1162 return CssPropertyWithConditionsVec::from_vec(props);
1163 }
1164
1165 let mut chars = style.chars().peekable();
1167 let mut current_segment = String::new();
1168 let mut brace_depth = 0;
1169
1170 while let Some(c) = chars.next() {
1171 match c {
1172 '{' => {
1173 brace_depth += 1;
1174 current_segment.push(c);
1175 }
1176 '}' => {
1177 brace_depth -= 1;
1178 current_segment.push(c);
1179
1180 if brace_depth == 0 {
1181 let segment = current_segment.trim().to_string();
1183 current_segment.clear();
1184
1185 if let Some(parsed) = Self::parse_block_segment(&segment, &inherited_conditions, &key_map) {
1186 props.extend(parsed);
1187 }
1188 }
1189 }
1190 ';' if brace_depth == 0 => {
1191 let segment = current_segment.trim().to_string();
1193 current_segment.clear();
1194
1195 if !segment.is_empty() {
1196 if let Some(parsed) = Self::parse_property_segment(&segment, &inherited_conditions, &key_map) {
1197 props.extend(parsed);
1198 }
1199 }
1200 }
1201 _ => {
1202 current_segment.push(c);
1203 }
1204 }
1205 }
1206
1207 let remaining = current_segment.trim();
1209 if !remaining.is_empty() && !remaining.contains('{') {
1210 if let Some(parsed) = Self::parse_property_segment(remaining, &inherited_conditions, &key_map) {
1211 props.extend(parsed);
1212 }
1213 }
1214
1215 CssPropertyWithConditionsVec::from_vec(props)
1216 }
1217
1218 #[cfg(feature = "parser")]
1220 fn parse_block_segment(
1221 segment: &str,
1222 inherited_conditions: &[DynamicSelector],
1223 key_map: &crate::props::property::CssKeyMap,
1224 ) -> Option<Vec<CssPropertyWithConditions>> {
1225 let brace_pos = segment.find('{')?;
1227 let selector = segment[..brace_pos].trim();
1228
1229 let content_start = brace_pos + 1;
1231 let content_end = segment.rfind('}')?;
1232 if content_end <= content_start {
1233 return None;
1234 }
1235 let content = &segment[content_start..content_end];
1236
1237 let mut conditions = inherited_conditions.to_vec();
1239
1240 if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
1241 conditions.extend(new_conditions);
1242 } else {
1243 return None;
1245 }
1246
1247 let parsed = Self::parse_with_conditions(content, conditions);
1249 Some(parsed.into_library_owned_vec())
1250 }
1251
1252 #[cfg(feature = "parser")]
1254 fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
1255 let selector = selector.trim();
1256
1257 if selector.starts_with(':') {
1259 let pseudo = &selector[1..];
1260 match pseudo {
1261 "hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
1262 "active" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)]),
1263 "focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
1264 "focus-within" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::FocusWithin)]),
1265 "disabled" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Disabled)]),
1266 "checked" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Checked)]),
1267 "visited" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)]),
1268 "backdrop" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Backdrop)]),
1269 "dragging" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Dragging)]),
1270 "drag-over" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::DragOver)]),
1271 _ => return None,
1272 }
1273 }
1274
1275 if selector.starts_with('@') {
1277 let rule_content = &selector[1..];
1278
1279 if rule_content.starts_with("os ") {
1281 let os_name = rule_content[3..].trim();
1282 if let Some(os_cond) = Self::parse_os_name(os_name) {
1283 return Some(vec![DynamicSelector::Os(os_cond)]);
1284 }
1285 }
1286
1287 if rule_content.starts_with("media ") {
1289 let media_query = rule_content[6..].trim();
1290 if let Some(media_conds) = Self::parse_media_query(media_query) {
1291 return Some(media_conds);
1292 }
1293 }
1294
1295 if rule_content.starts_with("theme ") {
1297 let theme = rule_content[6..].trim();
1298 match theme {
1299 "dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
1300 "light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
1301 _ => return None,
1302 }
1303 }
1304
1305 return None;
1306 }
1307
1308 if selector == "*" {
1310 return Some(vec![]);
1311 }
1312
1313 if selector.is_empty() {
1315 return Some(vec![]);
1316 }
1317
1318 None
1319 }
1320
1321 #[cfg(feature = "parser")]
1323 fn parse_os_name(name: &str) -> Option<OsCondition> {
1324 match name.to_lowercase().as_str() {
1325 "linux" => Some(OsCondition::Linux),
1326 "windows" | "win" => Some(OsCondition::Windows),
1327 "macos" | "mac" | "osx" => Some(OsCondition::MacOS),
1328 "ios" => Some(OsCondition::IOS),
1329 "android" => Some(OsCondition::Android),
1330 "apple" => Some(OsCondition::Apple),
1331 "web" | "wasm" => Some(OsCondition::Web),
1332 "any" | "*" => Some(OsCondition::Any),
1333 _ => None,
1334 }
1335 }
1336
1337 #[cfg(feature = "parser")]
1339 fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
1340 let query = query.trim();
1341
1342 if query.starts_with('(') && query.ends_with(')') {
1344 let inner = &query[1..query.len()-1];
1345 if let Some((key, value)) = inner.split_once(':') {
1346 let key = key.trim();
1347 let value = value.trim();
1348
1349 let px_value = value.strip_suffix("px")
1351 .and_then(|v| v.trim().parse::<f32>().ok());
1352
1353 match key {
1354 "min-width" => {
1355 if let Some(px) = px_value {
1356 return Some(vec![DynamicSelector::ViewportWidth(
1357 MinMaxRange::with_min(px)
1358 )]);
1359 }
1360 }
1361 "max-width" => {
1362 if let Some(px) = px_value {
1363 return Some(vec![DynamicSelector::ViewportWidth(
1364 MinMaxRange::with_max(px)
1365 )]);
1366 }
1367 }
1368 "min-height" => {
1369 if let Some(px) = px_value {
1370 return Some(vec![DynamicSelector::ViewportHeight(
1371 MinMaxRange::with_min(px)
1372 )]);
1373 }
1374 }
1375 "max-height" => {
1376 if let Some(px) = px_value {
1377 return Some(vec![DynamicSelector::ViewportHeight(
1378 MinMaxRange::with_max(px)
1379 )]);
1380 }
1381 }
1382 _ => {}
1383 }
1384 }
1385 }
1386
1387 match query {
1389 "screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
1390 "print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
1391 "all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
1392 _ => None,
1393 }
1394 }
1395
1396 #[cfg(feature = "parser")]
1398 fn parse_property_segment(
1399 segment: &str,
1400 inherited_conditions: &[DynamicSelector],
1401 key_map: &crate::props::property::CssKeyMap,
1402 ) -> Option<Vec<CssPropertyWithConditions>> {
1403 use crate::props::property::{
1404 parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
1405 CssPropertyType,
1406 };
1407
1408 let segment = segment.trim();
1409 if segment.is_empty() {
1410 return None;
1411 }
1412
1413 let (key, value) = segment.split_once(':')?;
1414 let key = key.trim();
1415 let value = value.trim();
1416
1417 let mut props = Vec::new();
1418 let conditions = if inherited_conditions.is_empty() {
1419 DynamicSelectorVec::from_const_slice(&[])
1420 } else {
1421 DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
1422 };
1423
1424 if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
1426 if let Ok(prop) = parse_css_property(prop_type, value) {
1427 props.push(CssPropertyWithConditions {
1428 property: prop,
1429 apply_if: conditions.clone(),
1430 });
1431 return Some(props);
1432 }
1433 }
1434
1435 if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
1437 if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
1438 for prop in expanded_props {
1439 props.push(CssPropertyWithConditions {
1440 property: prop,
1441 apply_if: conditions.clone(),
1442 });
1443 }
1444 return Some(props);
1445 }
1446 }
1447
1448 None
1449 }
1450
1451 #[cfg(feature = "parser")]
1455 pub fn parse_normal(style: &str) -> Self {
1456 Self::parse(style)
1457 }
1458
1459 #[cfg(feature = "parser")]
1463 pub fn parse_hover(style: &str) -> Self {
1464 let wrapped = format!(":hover {{ {} }}", style);
1466 Self::parse(&wrapped)
1467 }
1468
1469 #[cfg(feature = "parser")]
1473 pub fn parse_active(style: &str) -> Self {
1474 let wrapped = format!(":active {{ {} }}", style);
1475 Self::parse(&wrapped)
1476 }
1477
1478 #[cfg(feature = "parser")]
1482 pub fn parse_focus(style: &str) -> Self {
1483 let wrapped = format!(":focus {{ {} }}", style);
1484 Self::parse(&wrapped)
1485 }
1486}
1487
1488#[cfg(test)]
1489mod tests {
1490 use super::*;
1491
1492 #[test]
1493 fn test_inline_overflow_parse() {
1494 let style = "overflow: scroll;";
1495 let parsed = CssPropertyWithConditionsVec::parse(style);
1496 let props = parsed.into_library_owned_vec();
1497 eprintln!("Parsed {} properties from '{}'", props.len(), style);
1498 for prop in &props {
1499 eprintln!(" {:?}", prop.property);
1500 }
1501 assert!(props.len() > 0, "Expected overflow to parse into at least 1 property");
1502 }
1503
1504 #[test]
1505 fn test_inline_overflow_y_parse() {
1506 let style = "overflow-y: scroll;";
1507 let parsed = CssPropertyWithConditionsVec::parse(style);
1508 let props = parsed.into_library_owned_vec();
1509 eprintln!("Parsed {} properties from '{}'", props.len(), style);
1510 for prop in &props {
1511 eprintln!(" {:?}", prop.property);
1512 }
1513 assert!(props.len() > 0, "Expected overflow-y to parse into at least 1 property");
1514 }
1515
1516 #[test]
1517 fn test_inline_combined_style_with_overflow() {
1518 let style = "padding: 20px; background-color: #f0f0f0; font-size: 14px; color: #222;overflow: scroll;";
1519 let parsed = CssPropertyWithConditionsVec::parse(style);
1520 let props = parsed.into_library_owned_vec();
1521 eprintln!("Parsed {} properties from combined style", props.len());
1522 for prop in &props {
1523 eprintln!(" {:?}", prop.property);
1524 }
1525 assert!(props.len() >= 9, "Expected at least 9 properties, got {}", props.len());
1527 }
1528
1529 #[test]
1530 fn test_inline_grid_template_columns_parse() {
1531 use crate::props::layout::grid::GridTrackSizing;
1532 let style = "display: grid; grid-template-columns: repeat(4, 160px); gap: 16px; padding: 10px;";
1533 let parsed = CssPropertyWithConditionsVec::parse(style);
1534 let props = parsed.into_library_owned_vec();
1535 eprintln!("Parsed {} properties from grid style", props.len());
1536 for prop in &props {
1537 eprintln!(" {:?}", prop.property);
1538 }
1539 let grid_cols = props.iter().find(|p| {
1541 matches!(p.property, CssProperty::GridTemplateColumns(_))
1542 }).expect("Expected GridTemplateColumns property");
1543
1544 if let CssProperty::GridTemplateColumns(ref value) = grid_cols.property {
1545 let template = value.get_property().expect("Expected Exact value");
1546 let tracks = template.tracks.as_ref();
1547 assert_eq!(tracks.len(), 4, "Expected 4 tracks");
1548 for (i, track) in tracks.iter().enumerate() {
1549 eprintln!(" Track {}: {:?} (is_fixed={})", i, track, matches!(track, GridTrackSizing::Fixed(_)));
1550 assert!(matches!(track, GridTrackSizing::Fixed(_)),
1551 "Track {} should be Fixed(160px), got {:?}", i, track);
1552 }
1553 } else {
1554 panic!("Expected CssProperty::GridTemplateColumns");
1555 }
1556 }
1557}