Files
Glint-Runtime/src/interpreter/style.rs
2026-07-08 23:45:37 +03:00

990 lines
34 KiB
Rust

use std::collections::HashMap;
use std::borrow::Cow;
#[derive(Debug, Clone)]
pub enum AttributeSelector {
Exists(String),
Equals(String, String),
}
fn parse_attribute(input: &str) -> AttributeSelector {
let input = input.trim();
if let Some((name, val)) = input.split_once('=') {
let name = name.trim().to_string();
let val = val.trim().trim_matches('"').trim_matches('\'').to_string();
AttributeSelector::Equals(name, val)
} else {
AttributeSelector::Exists(input.to_string())
}
}
#[derive(Debug, Clone)]
pub struct CompoundSelector {
pub tag: Option<String>,
pub id: Option<String>,
pub classes: Vec<String>,
pub pseudo_classes: Vec<String>,
pub attributes: Vec<AttributeSelector>,
}
impl CompoundSelector {
fn parse(input: &str) -> Self {
let input = input.trim();
let mut tag = None;
let mut id = None;
let mut classes = Vec::new();
let mut pseudo_classes = Vec::new();
let mut attributes = Vec::new();
let mut current = String::new();
let mut delim: Option<char> = None;
for ch in input.chars() {
match ch {
'#' | '.' | ':' | '[' => {
if !current.is_empty() {
match (delim, ch) {
(Some('.'), _) => classes.push(std::mem::take(&mut current)),
(Some(':'), _) => pseudo_classes.push(std::mem::take(&mut current)),
(Some('#'), _) => id = Some(std::mem::take(&mut current)),
(Some('['), _) => {
attributes.push(parse_attribute(&std::mem::take(&mut current)));
}
_ if tag.is_none() && id.is_none()
&& classes.is_empty() && pseudo_classes.is_empty()
&& attributes.is_empty() =>
{
tag = Some(std::mem::take(&mut current));
}
_ => current.clear(),
}
}
delim = Some(ch);
}
']' => {
if delim == Some('[') && !current.is_empty() {
attributes.push(parse_attribute(&std::mem::take(&mut current)));
delim = None;
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
match delim {
Some('#') => id = Some(current),
Some('.') => classes.push(current),
Some(':') => pseudo_classes.push(current),
Some('[') => attributes.push(parse_attribute(&current)),
None => tag = Some(current),
_ => {}
}
}
Self { tag, id, classes, pseudo_classes, attributes }
}
fn specificity(&self) -> (u32, u32, u32) {
(
if self.id.is_some() { 1 } else { 0 },
self.classes.len() as u32 + self.attributes.len() as u32 + self.pseudo_classes.len() as u32,
if self.tag.is_some() { 1 } else { 0 },
)
}
pub fn matches_element(
&self,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
el_attributes: &HashMap<String, String>,
) -> bool {
if let Some(ref t) = self.tag {
if t != type_name && t != "*" {
return false;
}
}
if let Some(ref i) = self.id {
match el_id {
Some(eid) if eid == i => {}
_ => return false,
}
}
for cls in &self.classes {
if !el_classes.contains(&cls.as_str()) {
return false;
}
}
for attr in &self.attributes {
match attr {
AttributeSelector::Exists(name) => {
if !el_attributes.contains_key(name) {
return false;
}
}
AttributeSelector::Equals(name, val) => {
match el_attributes.get(name) {
Some(v) if v == val => {}
_ => return false,
}
}
}
}
for pc in &self.pseudo_classes {
match pc.as_str() {
"first-child" => {
if structural.sibling_index != 0 { return false; }
}
"last-child" => {
if structural.sibling_index + 1 != structural.sibling_total { return false; }
}
"first-of-type" => {
if structural.type_index != 0 { return false; }
}
"empty" => {
if structural.has_children { return false; }
}
"root" => {
if !structural.is_root { return false; }
}
s if s.starts_with("nth-child(") => {
let inner = &s[10..s.len().saturating_sub(1)];
if !nth_matches(inner, structural.sibling_index + 1) {
return false;
}
}
_ => {
if !active_pseudo.contains(&pc.as_str()) {
return false;
}
}
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Combinator {
Descendant, // Space
Child, // >
NextSibling, // +
Subsequent, // ~
}
#[derive(Debug, Clone)]
pub struct ComplexSelector {
pub compounds: Vec<CompoundSelector>,
pub combinators: Vec<Combinator>,
}
impl ComplexSelector {
pub fn parse(input: &str) -> Self {
let input = input.trim();
if input.is_empty() {
return Self { compounds: vec![], combinators: vec![] };
}
let mut parts: Vec<(String, Combinator)> = Vec::new();
let mut buf = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'>' | '+' | '~' => {
let right = buf.trim().to_string();
let combinator = match ch {
'>' => Combinator::Child,
'+' => Combinator::NextSibling,
'~' => Combinator::Subsequent,
_ => unreachable!(),
};
if !right.is_empty() {
parts.push((right, combinator));
}
buf.clear();
while let Some(&c) = chars.peek() {
if c.is_ascii_whitespace() { chars.next(); } else { break; }
}
}
'#' | '.' | ':' => {
buf.push(ch);
}
c if c.is_ascii_whitespace() => {
let candidate = buf.trim().to_string();
if !candidate.is_empty() {
let mut peek_pos = chars.clone();
let next_nonws = peek_pos.find(|c| !c.is_ascii_whitespace());
match next_nonws {
Some('>') | Some('+') | Some('~') => {
buf.push(' ');
}
_ => {
parts.push((candidate, Combinator::Descendant));
buf.clear();
}
}
}
}
_ => buf.push(ch),
}
}
let last = buf.trim().to_string();
if !last.is_empty() {
if parts.is_empty() {
parts.push((last, Combinator::Descendant));
} else {
parts.push((last, Combinator::Descendant));
}
}
let compounds: Vec<CompoundSelector> = parts.iter().map(|(s, _)| CompoundSelector::parse(s)).collect();
let combinators: Vec<Combinator> = parts.iter().rev().skip(1).rev().map(|(_, c)| *c).collect();
Self { compounds, combinators }
}
fn check_compound_against(&self, i: usize, info: &AncestorInfo) -> bool {
let compound = &self.compounds[i];
let tag_ok = compound.tag.as_ref().map_or(true, |t| {
t == &info.type_name || t == "*"
});
let id_ok = compound.id.as_ref().map_or(true, |id| {
info.id.as_ref().map_or(false, |sid| sid == id)
});
let classes_ok = compound.classes.iter()
.all(|c| info.classes.contains(c));
tag_ok && id_ok && classes_ok
}
pub fn matches(
&self,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> bool {
if self.compounds.is_empty() {
return false;
}
let target = self.compounds.last().unwrap();
if !target.matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) {
return false;
}
if self.compounds.len() == 1 {
return true;
}
let mut ai = 0;
let mut si = preceding_siblings.len();
for i in (0..self.compounds.len() - 1).rev() {
let combinator = self.combinators[i];
match combinator {
Combinator::Descendant => {
let mut found = false;
for a in &ancestors[ai..] {
if self.check_compound_against(i, a) {
found = true;
break;
}
}
if !found { return false; }
}
Combinator::Child => {
if ai >= ancestors.len() { return false; }
let a = &ancestors[ai];
if !self.check_compound_against(i, a) { return false; }
ai += 1;
}
Combinator::NextSibling => {
if si == 0 { return false; }
let sib = &preceding_siblings[si - 1];
if !self.check_compound_against(i, sib) { return false; }
si -= 1;
}
Combinator::Subsequent => {
let mut found = false;
for sib in preceding_siblings[..si].iter().rev() {
if self.check_compound_against(i, sib) {
found = true;
break;
}
}
if !found { return false; }
}
}
}
true
}
pub fn specificity(&self) -> (u32, u32, u32) {
let mut a = 0u32;
let mut b = 0u32;
let mut c = 0u32;
for compound in &self.compounds {
let (sa, sb, sc) = compound.specificity();
a += sa;
b += sb;
c += sc;
}
(a, b, c)
}
pub fn as_simple(&self) -> Option<&CompoundSelector> {
if self.compounds.len() == 1 {
self.compounds.first()
} else {
None
}
}
pub fn has_pseudo_class(&self, pc: &str) -> bool {
self.compounds.iter().any(|c| c.pseudo_classes.iter().any(|p| p == pc))
}
}
#[derive(Debug, Clone)]
pub struct AncestorInfo {
pub type_name: String,
pub id: Option<String>,
pub classes: Vec<String>,
}
impl AncestorInfo {
pub fn new(type_name: &str, classes: Vec<String>) -> Self {
Self { type_name: type_name.to_string(), id: None, classes }
}
pub fn new_with_id(type_name: &str, id: Option<String>, classes: Vec<String>) -> Self {
Self { type_name: type_name.to_string(), id, classes }
}
}
#[derive(Debug, Clone, Default)]
pub struct StructuralContext {
pub sibling_index: usize,
pub sibling_total: usize,
pub type_index: usize,
pub type_total: usize,
pub has_children: bool,
pub is_root: bool,
}
fn nth_matches(expr: &str, n: usize) -> bool {
let expr = expr.trim();
if expr.eq_ignore_ascii_case("odd") {
return n % 2 == 1;
}
if expr.eq_ignore_ascii_case("even") {
return n % 2 == 0;
}
if let Ok(num) = expr.parse::<i32>() {
return n == num as usize;
}
let expr_lower = expr.to_lowercase();
if let Some(n_pos) = expr_lower.find('n') {
let a_str = expr_lower[..n_pos].trim();
let b_str = expr_lower[n_pos + 1..].trim();
let a = if a_str.is_empty() || a_str == "+" {
1
} else if a_str == "-" {
-1
} else {
a_str.parse::<i32>().unwrap_or(0)
};
let b = if b_str.is_empty() {
0
} else if b_str.starts_with('+') {
b_str[1..].trim().parse::<i32>().unwrap_or(0)
} else if b_str.starts_with('-') {
b_str.parse::<i32>().unwrap_or(0)
} else {
b_str.parse::<i32>().unwrap_or(0)
};
if a == 0 {
return n == b as usize;
}
let n_i32 = n as i32;
if a > 0 {
let k = n_i32 - b;
k >= 0 && k % a == 0
} else {
let diff = b - n_i32;
diff >= 0 && diff % a.abs() == 0
}
} else {
false
}
}
#[derive(Debug, Clone)]
pub struct StyleRule {
pub selector: ComplexSelector,
pub properties: HashMap<String, String>,
}
impl StyleRule {
pub fn build(selector_str: String, properties: HashMap<String, String>) -> Self {
Self {
selector: ComplexSelector::parse(&selector_str),
properties,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StyleSheet {
rules: Vec<StyleRule>,
}
impl StyleSheet {
pub fn new() -> Self {
Self::default()
}
pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>) {
for part in split_selectors(&selector) {
self.rules.push(StyleRule::build(part, properties.clone()));
}
}
pub fn matching_rules<'a>(
&'a self,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> Vec<&'a HashMap<String, String>> {
let mut matched: Vec<(usize, &StyleRule)> = self
.rules
.iter()
.enumerate()
.filter(|(_, rule)| {
rule.selector
.matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes)
})
.collect();
matched.sort_by(|(i, a), (j, b)| {
a.selector
.specificity()
.cmp(&b.selector.specificity())
.then_with(|| i.cmp(j))
});
matched.into_iter().map(|(_, rule)| &rule.properties).collect()
}
pub fn matching_pseudo_rules(
&self,
pseudo: &str,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut props = HashMap::new();
for rule in &self.rules {
if !rule.selector.has_pseudo_class(pseudo) {
continue;
}
if !rule.selector.matches(type_name, el_id, el_classes, &[pseudo], structural, ancestors, preceding_siblings, el_attributes) {
continue;
}
for (k, v) in &rule.properties {
props.insert(k.clone(), v.clone());
}
}
props
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
fn split_selectors(input: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0u32;
let mut start = 0usize;
for (i, ch) in input.char_indices() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
let part = input[start..i].trim();
if !part.is_empty() {
parts.push(part.to_string());
}
start = i + 1;
}
_ => {}
}
}
let last = input[start..].trim();
if !last.is_empty() {
parts.push(last.to_string());
}
parts
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Overflow {
#[default]
Visible,
Hidden,
Scroll,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Position {
#[default]
Static,
Relative,
Absolute,
Sticky,
Fixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Display {
#[default]
Block,
Flex,
Grid,
Inline,
None,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ComputedStyle {
pub font_size: Option<f32>,
pub color: Option<iced::Color>,
pub padding: Option<f32>,
pub padding_top: Option<f32>,
pub padding_right: Option<f32>,
pub padding_bottom:Option<f32>,
pub padding_left: Option<f32>,
pub margin: Option<f32>,
pub margin_top: Option<f32>,
pub margin_right: Option<f32>,
pub margin_bottom: Option<f32>,
pub margin_left: Option<f32>,
pub background: Option<iced::Color>,
pub spacing: Option<f32>,
pub border_radius: Option<f32>,
pub border_width: Option<f32>,
pub border_color: Option<iced::Color>,
pub width: Option<iced::Length>,
pub height: Option<iced::Length>,
pub min_width: Option<f32>,
pub max_width: Option<f32>,
pub min_height: Option<f32>,
pub max_height: Option<f32>,
pub direction: Option<LayoutDirection>,
pub align_items: Option<iced::Alignment>,
pub content_align: Option<ContentAlign>,
pub flex_grow: Option<u16>,
pub position: Option<Position>,
pub top: Option<f32>,
pub right: Option<f32>,
pub bottom: Option<f32>,
pub left: Option<f32>,
pub overflow_x: Option<Overflow>,
pub overflow_y: Option<Overflow>,
pub display: Option<Display>,
pub opacity: Option<f32>,
pub font_weight: Option<u16>,
pub line_height: Option<f32>,
pub text_align: Option<TextAlign>,
}
impl ComputedStyle {
pub fn compute(
inline: &[(Cow<'_, str>, Cow<'_, str>)],
matched_sheets: &[&HashMap<String, String>],
) -> Self {
let overflow = lookup("overflow", inline, matched_sheets).and_then(parse_overflow);
Self {
font_size: lookup("font-size", inline, matched_sheets).and_then(parse_size),
color: lookup("color", inline, matched_sheets).and_then(parse_color),
padding: lookup("padding", inline, matched_sheets).and_then(parse_size),
padding_top: lookup("padding-top", inline, matched_sheets).and_then(parse_size),
padding_right: lookup("padding-right", inline, matched_sheets).and_then(parse_size),
padding_bottom: lookup("padding-bottom", inline, matched_sheets).and_then(parse_size),
padding_left: lookup("padding-left", inline, matched_sheets).and_then(parse_size),
margin: lookup("margin", inline, matched_sheets).and_then(parse_size),
margin_top: lookup("margin-top", inline, matched_sheets).and_then(parse_size),
margin_right: lookup("margin-right", inline, matched_sheets).and_then(parse_size),
margin_bottom: lookup("margin-bottom", inline, matched_sheets).and_then(parse_size),
margin_left: lookup("margin-left", inline, matched_sheets).and_then(parse_size),
background: lookup("background", inline, matched_sheets)
.or_else(|| lookup("background-color", inline, matched_sheets))
.and_then(parse_color),
spacing: lookup("spacing", inline, matched_sheets)
.or_else(|| lookup("gap", inline, matched_sheets))
.and_then(parse_size),
border_radius: lookup("border-radius", inline, matched_sheets).and_then(parse_size),
border_width: lookup("border-width", inline, matched_sheets).and_then(parse_size),
border_color: lookup("border-color", inline, matched_sheets).and_then(parse_color),
width: lookup("width", inline, matched_sheets).and_then(parse_length),
height: lookup("height", inline, matched_sheets).and_then(parse_length),
min_width: lookup("min-width", inline, matched_sheets).and_then(parse_size),
max_width: lookup("max-width", inline, matched_sheets).and_then(parse_size),
min_height: lookup("min-height", inline, matched_sheets).and_then(parse_size),
max_height: lookup("max-height", inline, matched_sheets).and_then(parse_size),
direction: lookup("direction", inline, matched_sheets).and_then(parse_direction),
align_items: lookup("align-items", inline, matched_sheets).and_then(parse_alignment),
content_align: lookup("content-align", inline, matched_sheets).and_then(parse_content_align),
flex_grow: lookup("flex-grow", inline, matched_sheets)
.and_then(|s| s.trim().parse::<f32>().ok())
.map(|v| v as u16),
position: lookup("position", inline, matched_sheets).and_then(parse_position),
top: lookup("top", inline, matched_sheets).and_then(parse_size),
right: lookup("right", inline, matched_sheets).and_then(parse_size),
bottom: lookup("bottom", inline, matched_sheets).and_then(parse_size),
left: lookup("left", inline, matched_sheets).and_then(parse_size),
overflow_x: lookup("overflow-x", inline, matched_sheets)
.and_then(parse_overflow)
.or(overflow),
overflow_y: lookup("overflow-y", inline, matched_sheets)
.and_then(parse_overflow)
.or(overflow),
display: lookup("display", inline, matched_sheets).and_then(parse_display),
opacity: lookup("opacity", inline, matched_sheets).and_then(parse_opacity),
font_weight: lookup("font-weight", inline, matched_sheets).and_then(parse_font_weight),
line_height: lookup("line-height", inline, matched_sheets).and_then(parse_size),
text_align: lookup("text-align", inline, matched_sheets).and_then(parse_text_align),
}
}
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>) {
struct Override<'a>(&'a HashMap<String, String>);
impl<'a> Override<'a> {
fn get(&self, key: &str) -> Option<&'a str> { self.0.get(key).map(|s| s.as_str()) }
}
let ov = Override(sheet);
if let Some(v) = ov.get("font-size") { self.font_size = parse_size(v); }
if let Some(v) = ov.get("color") { self.color = parse_color(v); }
if let Some(v) = ov.get("padding") { self.padding = parse_size(v); }
if let Some(v) = ov.get("padding-top") { self.padding_top = parse_size(v); }
if let Some(v) = ov.get("padding-right") { self.padding_right = parse_size(v); }
if let Some(v) = ov.get("padding-bottom") { self.padding_bottom = parse_size(v); }
if let Some(v) = ov.get("padding-left") { self.padding_left = parse_size(v); }
if let Some(v) = ov.get("margin") { self.margin = parse_size(v); }
if let Some(v) = ov.get("margin-top") { self.margin_top = parse_size(v); }
if let Some(v) = ov.get("margin-right") { self.margin_right = parse_size(v); }
if let Some(v) = ov.get("margin-bottom") { self.margin_bottom = parse_size(v); }
if let Some(v) = ov.get("margin-left") { self.margin_left = parse_size(v); }
if let Some(v) = ov.get("background").or_else(|| ov.get("background-color")) {
self.background = parse_color(v);
}
if let Some(v) = ov.get("spacing").or_else(|| ov.get("gap")) {
self.spacing = parse_size(v);
}
if let Some(v) = ov.get("border-radius") { self.border_radius = parse_size(v); }
if let Some(v) = ov.get("border-width") { self.border_width = parse_size(v); }
if let Some(v) = ov.get("border-color") { self.border_color = parse_color(v); }
if let Some(v) = ov.get("width") { self.width = parse_length(v); }
if let Some(v) = ov.get("height") { self.height = parse_length(v); }
if let Some(v) = ov.get("direction") { self.direction = parse_direction(v); }
if let Some(v) = ov.get("align-items") { self.align_items = parse_alignment(v); }
if let Some(v) = ov.get("content-align") { self.content_align = parse_content_align(v); }
if let Some(v) = ov.get("flex-grow") { self.flex_grow = v.trim().parse::<f32>().ok().map(|x| x as u16); }
if let Some(v) = ov.get("position") { self.position = parse_position(v); }
if let Some(v) = ov.get("top") { self.top = parse_size(v); }
if let Some(v) = ov.get("right") { self.right = parse_size(v); }
if let Some(v) = ov.get("bottom") { self.bottom = parse_size(v); }
if let Some(v) = ov.get("left") { self.left = parse_size(v); }
if let Some(v) = ov.get("overflow") { let o = parse_overflow(v);
if o.is_some() { self.overflow_x = o; self.overflow_y = o; } }
if let Some(v) = ov.get("overflow-x") { self.overflow_x = parse_overflow(v); }
if let Some(v) = ov.get("overflow-y") { self.overflow_y = parse_overflow(v); }
if let Some(v) = ov.get("display") { self.display = parse_display(v); }
if let Some(v) = ov.get("opacity") { self.opacity = parse_opacity(v); }
if let Some(v) = ov.get("font-weight") { self.font_weight = parse_font_weight(v); }
if let Some(v) = ov.get("line-height") { self.line_height = parse_size(v); }
if let Some(v) = ov.get("text-align") { self.text_align = parse_text_align(v); }
}
}
#[inline]
fn lookup<'a>(
key: &str,
inline: &'a [(Cow<'_, str>, Cow<'_, str>)],
matched_sheets: &[&'a HashMap<String, String>],
) -> Option<&'a str> {
for (k, v) in inline {
if **k == *key { return Some(v.as_ref()); }
if k.len() == key.len() + 6
&& k.starts_with("style:")
&& &k[6..] == key
{
return Some(v.as_ref());
}
}
for sheet in matched_sheets.iter().rev() {
if let Some(v) = sheet.get(key) {
return Some(v.as_str());
}
}
None
}
pub fn parse_overflow(s: &str) -> Option<Overflow> {
let s = s.trim();
if s.eq_ignore_ascii_case("visible") { Some(Overflow::Visible) }
else if s.eq_ignore_ascii_case("hidden") { Some(Overflow::Hidden) }
else if s.eq_ignore_ascii_case("scroll") { Some(Overflow::Scroll) }
else if s.eq_ignore_ascii_case("auto") { Some(Overflow::Auto) }
else { None }
}
pub fn parse_position(s: &str) -> Option<Position> {
let s = s.trim();
if s.eq_ignore_ascii_case("fixed") { Some(Position::Fixed) }
else if s.eq_ignore_ascii_case("sticky") { Some(Position::Sticky) }
else if s.eq_ignore_ascii_case("absolute") { Some(Position::Absolute) }
else if s.eq_ignore_ascii_case("relative") { Some(Position::Relative) }
else if s.eq_ignore_ascii_case("static") { Some(Position::Static) }
else { None }
}
pub fn parse_length(s: &str) -> Option<iced::Length> {
let s = s.trim();
if s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("100%") ||
s.eq_ignore_ascii_case("100vw") || s.eq_ignore_ascii_case("100vh") ||
s.eq_ignore_ascii_case("stretch") {
return Some(iced::Length::Fill);
}
if s.eq_ignore_ascii_case("shrink") || s.eq_ignore_ascii_case("fit-content") || s.eq_ignore_ascii_case("auto") {
return Some(iced::Length::Shrink);
}
let val = s.trim_end_matches(|c: char| c.is_alphabetic() || c == '%').parse::<f32>().ok()?;
if s.ends_with('%') {
if val >= 100.0 {
Some(iced::Length::Fill)
} else {
Some(iced::Length::FillPortion(val as u16))
}
} else {
Some(iced::Length::Fixed(val))
}
}
pub fn parse_color(s: &str) -> Option<iced::Color> {
let s = s.trim();
if let Some(hex) = s.strip_prefix('#') {
return match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
Some(iced::Color::from_rgb(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
))
}
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(iced::Color::from_rgb(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
))
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
Some(iced::Color::from_rgba(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
))
}
_ => None,
};
}
match s {
"white" => Some(iced::Color::WHITE),
"black" => Some(iced::Color::BLACK),
"transparent" => Some(iced::Color::TRANSPARENT),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutDirection {
Column,
Row,
Grid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentAlign {
Start,
Center,
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
}
impl From<TextAlign> for iced::alignment::Horizontal {
fn from(ta: TextAlign) -> Self {
match ta {
TextAlign::Left => iced::alignment::Horizontal::Left,
TextAlign::Center => iced::alignment::Horizontal::Center,
TextAlign::Right => iced::alignment::Horizontal::Right,
}
}
}
pub fn parse_direction(s: &str) -> Option<LayoutDirection> {
let s = s.trim();
if s.eq_ignore_ascii_case("horizontal") || s.eq_ignore_ascii_case("row") { Some(LayoutDirection::Row) }
else if s.eq_ignore_ascii_case("vertical") || s.eq_ignore_ascii_case("column") { Some(LayoutDirection::Column) }
else if s.eq_ignore_ascii_case("grid") { Some(LayoutDirection::Grid) }
else { None }
}
pub fn parse_alignment(s: &str) -> Option<iced::Alignment> {
let s = s.trim();
if s.eq_ignore_ascii_case("start") { Some(iced::Alignment::Start) }
else if s.eq_ignore_ascii_case("center") { Some(iced::Alignment::Center) }
else if s.eq_ignore_ascii_case("end") { Some(iced::Alignment::End) }
else { None }
}
pub fn parse_content_align(s: &str) -> Option<ContentAlign> {
let s = s.trim();
if s.eq_ignore_ascii_case("start") || s.eq_ignore_ascii_case("left") || s.eq_ignore_ascii_case("top") { Some(ContentAlign::Start) }
else if s.eq_ignore_ascii_case("center") { Some(ContentAlign::Center) }
else if s.eq_ignore_ascii_case("end") || s.eq_ignore_ascii_case("right") || s.eq_ignore_ascii_case("bottom") { Some(ContentAlign::End) }
else { None }
}
pub fn parse_display(s: &str) -> Option<Display> {
let s = s.trim();
if s.eq_ignore_ascii_case("none") { Some(Display::None) }
else if s.eq_ignore_ascii_case("block") { Some(Display::Block) }
else if s.eq_ignore_ascii_case("flex") { Some(Display::Flex) }
else if s.eq_ignore_ascii_case("grid") { Some(Display::Grid) }
else if s.eq_ignore_ascii_case("inline") { Some(Display::Inline) }
else { None }
}
pub fn parse_opacity(s: &str) -> Option<f32> {
let v = s.trim().parse::<f32>().ok()?;
Some(v.clamp(0.0, 1.0))
}
pub fn parse_font_weight(s: &str) -> Option<u16> {
let s = s.trim();
match s {
"normal" => Some(400),
"bold" => Some(700),
"lighter" => Some(300),
"bolder" => Some(900),
_ => s.parse::<f32>().ok().map(|v| v as u16),
}
}
pub fn parse_text_align(s: &str) -> Option<TextAlign> {
let s = s.trim();
if s.eq_ignore_ascii_case("left") { Some(TextAlign::Left) }
else if s.eq_ignore_ascii_case("center") { Some(TextAlign::Center) }
else if s.eq_ignore_ascii_case("right") { Some(TextAlign::Right) }
else { None }
}
pub fn parse_size(s: &str) -> Option<f32> {
let s = s.trim();
if s.ends_with('%') || s.eq_ignore_ascii_case("auto") ||
s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("stretch") {
return None;
}
if s.eq_ignore_ascii_case("vw") || s.eq_ignore_ascii_case("vh") {
return None;
}
s.trim_end_matches(|c: char| c.is_alphabetic())
.parse::<f32>()
.ok()
}