use crate::corety::{AzString, OptionString};
use crate::props::property::CssProperty;
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct PseudoStateFlags {
pub hover: bool,
pub active: bool,
pub focused: bool,
pub disabled: bool,
pub checked: bool,
pub focus_within: bool,
pub visited: bool,
pub backdrop: bool,
pub dragging: bool,
pub drag_over: bool,
}
impl PseudoStateFlags {
pub fn has_state(&self, state: PseudoStateType) -> bool {
match state {
PseudoStateType::Normal => true,
PseudoStateType::Hover => self.hover,
PseudoStateType::Active => self.active,
PseudoStateType::Focus => self.focused,
PseudoStateType::Disabled => self.disabled,
PseudoStateType::CheckedTrue => self.checked,
PseudoStateType::CheckedFalse => !self.checked,
PseudoStateType::FocusWithin => self.focus_within,
PseudoStateType::Visited => self.visited,
PseudoStateType::Backdrop => self.backdrop,
PseudoStateType::Dragging => self.dragging,
PseudoStateType::DragOver => self.drag_over,
}
}
}
#[repr(C, u8)]
#[derive(Debug, Clone, PartialEq)]
pub enum DynamicSelector {
Os(OsCondition) = 0,
OsVersion(OsVersionCondition) = 1,
Media(MediaType) = 2,
ViewportWidth(MinMaxRange) = 3,
ViewportHeight(MinMaxRange) = 4,
ContainerWidth(MinMaxRange) = 5,
ContainerHeight(MinMaxRange) = 6,
ContainerName(AzString) = 7,
Theme(ThemeCondition) = 8,
AspectRatio(MinMaxRange) = 9,
Orientation(OrientationType) = 10,
PrefersReducedMotion(BoolCondition) = 11,
PrefersHighContrast(BoolCondition) = 12,
PseudoState(PseudoStateType) = 13,
Language(LanguageCondition) = 14,
}
impl_option!(
DynamicSelector,
OptionDynamicSelector,
copy = false,
[Debug, Clone, PartialEq]
);
impl_vec!(DynamicSelector, DynamicSelectorVec, DynamicSelectorVecDestructor, DynamicSelectorVecDestructorType, DynamicSelectorVecSlice, OptionDynamicSelector);
impl_vec_clone!(
DynamicSelector,
DynamicSelectorVec,
DynamicSelectorVecDestructor
);
impl_vec_debug!(DynamicSelector, DynamicSelectorVec);
impl_vec_partialeq!(DynamicSelector, DynamicSelectorVec);
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct MinMaxRange {
pub min: f32,
pub max: f32,
}
impl MinMaxRange {
pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
Self {
min: if let Some(m) = min { m } else { f32::NAN },
max: if let Some(m) = max { m } else { f32::NAN },
}
}
pub const fn with_min(min_val: f32) -> Self {
Self {
min: min_val,
max: f32::NAN,
}
}
pub const fn with_max(max_val: f32) -> Self {
Self {
min: f32::NAN,
max: max_val,
}
}
pub fn min(&self) -> Option<f32> {
if self.min.is_nan() {
None
} else {
Some(self.min)
}
}
pub fn max(&self) -> Option<f32> {
if self.max.is_nan() {
None
} else {
Some(self.max)
}
}
pub fn matches(&self, value: f32) -> bool {
let min_ok = self.min.is_nan() || value >= self.min;
let max_ok = self.max.is_nan() || value <= self.max;
min_ok && max_ok
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum BoolCondition {
#[default]
False,
True,
}
impl From<bool> for BoolCondition {
fn from(b: bool) -> Self {
if b {
Self::True
} else {
Self::False
}
}
}
impl From<BoolCondition> for bool {
fn from(b: BoolCondition) -> Self {
matches!(b, BoolCondition::True)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OsCondition {
Any,
Apple, MacOS,
IOS,
Linux,
Windows,
Android,
Web, }
impl_option!(
OsCondition,
OptionOsCondition,
[Debug, Clone, Copy, PartialEq, Eq, Hash]
);
impl OsCondition {
pub fn from_system_platform(platform: &crate::system::Platform) -> Self {
use crate::system::Platform;
match platform {
Platform::Windows => OsCondition::Windows,
Platform::MacOs => OsCondition::MacOS,
Platform::Linux(_) => OsCondition::Linux,
Platform::Android => OsCondition::Android,
Platform::Ios => OsCondition::IOS,
Platform::Unknown => OsCondition::Any,
}
}
}
#[repr(C, u8)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OsVersionCondition {
Min(OsVersion),
Max(OsVersion),
Exact(OsVersion),
DesktopEnvironment(LinuxDesktopEnv),
DesktopEnvMin(DesktopEnvVersion),
DesktopEnvMax(DesktopEnvVersion),
DesktopEnvExact(DesktopEnvVersion),
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DesktopEnvVersion {
pub env: LinuxDesktopEnv,
pub version_id: u32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OsVersion {
pub os: OsFamily,
pub version_id: u32,
}
impl Default for OsVersion {
fn default() -> Self {
Self::unknown()
}
}
impl OsVersion {
pub const fn new(os: OsFamily, version_id: u32) -> Self {
Self { os, version_id }
}
pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
if self.os != other.os {
None } else {
Some(self.version_id.cmp(&other.version_id))
}
}
pub fn is_at_least(&self, other: &Self) -> bool {
self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Less)
}
pub fn is_at_most(&self, other: &Self) -> bool {
self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Greater)
}
}
impl_option!(
OsVersion,
OptionOsVersion,
[Debug, Clone, Copy, PartialEq, Eq, Hash]
);
impl OsVersion {
pub fn is_exactly(&self, other: &Self) -> bool {
self.compare(other) == Some(core::cmp::Ordering::Equal)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OsFamily {
Windows,
MacOS,
IOS,
Linux,
Android,
}
impl OsVersion {
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);
pub const IOS_2: Self = Self::new(OsFamily::IOS, 200);
pub const IOS_3: Self = Self::new(OsFamily::IOS, 300);
pub const IOS_4: Self = Self::new(OsFamily::IOS, 400);
pub const IOS_5: Self = Self::new(OsFamily::IOS, 500);
pub const IOS_6: Self = Self::new(OsFamily::IOS, 600);
pub const IOS_7: Self = Self::new(OsFamily::IOS, 700);
pub const IOS_8: Self = Self::new(OsFamily::IOS, 800);
pub const IOS_9: Self = Self::new(OsFamily::IOS, 900);
pub const IOS_10: Self = Self::new(OsFamily::IOS, 1000);
pub const IOS_11: Self = Self::new(OsFamily::IOS, 1100);
pub const IOS_12: Self = Self::new(OsFamily::IOS, 1200);
pub const IOS_13: Self = Self::new(OsFamily::IOS, 1300);
pub const IOS_14: Self = Self::new(OsFamily::IOS, 1400);
pub const IOS_15: Self = Self::new(OsFamily::IOS, 1500);
pub const IOS_16: Self = Self::new(OsFamily::IOS, 1600);
pub const IOS_17: Self = Self::new(OsFamily::IOS, 1700);
pub const IOS_18: Self = Self::new(OsFamily::IOS, 1800);
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);
pub const LINUX_3_0: Self = Self::new(OsFamily::Linux, 3000);
pub const LINUX_4_0: Self = Self::new(OsFamily::Linux, 4000);
pub const LINUX_5_0: Self = Self::new(OsFamily::Linux, 5000);
pub const LINUX_6_0: Self = Self::new(OsFamily::Linux, 6000);
pub const fn unknown() -> Self {
Self {
os: OsFamily::Linux, version_id: 0,
}
}
}
pub fn parse_os_version(os: OsFamily, version_str: &str) -> Option<OsVersion> {
let version_str = version_str.trim().to_lowercase();
let version_str = version_str.as_str();
match os {
OsFamily::Windows => parse_windows_version(version_str),
OsFamily::MacOS => parse_macos_version(version_str),
OsFamily::IOS => parse_ios_version(version_str),
OsFamily::Android => parse_android_version(version_str),
OsFamily::Linux => parse_linux_version(version_str),
}
}
fn parse_windows_version(s: &str) -> Option<OsVersion> {
let core = strip_os_prefix(s, &["windows", "win"]);
match core {
"2000" => Some(OsVersion::WIN_2000),
"xp" => Some(OsVersion::WIN_XP),
"vista" => Some(OsVersion::WIN_VISTA),
"7" => Some(OsVersion::WIN_7),
"8" => Some(OsVersion::WIN_8),
"8.1" | "8-1" => Some(OsVersion::WIN_8_1),
"10" => Some(OsVersion::WIN_10),
"11" => Some(OsVersion::WIN_11),
"5.0" | "nt5.0" => Some(OsVersion::WIN_2000),
"5.1" | "nt5.1" => Some(OsVersion::WIN_XP),
"6.0" | "nt6.0" => Some(OsVersion::WIN_VISTA),
"6.1" | "nt6.1" => Some(OsVersion::WIN_7),
"6.2" | "nt6.2" => Some(OsVersion::WIN_8),
"6.3" | "nt6.3" => Some(OsVersion::WIN_8_1),
"10.0" | "nt10.0" => Some(OsVersion::WIN_10),
_ => None,
}
}
fn strip_os_prefix<'a>(s: &'a str, prefixes: &[&str]) -> &'a str {
for p in prefixes {
if let Some(rest) = s.strip_prefix(p) {
return rest.strip_prefix(['-', '_']).unwrap_or(rest);
}
}
s
}
fn parse_macos_version(s: &str) -> Option<OsVersion> {
match s {
"cheetah" | "10.0" => Some(OsVersion::MACOS_CHEETAH),
"puma" | "10.1" => Some(OsVersion::MACOS_PUMA),
"jaguar" | "10.2" => Some(OsVersion::MACOS_JAGUAR),
"panther" | "10.3" => Some(OsVersion::MACOS_PANTHER),
"tiger" | "10.4" => Some(OsVersion::MACOS_TIGER),
"leopard" | "10.5" => Some(OsVersion::MACOS_LEOPARD),
"snow-leopard" | "snowleopard" | "10.6" => Some(OsVersion::MACOS_SNOW_LEOPARD),
"lion" | "10.7" => Some(OsVersion::MACOS_LION),
"mountain-lion" | "mountainlion" | "10.8" => Some(OsVersion::MACOS_MOUNTAIN_LION),
"mavericks" | "10.9" => Some(OsVersion::MACOS_MAVERICKS),
"yosemite" | "10.10" => Some(OsVersion::MACOS_YOSEMITE),
"el-capitan" | "elcapitan" | "10.11" => Some(OsVersion::MACOS_EL_CAPITAN),
"sierra" | "10.12" => Some(OsVersion::MACOS_SIERRA),
"high-sierra" | "highsierra" | "10.13" => Some(OsVersion::MACOS_HIGH_SIERRA),
"mojave" | "10.14" => Some(OsVersion::MACOS_MOJAVE),
"catalina" | "10.15" => Some(OsVersion::MACOS_CATALINA),
"big-sur" | "bigsur" | "11" | "11.0" => Some(OsVersion::MACOS_BIG_SUR),
"monterey" | "12" | "12.0" => Some(OsVersion::MACOS_MONTEREY),
"ventura" | "13" | "13.0" => Some(OsVersion::MACOS_VENTURA),
"sonoma" | "14" | "14.0" => Some(OsVersion::MACOS_SONOMA),
"sequoia" | "15" | "15.0" => Some(OsVersion::MACOS_SEQUOIA),
"tahoe" | "26" | "26.0" => Some(OsVersion::MACOS_TAHOE),
_ => None,
}
}
fn parse_ios_version(s: &str) -> Option<OsVersion> {
match s {
"1" | "1.0" => Some(OsVersion::IOS_1),
"2" | "2.0" => Some(OsVersion::IOS_2),
"3" | "3.0" => Some(OsVersion::IOS_3),
"4" | "4.0" => Some(OsVersion::IOS_4),
"5" | "5.0" => Some(OsVersion::IOS_5),
"6" | "6.0" => Some(OsVersion::IOS_6),
"7" | "7.0" => Some(OsVersion::IOS_7),
"8" | "8.0" => Some(OsVersion::IOS_8),
"9" | "9.0" => Some(OsVersion::IOS_9),
"10" | "10.0" => Some(OsVersion::IOS_10),
"11" | "11.0" => Some(OsVersion::IOS_11),
"12" | "12.0" => Some(OsVersion::IOS_12),
"13" | "13.0" => Some(OsVersion::IOS_13),
"14" | "14.0" => Some(OsVersion::IOS_14),
"15" | "15.0" => Some(OsVersion::IOS_15),
"16" | "16.0" => Some(OsVersion::IOS_16),
"17" | "17.0" => Some(OsVersion::IOS_17),
"18" | "18.0" => Some(OsVersion::IOS_18),
_ => None,
}
}
fn parse_android_version(s: &str) -> Option<OsVersion> {
match s {
"cupcake" | "1.5" => Some(OsVersion::ANDROID_CUPCAKE),
"donut" | "1.6" => Some(OsVersion::ANDROID_DONUT),
"eclair" | "2.1" => Some(OsVersion::ANDROID_ECLAIR),
"froyo" | "2.2" => Some(OsVersion::ANDROID_FROYO),
"gingerbread" | "2.3" => Some(OsVersion::ANDROID_GINGERBREAD),
"honeycomb" | "3.0" | "3.2" => Some(OsVersion::ANDROID_HONEYCOMB),
"ice-cream-sandwich" | "ics" | "4.0" => Some(OsVersion::ANDROID_ICE_CREAM_SANDWICH),
"jelly-bean" | "jellybean" | "4.3" => Some(OsVersion::ANDROID_JELLY_BEAN),
"kitkat" | "4.4" => Some(OsVersion::ANDROID_KITKAT),
"lollipop" | "5.0" | "5.1" => Some(OsVersion::ANDROID_LOLLIPOP),
"marshmallow" | "6.0" => Some(OsVersion::ANDROID_MARSHMALLOW),
"nougat" | "7.0" | "7.1" => Some(OsVersion::ANDROID_NOUGAT),
"oreo" | "8.0" | "8.1" => Some(OsVersion::ANDROID_OREO),
"pie" | "9" | "9.0" => Some(OsVersion::ANDROID_PIE),
"10" | "q" => Some(OsVersion::ANDROID_10),
"11" | "r" => Some(OsVersion::ANDROID_11),
"12" | "s" => Some(OsVersion::ANDROID_12),
"12l" | "12L" => Some(OsVersion::ANDROID_12L),
"13" | "t" | "tiramisu" => Some(OsVersion::ANDROID_13),
"14" | "u" | "upside-down-cake" => Some(OsVersion::ANDROID_14),
"15" | "v" | "vanilla-ice-cream" => Some(OsVersion::ANDROID_15),
_ => {
if let Some(api) = s.strip_prefix("api") {
if let Ok(level) = api.trim().parse::<u32>() {
return Some(OsVersion::new(OsFamily::Android, level));
}
}
None
}
}
}
fn parse_linux_version(s: &str) -> Option<OsVersion> {
let s = strip_os_prefix(s, &["linux"]);
let mut parts = s.split('.');
let major = parts.next()?.parse::<u32>().ok()?;
let minor = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
let patch = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
Some(OsVersion::new(OsFamily::Linux, major * 1000 + minor * 10 + patch))
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LinuxDesktopEnv {
Gnome,
KDE,
XFCE,
Unity,
Cinnamon,
MATE,
Other,
}
impl LinuxDesktopEnv {
pub fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
use crate::system::DesktopEnvironment;
match de {
DesktopEnvironment::Gnome => LinuxDesktopEnv::Gnome,
DesktopEnvironment::Kde => LinuxDesktopEnv::KDE,
DesktopEnvironment::Other(_) => LinuxDesktopEnv::Other,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MediaType {
Screen,
Print,
All,
}
#[repr(C, u8)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ThemeCondition {
Light,
Dark,
Custom(AzString),
SystemPreferred,
}
impl_option!(
ThemeCondition,
OptionThemeCondition,
copy = false,
[Debug, Clone, PartialEq, Eq, Hash]
);
impl ThemeCondition {
pub fn from_system_theme(theme: crate::system::Theme) -> Self {
use crate::system::Theme;
match theme {
Theme::Light => ThemeCondition::Light,
Theme::Dark => ThemeCondition::Dark,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrientationType {
Portrait,
Landscape,
}
#[repr(C, u8)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LanguageCondition {
Exact(AzString),
Prefix(AzString),
}
impl LanguageCondition {
pub fn matches(&self, language: &str) -> bool {
match self {
LanguageCondition::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
LanguageCondition::Prefix(prefix) => {
let prefix_str = prefix.as_str();
if language.len() < prefix_str.len() {
return false;
}
let lang_prefix = &language[..prefix_str.len()];
if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
return false;
}
language.len() == prefix_str.len()
|| language.as_bytes().get(prefix_str.len()) == Some(&b'-')
}
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PseudoStateType {
Normal,
Hover,
Active,
Focus,
Disabled,
CheckedTrue,
CheckedFalse,
FocusWithin,
Visited,
Backdrop,
Dragging,
DragOver,
}
impl_option!(
LinuxDesktopEnv,
OptionLinuxDesktopEnv,
[Debug, Clone, Copy, PartialEq, Eq, Hash]
);
pub const DEFAULT_VIEWPORT_WIDTH: f32 = 800.0;
pub const DEFAULT_VIEWPORT_HEIGHT: f32 = 600.0;
#[repr(C)]
#[derive(Debug, Clone)]
pub struct DynamicSelectorContext {
pub os: OsCondition,
pub os_version: OsVersion,
pub desktop_env: OptionLinuxDesktopEnv,
pub de_version: u32,
pub theme: ThemeCondition,
pub media_type: MediaType,
pub viewport_width: f32,
pub viewport_height: f32,
pub container_width: f32,
pub container_height: f32,
pub container_name: OptionString,
pub prefers_reduced_motion: BoolCondition,
pub prefers_high_contrast: BoolCondition,
pub orientation: OrientationType,
pub pseudo_state: PseudoStateFlags,
pub language: AzString,
pub window_focused: bool,
}
impl Default for DynamicSelectorContext {
fn default() -> Self {
Self {
os: OsCondition::Any,
os_version: OsVersion::unknown(),
desktop_env: OptionLinuxDesktopEnv::None,
de_version: 0,
theme: ThemeCondition::Light,
media_type: MediaType::Screen,
viewport_width: DEFAULT_VIEWPORT_WIDTH,
viewport_height: DEFAULT_VIEWPORT_HEIGHT,
container_width: f32::NAN,
container_height: f32::NAN,
container_name: OptionString::None,
prefers_reduced_motion: BoolCondition::False,
prefers_high_contrast: BoolCondition::False,
orientation: OrientationType::Landscape,
pseudo_state: PseudoStateFlags::default(),
language: AzString::from_const_str("en-US"),
window_focused: true,
}
}
}
impl DynamicSelectorContext {
pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
let os = OsCondition::from_system_platform(&system_style.platform);
let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
} else {
OptionLinuxDesktopEnv::None
};
let theme = ThemeCondition::from_system_theme(system_style.theme);
Self {
os,
os_version: system_style.os_version, desktop_env,
de_version: 0, theme,
media_type: MediaType::Screen,
viewport_width: DEFAULT_VIEWPORT_WIDTH, viewport_height: DEFAULT_VIEWPORT_HEIGHT,
container_width: f32::NAN,
container_height: f32::NAN,
container_name: OptionString::None,
prefers_reduced_motion: system_style.prefers_reduced_motion,
prefers_high_contrast: system_style.prefers_high_contrast,
orientation: OrientationType::Landscape,
pseudo_state: PseudoStateFlags::default(),
language: system_style.language.clone(),
window_focused: true,
}
}
pub fn with_viewport(&self, width: f32, height: f32) -> Self {
let mut ctx = self.clone();
ctx.viewport_width = width;
ctx.viewport_height = height;
ctx.orientation = if width > height {
OrientationType::Landscape
} else {
OrientationType::Portrait
};
ctx
}
pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
let mut ctx = self.clone();
ctx.container_width = width;
ctx.container_height = height;
ctx.container_name = name.into();
ctx
}
pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
let mut ctx = self.clone();
ctx.pseudo_state = state;
ctx
}
pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
for bp in breakpoints {
let self_above = self.viewport_width >= *bp;
let other_above = other.viewport_width >= *bp;
if self_above != other_above {
return true;
}
}
false
}
}
impl DynamicSelector {
pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
match self {
Self::Os(os) => Self::match_os(*os, ctx.os),
Self::OsVersion(ver) => Self::match_os_version(ver, &ctx.os_version, &ctx.desktop_env, ctx.de_version),
Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
Self::ContainerWidth(range) => {
!ctx.container_width.is_nan() && range.matches(ctx.container_width)
}
Self::ContainerHeight(range) => {
!ctx.container_height.is_nan() && range.matches(ctx.container_height)
}
Self::ContainerName(name) => ctx.container_name.as_ref() == Some(name),
Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
Self::AspectRatio(range) => {
let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
range.matches(ratio)
}
Self::Orientation(orient) => *orient == ctx.orientation,
Self::PrefersReducedMotion(pref) => {
bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
}
Self::PrefersHighContrast(pref) => {
bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
}
Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
}
}
fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
match condition {
OsCondition::Any => true,
OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
_ => condition == actual,
}
}
fn match_os_version(
condition: &OsVersionCondition,
actual: &OsVersion,
desktop_env: &OptionLinuxDesktopEnv,
de_version: u32,
) -> bool {
let de_matches = |env: &LinuxDesktopEnv| desktop_env.as_ref() == Some(env);
match condition {
OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
OsVersionCondition::Min(ver) => actual.is_at_least(ver),
OsVersionCondition::Max(ver) => actual.is_at_most(ver),
OsVersionCondition::DesktopEnvironment(env) => de_matches(env),
OsVersionCondition::DesktopEnvMin(d) =>
de_matches(&d.env) && de_version != 0 && de_version >= d.version_id,
OsVersionCondition::DesktopEnvMax(d) =>
de_matches(&d.env) && de_version != 0 && de_version <= d.version_id,
OsVersionCondition::DesktopEnvExact(d) =>
de_matches(&d.env) && de_version == d.version_id,
}
}
fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
match (condition, actual) {
(ThemeCondition::SystemPreferred, _) => true,
_ => condition == actual,
}
}
fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
let node_state = &ctx.pseudo_state;
match state {
PseudoStateType::Normal => true, PseudoStateType::Hover => node_state.hover,
PseudoStateType::Active => node_state.active,
PseudoStateType::Focus => node_state.focused,
PseudoStateType::Disabled => node_state.disabled,
PseudoStateType::CheckedTrue => node_state.checked,
PseudoStateType::CheckedFalse => !node_state.checked,
PseudoStateType::FocusWithin => node_state.focus_within,
PseudoStateType::Visited => node_state.visited,
PseudoStateType::Backdrop => node_state.backdrop,
PseudoStateType::Dragging => node_state.dragging,
PseudoStateType::DragOver => node_state.drag_over,
}
}
}
#[cfg(feature = "parser")]
pub fn parse_os_at_rule_content(content: &str) -> Option<Vec<DynamicSelector>> {
let trimmed = content.trim();
let inner = trimmed
.strip_prefix('(').and_then(|s| s.strip_suffix(')'))
.unwrap_or(trimmed)
.trim();
let inner = inner
.strip_prefix('"').and_then(|s| s.strip_suffix('"'))
.or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
.unwrap_or(inner)
.trim();
if inner.is_empty() {
return None;
}
let (subject, op_and_version) = split_op_and_version(inner);
let subject = subject.trim();
let (family_str, de_str) = match subject.split_once(':') {
Some((f, d)) => (f.trim(), Some(d.trim())),
None => (subject, None),
};
let family = parse_os_family_token(family_str)?;
let de = match de_str {
Some(s) if !s.is_empty() => Some(parse_de_token(s)),
_ => None,
};
let mut out = Vec::new();
out.push(DynamicSelector::Os(family));
match (de, op_and_version) {
(Some(env), None) => {
out.push(DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(env)));
}
(Some(env), Some((op, ver_str))) => {
let v: u32 = ver_str.parse().ok()?;
let dev = DesktopEnvVersion { env, version_id: v };
let cond = match op {
VersionOp::Min => OsVersionCondition::DesktopEnvMin(dev),
VersionOp::Max => OsVersionCondition::DesktopEnvMax(dev),
VersionOp::Exact => OsVersionCondition::DesktopEnvExact(dev),
};
out.push(DynamicSelector::OsVersion(cond));
}
(None, Some((op, ver_str))) => {
let os_family = match family {
OsCondition::Linux => OsFamily::Linux,
OsCondition::Windows => OsFamily::Windows,
OsCondition::MacOS => OsFamily::MacOS,
OsCondition::IOS => OsFamily::IOS,
OsCondition::Android => OsFamily::Android,
_ => return None,
};
let version = parse_os_version(os_family, ver_str)?;
let cond = match op {
VersionOp::Min => OsVersionCondition::Min(version),
VersionOp::Max => OsVersionCondition::Max(version),
VersionOp::Exact => OsVersionCondition::Exact(version),
};
out.push(DynamicSelector::OsVersion(cond));
}
(None, None) => {}
}
Some(out)
}
#[cfg(feature = "parser")]
#[derive(Copy, Clone)]
enum VersionOp { Min, Max, Exact }
#[cfg(feature = "parser")]
fn split_op_and_version(s: &str) -> (&str, Option<(VersionOp, &str)>) {
let candidates: &[(&str, VersionOp)] = &[
(">=", VersionOp::Min),
("<=", VersionOp::Max),
("=", VersionOp::Exact),
(">", VersionOp::Min),
("<", VersionOp::Max),
];
let mut best: Option<(usize, usize, VersionOp)> = None;
for (op_str, op) in candidates {
if let Some(pos) = s.find(op_str) {
let len = op_str.len();
best = Some(match best {
None => (pos, len, *op),
Some((bp, bl, _)) if pos < bp || (pos == bp && len > bl) => (pos, len, *op),
Some(b) => b,
});
}
}
match best {
Some((pos, len, op)) => (&s[..pos], Some((op, s[pos + len..].trim()))),
None => (s, None),
}
}
#[cfg(feature = "parser")]
fn parse_os_family_token(s: &str) -> Option<OsCondition> {
match s.to_lowercase().as_str() {
"linux" => Some(OsCondition::Linux),
"windows" | "win" => Some(OsCondition::Windows),
"macos" | "mac" | "osx" => Some(OsCondition::MacOS),
"ios" => Some(OsCondition::IOS),
"android" => Some(OsCondition::Android),
"apple" => Some(OsCondition::Apple),
"web" | "wasm" => Some(OsCondition::Web),
"any" | "all" | "*" => Some(OsCondition::Any),
_ => None,
}
}
#[cfg(feature = "parser")]
fn parse_de_token(s: &str) -> LinuxDesktopEnv {
match s.to_lowercase().as_str() {
"gnome" => LinuxDesktopEnv::Gnome,
"kde" => LinuxDesktopEnv::KDE,
"xfce" => LinuxDesktopEnv::XFCE,
"unity" => LinuxDesktopEnv::Unity,
"cinnamon" => LinuxDesktopEnv::Cinnamon,
"mate" => LinuxDesktopEnv::MATE,
_ => LinuxDesktopEnv::Other,
}
}
#[repr(C)]
#[derive(Debug, Clone, PartialEq)]
pub struct CssPropertyWithConditions {
pub property: CssProperty,
pub apply_if: DynamicSelectorVec,
}
impl_option!(
CssPropertyWithConditions,
OptionCssPropertyWithConditions,
copy = false,
[Debug, Clone, PartialEq, PartialOrd]
);
impl Eq for CssPropertyWithConditions {}
impl PartialOrd for CssPropertyWithConditions {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CssPropertyWithConditions {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.apply_if
.as_slice()
.len()
.cmp(&other.apply_if.as_slice().len())
}
}
impl CssPropertyWithConditions {
pub const fn simple(property: CssProperty) -> Self {
Self {
property,
apply_if: DynamicSelectorVec::from_const_slice(&[]),
}
}
pub const fn with_single_condition(
property: CssProperty,
conditions: &'static [DynamicSelector],
) -> Self {
Self {
property,
apply_if: DynamicSelectorVec::from_const_slice(conditions),
}
}
pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
Self {
property,
apply_if: DynamicSelectorVec::from_vec(vec![condition]),
}
}
pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
Self {
property,
apply_if: conditions,
}
}
pub const fn on_hover(property: CssProperty) -> Self {
Self::with_single_condition(
property,
&[DynamicSelector::PseudoState(PseudoStateType::Hover)],
)
}
pub const fn on_active(property: CssProperty) -> Self {
Self::with_single_condition(
property,
&[DynamicSelector::PseudoState(PseudoStateType::Active)],
)
}
pub const fn on_focus(property: CssProperty) -> Self {
Self::with_single_condition(
property,
&[DynamicSelector::PseudoState(PseudoStateType::Focus)],
)
}
pub const fn when_disabled(property: CssProperty) -> Self {
Self::with_single_condition(
property,
&[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
)
}
pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
Self::with_condition(property, DynamicSelector::Os(os))
}
pub const fn dark_theme(property: CssProperty) -> Self {
Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
}
pub const fn light_theme(property: CssProperty) -> Self {
Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
}
pub const fn on_windows(property: CssProperty) -> Self {
Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
}
pub const fn on_macos(property: CssProperty) -> Self {
Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
}
pub const fn on_linux(property: CssProperty) -> Self {
Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
}
pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
if self.apply_if.as_slice().is_empty() {
return true;
}
self.apply_if
.as_slice()
.iter()
.all(|selector| selector.matches(ctx))
}
pub fn is_conditional(&self) -> bool {
!self.apply_if.as_slice().is_empty()
}
pub fn is_pseudo_state_only(&self) -> bool {
let conditions = self.apply_if.as_slice();
!conditions.is_empty()
&& conditions
.iter()
.all(|c| matches!(c, DynamicSelector::PseudoState(_)))
}
pub fn is_layout_affecting(&self) -> bool {
self.property.get_type().can_trigger_relayout()
}
}
impl_vec!(CssPropertyWithConditions, CssPropertyWithConditionsVec, CssPropertyWithConditionsVecDestructor, CssPropertyWithConditionsVecDestructorType, CssPropertyWithConditionsVecSlice, OptionCssPropertyWithConditions);
impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
impl_vec_clone!(
CssPropertyWithConditions,
CssPropertyWithConditionsVec,
CssPropertyWithConditionsVecDestructor
);
impl Eq for CssPropertyWithConditionsVec {}
impl Ord for CssPropertyWithConditionsVec {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.as_slice().len().cmp(&other.as_slice().len())
}
}
impl core::hash::Hash for CssPropertyWithConditions {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.property.hash(state);
self.apply_if.as_slice().len().hash(state);
}
}
impl core::hash::Hash for CssPropertyWithConditionsVec {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
for item in self.as_slice() {
item.hash(state);
}
}
}
impl CssPropertyWithConditionsVec {
#[cfg(feature = "parser")]
pub fn parse(style: &str) -> Self {
Self::parse_with_conditions(style, Vec::new())
}
#[cfg(feature = "parser")]
fn parse_with_conditions(style: &str, inherited_conditions: Vec<DynamicSelector>) -> Self {
use crate::props::property::{
parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
CssPropertyType,
};
let mut props = Vec::new();
let key_map = CssKeyMap::get();
let style = style.trim();
if style.is_empty() {
return CssPropertyWithConditionsVec::from_vec(props);
}
let mut chars = style.chars().peekable();
let mut current_segment = String::new();
let mut brace_depth = 0;
for c in chars {
match c {
'{' => {
brace_depth += 1;
current_segment.push(c);
}
'}' => {
brace_depth -= 1;
current_segment.push(c);
if brace_depth == 0 {
let segment = current_segment.trim().to_string();
current_segment.clear();
if let Some(parsed) = Self::parse_block_segment(&segment, &inherited_conditions, &key_map) {
props.extend(parsed);
}
}
}
';' if brace_depth == 0 => {
let segment = current_segment.trim().to_string();
current_segment.clear();
if !segment.is_empty() {
if let Some(parsed) = Self::parse_property_segment(&segment, &inherited_conditions, &key_map) {
props.extend(parsed);
}
}
}
_ => {
current_segment.push(c);
}
}
}
let remaining = current_segment.trim();
if !remaining.is_empty() && !remaining.contains('{') {
if let Some(parsed) = Self::parse_property_segment(remaining, &inherited_conditions, &key_map) {
props.extend(parsed);
}
}
CssPropertyWithConditionsVec::from_vec(props)
}
#[cfg(feature = "parser")]
fn parse_block_segment(
segment: &str,
inherited_conditions: &[DynamicSelector],
key_map: &crate::props::property::CssKeyMap,
) -> Option<Vec<CssPropertyWithConditions>> {
let brace_pos = segment.find('{')?;
let selector = segment[..brace_pos].trim();
let content_start = brace_pos + 1;
let content_end = segment.rfind('}')?;
if content_end <= content_start {
return None;
}
let content = &segment[content_start..content_end];
let mut conditions = inherited_conditions.to_vec();
if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
conditions.extend(new_conditions);
} else {
return None;
}
let parsed = Self::parse_with_conditions(content, conditions);
Some(parsed.into_library_owned_vec())
}
#[cfg(feature = "parser")]
fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
let selector = selector.trim();
if let Some(pseudo) = selector.strip_prefix(':') {
match pseudo {
"hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
"active" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)]),
"focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
"focus-within" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::FocusWithin)]),
"disabled" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Disabled)]),
"checked" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::CheckedTrue)]),
"visited" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)]),
"backdrop" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Backdrop)]),
"dragging" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Dragging)]),
"drag-over" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::DragOver)]),
_ => return None,
}
}
if let Some(rule_content) = selector.strip_prefix('@') {
return Self::parse_at_rule(rule_content);
}
if selector == "*" {
return Some(vec![]);
}
if selector.is_empty() {
return Some(vec![]);
}
None
}
#[cfg(feature = "parser")]
fn parse_at_rule(rule_content: &str) -> Option<Vec<DynamicSelector>> {
if let Some(rest) = rule_content
.strip_prefix("os ")
.or_else(|| if rule_content.starts_with("os(") { Some(&rule_content[2..]) } else { None })
{
if let Some(conds) = parse_os_at_rule_content(rest) {
return Some(conds);
}
}
if rule_content.starts_with("media ") {
let media_query = rule_content[6..].trim();
if let Some(media_conds) = Self::parse_media_query(media_query) {
return Some(media_conds);
}
}
if rule_content.starts_with("theme ") {
let theme = rule_content[6..].trim();
match theme {
"dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
"light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
_ => return None,
}
}
if rule_content.starts_with("lang ") || rule_content.starts_with("lang(") {
let lang_str = if rule_content.starts_with("lang(") {
rule_content[5..].trim_end_matches(')').trim()
} else {
rule_content[5..].trim()
};
let lang_str = lang_str
.strip_prefix('"').and_then(|s| s.strip_suffix('"'))
.or_else(|| lang_str.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
.unwrap_or(lang_str);
if !lang_str.is_empty() {
return Some(vec![DynamicSelector::Language(
LanguageCondition::Prefix(AzString::from(lang_str.to_string()))
)]);
}
}
if rule_content.starts_with("container ") || rule_content.starts_with("container(") {
let container_str = if rule_content.starts_with("container(") {
&rule_content[9..] } else {
rule_content[10..].trim()
};
let mut conds = Vec::new();
let (name_part, query_part) = if container_str.starts_with('(') {
(None, container_str)
} else if let Some(paren_idx) = container_str.find('(') {
let name = container_str[..paren_idx].trim();
if !name.is_empty() {
(Some(name), &container_str[paren_idx..])
} else {
(None, container_str)
}
} else {
if !container_str.is_empty() {
return Some(vec![DynamicSelector::ContainerName(
AzString::from(container_str.to_string())
)]);
}
return None;
};
if let Some(name) = name_part {
conds.push(DynamicSelector::ContainerName(
AzString::from(name.to_string())
));
}
if let Some(inner) = query_part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
if let Some((key, value)) = inner.split_once(':') {
let key = key.trim();
let value = value.trim();
let px_value = value.strip_suffix("px")
.and_then(|v| v.trim().parse::<f32>().ok());
match key {
"min-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_min(px))); } }
"max-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_max(px))); } }
"min-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_min(px))); } }
"max-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_max(px))); } }
_ => {}
}
}
}
if !conds.is_empty() {
return Some(conds);
}
}
if rule_content == "prefers-reduced-motion" || rule_content == "reduced-motion" {
return Some(vec![DynamicSelector::PrefersReducedMotion(BoolCondition::True)]);
}
if rule_content == "prefers-high-contrast" || rule_content == "high-contrast" {
return Some(vec![DynamicSelector::PrefersHighContrast(BoolCondition::True)]);
}
None
}
#[cfg(feature = "parser")]
fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
let query = query.trim();
if query.starts_with('(') && query.ends_with(')') {
let inner = &query[1..query.len()-1];
if let Some((key, value)) = inner.split_once(':') {
let key = key.trim();
let value = value.trim();
let px_value = value.strip_suffix("px")
.and_then(|v| v.trim().parse::<f32>().ok());
match key {
"min-width" => {
if let Some(px) = px_value {
return Some(vec![DynamicSelector::ViewportWidth(
MinMaxRange::with_min(px)
)]);
}
}
"max-width" => {
if let Some(px) = px_value {
return Some(vec![DynamicSelector::ViewportWidth(
MinMaxRange::with_max(px)
)]);
}
}
"min-height" => {
if let Some(px) = px_value {
return Some(vec![DynamicSelector::ViewportHeight(
MinMaxRange::with_min(px)
)]);
}
}
"max-height" => {
if let Some(px) = px_value {
return Some(vec![DynamicSelector::ViewportHeight(
MinMaxRange::with_max(px)
)]);
}
}
other => {
if let Some(sel) = Self::parse_media_feature_inline(other, value) {
return Some(vec![sel]);
}
}
}
}
}
match query {
"screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
"print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
"all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
_ => None,
}
}
#[cfg(feature = "parser")]
fn parse_media_feature_inline(key: &str, value: &str) -> Option<DynamicSelector> {
match key {
"orientation" => {
if value.eq_ignore_ascii_case("portrait") {
Some(DynamicSelector::Orientation(OrientationType::Portrait))
} else if value.eq_ignore_ascii_case("landscape") {
Some(DynamicSelector::Orientation(OrientationType::Landscape))
} else {
None
}
}
"prefers-color-scheme" => {
if value.eq_ignore_ascii_case("dark") {
Some(DynamicSelector::Theme(ThemeCondition::Dark))
} else if value.eq_ignore_ascii_case("light") {
Some(DynamicSelector::Theme(ThemeCondition::Light))
} else {
None
}
}
"prefers-reduced-motion" => {
if value.eq_ignore_ascii_case("reduce") {
Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
} else if value.eq_ignore_ascii_case("no-preference") {
Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
} else {
None
}
}
"prefers-contrast" | "prefers-high-contrast" => {
if value.eq_ignore_ascii_case("more") || value.eq_ignore_ascii_case("high") || value.eq_ignore_ascii_case("active") {
Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
} else if value.eq_ignore_ascii_case("no-preference") || value.eq_ignore_ascii_case("none") {
Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
} else {
None
}
}
_ => None,
}
}
#[cfg(feature = "parser")]
fn parse_property_segment(
segment: &str,
inherited_conditions: &[DynamicSelector],
key_map: &crate::props::property::CssKeyMap,
) -> Option<Vec<CssPropertyWithConditions>> {
use crate::props::property::{
parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
CssPropertyType,
};
let segment = segment.trim();
if segment.is_empty() {
return None;
}
let (key, value) = segment.split_once(':')?;
let key = key.trim();
let value = value.trim();
let mut props = Vec::new();
let conditions = if inherited_conditions.is_empty() {
DynamicSelectorVec::from_const_slice(&[])
} else {
DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
};
if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
if let Ok(prop) = parse_css_property(prop_type, value) {
props.push(CssPropertyWithConditions {
property: prop,
apply_if: conditions.clone(),
});
return Some(props);
}
}
if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
for prop in expanded_props {
props.push(CssPropertyWithConditions {
property: prop,
apply_if: conditions.clone(),
});
}
return Some(props);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inline_overflow_parse() {
let style = "overflow: scroll;";
let parsed = CssPropertyWithConditionsVec::parse(style);
let props = parsed.into_library_owned_vec();
assert!(props.len() > 0, "Expected overflow to parse into at least 1 property");
}
#[test]
fn test_inline_overflow_y_parse() {
let style = "overflow-y: scroll;";
let parsed = CssPropertyWithConditionsVec::parse(style);
let props = parsed.into_library_owned_vec();
assert!(props.len() > 0, "Expected overflow-y to parse into at least 1 property");
}
#[test]
fn test_inline_combined_style_with_overflow() {
let style = "padding: 20px; background-color: #f0f0f0; font-size: 14px; color: #222;overflow: scroll;";
let parsed = CssPropertyWithConditionsVec::parse(style);
let props = parsed.into_library_owned_vec();
assert!(props.len() >= 9, "Expected at least 9 properties, got {}", props.len());
}
#[test]
fn test_inline_grid_template_columns_parse() {
use crate::props::layout::grid::GridTrackSizing;
let style = "display: grid; grid-template-columns: repeat(4, 160px); gap: 16px; padding: 10px;";
let parsed = CssPropertyWithConditionsVec::parse(style);
let props = parsed.into_library_owned_vec();
let grid_cols = props.iter().find(|p| {
matches!(p.property, CssProperty::GridTemplateColumns(_))
}).expect("Expected GridTemplateColumns property");
if let CssProperty::GridTemplateColumns(ref value) = grid_cols.property {
let template = value.get_property().expect("Expected Exact value");
let tracks = template.tracks.as_ref();
assert_eq!(tracks.len(), 4, "Expected 4 tracks");
for (i, track) in tracks.iter().enumerate() {
assert!(matches!(track, GridTrackSizing::Fixed(_)),
"Track {} should be Fixed(160px), got {:?}", i, track);
}
} else {
panic!("Expected CssProperty::GridTemplateColumns");
}
}
}