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::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#[repr(C, u8)]
49#[derive(Debug, Clone, PartialEq)]
50pub enum DynamicSelector {
51 Os(OsCondition) = 0,
53 OsVersion(OsVersionCondition) = 1,
55 Media(MediaType) = 2,
57 ViewportWidth(MinMaxRange) = 3,
59 ViewportHeight(MinMaxRange) = 4,
61 ContainerWidth(MinMaxRange) = 5,
63 ContainerHeight(MinMaxRange) = 6,
65 ContainerName(AzString) = 7,
67 Theme(ThemeCondition) = 8,
69 AspectRatio(MinMaxRange) = 9,
71 Orientation(OrientationType) = 10,
73 PrefersReducedMotion(BoolCondition) = 11,
75 PrefersHighContrast(BoolCondition) = 12,
77 PseudoState(PseudoStateType) = 13,
79 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#[repr(C)]
102#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
103pub struct MinMaxRange {
104 pub min: f32,
106 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 pub const fn with_min(min_val: f32) -> Self {
120 Self {
121 min: min_val,
122 max: f32::NAN,
123 }
124 }
125
126 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#[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#[repr(C)]
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
185pub enum OsCondition {
186 Any,
187 Apple, MacOS,
189 IOS,
190 Linux,
191 Windows,
192 Android,
193 Web, }
195
196impl_option!(
197 OsCondition,
198 OptionOsCondition,
199 [Debug, Clone, Copy, PartialEq, Eq, Hash]
200);
201
202impl OsCondition {
203 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 Min(OsVersion),
223 Max(OsVersion),
225 Exact(OsVersion),
227 DesktopEnvironment(LinuxDesktopEnv),
229 DesktopEnvMin(DesktopEnvVersion),
231 DesktopEnvMax(DesktopEnvVersion),
233 DesktopEnvExact(DesktopEnvVersion),
235}
236
237#[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#[repr(C)]
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
252pub struct OsVersion {
253 pub os: OsFamily,
255 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 pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
274 if self.os != other.os {
275 None } else {
277 Some(self.version_id.cmp(&other.version_id))
278 }
279 }
280
281 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 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 pub fn is_exactly(&self, other: &Self) -> bool {
302 self.compare(other) == Some(core::cmp::Ordering::Equal)
303 }
304}
305
306#[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
317impl OsVersion {
323 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);
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 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);
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 pub const fn unknown() -> Self {
428 Self {
429 os: OsFamily::Linux, version_id: 0,
431 }
432 }
433}
434
435pub 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 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 "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
475fn 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 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 let s = strip_os_prefix(s, &["linux"]);
577 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#[repr(C)]
591#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
592pub enum LinuxDesktopEnv {
593 Gnome,
594 KDE,
595 XFCE,
597 Unity,
599 Cinnamon,
601 MATE,
603 Other,
604}
605
606impl LinuxDesktopEnv {
607 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#[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 SystemPreferred,
635}
636
637impl_option!(
638 ThemeCondition,
639 OptionThemeCondition,
640 copy = false,
641 [Debug, Clone, PartialEq, Eq, Hash]
642);
643
644impl ThemeCondition {
645 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#[repr(C)]
657#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
658pub enum OrientationType {
659 Portrait,
660 Landscape,
661}
662
663#[repr(C, u8)]
666#[derive(Debug, Clone, PartialEq, Eq, Hash)]
667pub enum LanguageCondition {
668 Exact(AzString),
670 Prefix(AzString),
672}
673
674impl LanguageCondition {
675 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 let lang_prefix = &language[..prefix_str.len()];
686 if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
687 return false;
688 }
689 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 Normal,
702 Hover,
704 Active,
706 Focus,
708 Disabled,
710 CheckedTrue,
712 CheckedFalse,
714 FocusWithin,
716 Visited,
718 Backdrop,
720 Dragging,
722 DragOver,
724}
725
726impl_option!(
727 LinuxDesktopEnv,
728 OptionLinuxDesktopEnv,
729 [Debug, Clone, Copy, PartialEq, Eq, Hash]
730);
731
732pub const DEFAULT_VIEWPORT_WIDTH: f32 = 800.0;
734pub const DEFAULT_VIEWPORT_HEIGHT: f32 = 600.0;
736
737#[repr(C)]
739#[derive(Debug, Clone)]
740pub struct DynamicSelectorContext {
741 pub os: OsCondition,
743 pub os_version: OsVersion,
744 pub desktop_env: OptionLinuxDesktopEnv,
745 pub de_version: u32,
750
751 pub theme: ThemeCondition,
753
754 pub media_type: MediaType,
756 pub viewport_width: f32,
757 pub viewport_height: f32,
758
759 pub container_width: f32,
762 pub container_height: f32,
763 pub container_name: OptionString,
764
765 pub prefers_reduced_motion: BoolCondition,
767 pub prefers_high_contrast: BoolCondition,
768
769 pub orientation: OrientationType,
771
772 pub pseudo_state: PseudoStateFlags,
774
775 pub language: AzString,
777
778 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 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, desktop_env,
822 de_version: 0, theme,
824 media_type: MediaType::Screen,
825 viewport_width: DEFAULT_VIEWPORT_WIDTH, 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 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 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 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 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 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 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, 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#[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 let (subject, op_and_version) = split_op_and_version(inner);
1002 let subject = subject.trim();
1003
1004 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 out.push(DynamicSelector::Os(family));
1021
1022 match (de, op_and_version) {
1023 (Some(env), None) => {
1025 out.push(DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(env)));
1026 }
1027 (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 (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 _ => 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 (None, None) => {}
1059 }
1060
1061 Some(out)
1062}
1063
1064#[cfg(feature = "parser")]
1065#[derive(Copy, Clone)]
1066enum VersionOp { Min, Max, Exact }
1067
1068#[cfg(feature = "parser")]
1071fn split_op_and_version(s: &str) -> (&str, Option<(VersionOp, &str)>) {
1072 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#[repr(C)]
1135#[derive(Debug, Clone, PartialEq)]
1136pub struct CssPropertyWithConditions {
1137 pub property: CssProperty,
1139 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 self.apply_if
1163 .as_slice()
1164 .len()
1165 .cmp(&other.apply_if.as_slice().len())
1166 }
1167}
1168
1169impl CssPropertyWithConditions {
1170 pub const fn simple(property: CssProperty) -> Self {
1172 Self {
1173 property,
1174 apply_if: DynamicSelectorVec::from_const_slice(&[]),
1175 }
1176 }
1177
1178 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 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 pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
1199 Self {
1200 property,
1201 apply_if: conditions,
1202 }
1203 }
1204
1205 pub const fn on_hover(property: CssProperty) -> Self {
1207 Self::with_single_condition(
1208 property,
1209 &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
1210 )
1211 }
1212
1213 pub const fn on_active(property: CssProperty) -> Self {
1215 Self::with_single_condition(
1216 property,
1217 &[DynamicSelector::PseudoState(PseudoStateType::Active)],
1218 )
1219 }
1220
1221 pub const fn on_focus(property: CssProperty) -> Self {
1223 Self::with_single_condition(
1224 property,
1225 &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
1226 )
1227 }
1228
1229 pub const fn when_disabled(property: CssProperty) -> Self {
1231 Self::with_single_condition(
1232 property,
1233 &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
1234 )
1235 }
1236
1237 pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
1239 Self::with_condition(property, DynamicSelector::Os(os))
1240 }
1241
1242 pub const fn dark_theme(property: CssProperty) -> Self {
1244 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
1245 }
1246
1247 pub const fn light_theme(property: CssProperty) -> Self {
1249 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
1250 }
1251
1252 pub const fn on_windows(property: CssProperty) -> Self {
1254 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
1255 }
1256
1257 pub const fn on_macos(property: CssProperty) -> Self {
1259 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
1260 }
1261
1262 pub const fn on_linux(property: CssProperty) -> Self {
1264 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
1265 }
1266
1267 pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1269 if self.apply_if.as_slice().is_empty() {
1271 return true;
1272 }
1273
1274 self.apply_if
1276 .as_slice()
1277 .iter()
1278 .all(|selector| selector.matches(ctx))
1279 }
1280
1281 pub fn is_conditional(&self) -> bool {
1283 !self.apply_if.as_slice().is_empty()
1284 }
1285
1286 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 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
1316impl 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 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 #[cfg(feature = "parser")]
1362 pub fn parse(style: &str) -> Self {
1363 Self::parse_with_conditions(style, Vec::new())
1364 }
1365
1366 #[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 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 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 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 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 #[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 let brace_pos = segment.find('{')?;
1444 let selector = segment[..brace_pos].trim();
1445
1446 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 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 return None;
1462 }
1463
1464 let parsed = Self::parse_with_conditions(content, conditions);
1466 Some(parsed.into_library_owned_vec())
1467 }
1468
1469 #[cfg(feature = "parser")]
1471 fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
1472 let selector = selector.trim();
1473
1474 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 if let Some(rule_content) = selector.strip_prefix('@') {
1493 return Self::parse_at_rule(rule_content);
1494 }
1495
1496 if selector == "*" {
1498 return Some(vec![]);
1499 }
1500
1501 if selector.is_empty() {
1503 return Some(vec![]);
1504 }
1505
1506 None
1507 }
1508
1509 #[cfg(feature = "parser")]
1513 fn parse_at_rule(rule_content: &str) -> Option<Vec<DynamicSelector>> {
1514 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 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 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 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 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..] } else {
1569 rule_content[10..].trim()
1570 };
1571 let mut conds = Vec::new();
1572 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 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 if rule_content == "prefers-reduced-motion" || rule_content == "reduced-motion" {
1618 return Some(vec![DynamicSelector::PrefersReducedMotion(BoolCondition::True)]);
1619 }
1620
1621 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 #[cfg(feature = "parser")]
1631 fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
1632 let query = query.trim();
1633
1634 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 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 if let Some(sel) = Self::parse_media_feature_inline(other, value) {
1677 return Some(vec![sel]);
1678 }
1679 }
1680 }
1681 }
1682 }
1683
1684 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 #[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 #[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 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 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 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 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}