use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct StyleProperties {
pub background: Option<Background>,
pub color: Option<Color>,
pub border: Option<Border>,
pub shadow: Option<Shadow>,
pub opacity: Option<f32>,
pub transform: Option<Transform>,
}
impl StyleProperties {
pub fn validate(&self) -> Result<(), String> {
if let Some(opacity) = self.opacity
&& !(0.0..=1.0).contains(&opacity)
{
return Err(format!("opacity must be 0.0-1.0, got {}", opacity));
}
if let Some(ref color) = self.color {
color.validate()?;
}
if let Some(ref background) = self.background {
background.validate()?;
}
if let Some(ref border) = self.border {
border.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Background {
Color(Color),
Gradient(Gradient),
Image { path: String, fit: ImageFit },
}
impl Background {
pub fn validate(&self) -> Result<(), String> {
match self {
Background::Color(color) => color.validate(),
Background::Gradient(gradient) => gradient.validate(),
Background::Image { .. } => Ok(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImageFit {
Fill,
Contain,
Cover,
ScaleDown,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub fn parse(s: &str) -> Result<Self, String> {
let css_color =
csscolorparser::parse(s).map_err(|e| format!("Invalid color '{}': {}", s, e))?;
let [r, g, b, a] = css_color.to_array();
Ok(Color {
r: r as f32,
g: g as f32,
b: b as f32,
a: a as f32,
})
}
pub fn from_hex(s: &str) -> Result<Self, String> {
let s = s.trim();
if !s.starts_with('#') {
return Err(format!("Invalid hex color '{}': must start with '#'", s));
}
let hex = &s[1..];
let (r, g, b, a) = match hex.len() {
3 => {
let r = u8::from_str_radix(&format!("{}{}", &hex[0..1], &hex[0..1]), 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
let g = u8::from_str_radix(&format!("{}{}", &hex[1..2], &hex[1..2]), 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
let b = u8::from_str_radix(&format!("{}{}", &hex[2..3], &hex[2..3]), 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
(r, g, b, 255)
}
6 => {
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
(r, g, b, 255)
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
let a = u8::from_str_radix(&hex[6..8], 16)
.map_err(|_| format!("Invalid hex color '{}'", s))?;
(r, g, b, a)
}
_ => {
return Err(format!(
"Invalid hex color '{}': expected 3, 6, or 8 hex digits",
s
));
}
};
Ok(Color {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: a as f32 / 255.0,
})
}
pub fn to_hex(&self) -> String {
let r = (self.r.clamp(0.0, 1.0) * 255.0) as u8;
let g = (self.g.clamp(0.0, 1.0) * 255.0) as u8;
let b = (self.b.clamp(0.0, 1.0) * 255.0) as u8;
format!("#{:02x}{:02x}{:02x}", r, g, b)
}
pub fn to_rgba_hex(&self) -> String {
let r = (self.r.clamp(0.0, 1.0) * 255.0) as u8;
let g = (self.g.clamp(0.0, 1.0) * 255.0) as u8;
let b = (self.b.clamp(0.0, 1.0) * 255.0) as u8;
let a = (self.a.clamp(0.0, 1.0) * 255.0) as u8;
format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
}
pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
Self {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: 1.0,
}
}
pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: a as f32 / 255.0,
}
}
pub fn validate(&self) -> Result<(), String> {
if self.r < 0.0 || self.r > 1.0 {
return Err(format!("Red component out of range: {}", self.r));
}
if self.g < 0.0 || self.g > 1.0 {
return Err(format!("Green component out of range: {}", self.g));
}
if self.b < 0.0 || self.b > 1.0 {
return Err(format!("Blue component out of range: {}", self.b));
}
if self.a < 0.0 || self.a > 1.0 {
return Err(format!("Alpha component out of range: {}", self.a));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Gradient {
Linear {
angle: f32,
stops: Vec<ColorStop>,
},
Radial {
shape: RadialShape,
stops: Vec<ColorStop>,
},
}
impl Gradient {
pub fn validate(&self) -> Result<(), String> {
let stops = match self {
Gradient::Linear { angle, stops } => {
if *angle < 0.0 || *angle > 360.0 {
return Err(format!("Gradient angle must be 0.0-360.0, got {}", angle));
}
stops
}
Gradient::Radial { stops, .. } => stops,
};
if stops.len() < 2 {
return Err("Gradient must have at least 2 color stops".to_string());
}
if stops.len() > 8 {
return Err(
"Gradient cannot have more than 8 color stops (Iced limitation)".to_string(),
);
}
let mut last_offset = -1.0;
for stop in stops {
if stop.offset < 0.0 || stop.offset > 1.0 {
return Err(format!(
"Color stop offset must be 0.0-1.0, got {}",
stop.offset
));
}
if stop.offset <= last_offset {
return Err("Color stop offsets must be in ascending order".to_string());
}
stop.color.validate()?;
last_offset = stop.offset;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ColorStop {
pub color: Color,
pub offset: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RadialShape {
Circle,
Ellipse,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Border {
pub width: f32,
pub color: Color,
pub radius: BorderRadius,
pub style: BorderStyle,
}
impl Border {
pub fn validate(&self) -> Result<(), String> {
if self.width < 0.0 {
return Err(format!(
"Border width must be non-negative, got {}",
self.width
));
}
self.color.validate()?;
self.radius.validate()?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BorderRadius {
pub top_left: f32,
pub top_right: f32,
pub bottom_right: f32,
pub bottom_left: f32,
}
impl BorderRadius {
pub fn parse(s: &str) -> Result<Self, String> {
let parts: Vec<&str> = s.split_whitespace().collect();
match parts.len() {
1 => {
let all: f32 = parts[0]
.parse()
.map_err(|_| format!("Invalid border radius: {}", s))?;
Ok(BorderRadius {
top_left: all,
top_right: all,
bottom_right: all,
bottom_left: all,
})
}
4 => {
let tl: f32 = parts[0]
.parse()
.map_err(|_| format!("Invalid top-left radius: {}", parts[0]))?;
let tr: f32 = parts[1]
.parse()
.map_err(|_| format!("Invalid top-right radius: {}", parts[1]))?;
let br: f32 = parts[2]
.parse()
.map_err(|_| format!("Invalid bottom-right radius: {}", parts[2]))?;
let bl: f32 = parts[3]
.parse()
.map_err(|_| format!("Invalid bottom-left radius: {}", parts[3]))?;
Ok(BorderRadius {
top_left: tl,
top_right: tr,
bottom_right: br,
bottom_left: bl,
})
}
_ => Err(format!(
"Invalid border radius format: '{}'. Expected 1 or 4 values",
s
)),
}
}
pub fn validate(&self) -> Result<(), String> {
if self.top_left < 0.0
|| self.top_right < 0.0
|| self.bottom_right < 0.0
|| self.bottom_left < 0.0
{
return Err("Border radius values must be non-negative".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BorderStyle {
Solid,
Dashed,
Dotted,
}
impl BorderStyle {
pub fn parse(s: &str) -> Result<Self, String> {
match s.trim().to_lowercase().as_str() {
"solid" => Ok(BorderStyle::Solid),
"dashed" => Ok(BorderStyle::Dashed),
"dotted" => Ok(BorderStyle::Dotted),
_ => Err(format!(
"Invalid border style: '{}'. Expected solid, dashed, or dotted",
s
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Shadow {
pub offset_x: f32,
pub offset_y: f32,
pub blur_radius: f32,
pub color: Color,
}
impl Shadow {
pub fn parse(s: &str) -> Result<Self, String> {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() < 4 {
return Err(format!(
"Invalid shadow format: '{}'. Expected: offset_x offset_y blur color",
s
));
}
let offset_x: f32 = parts[0]
.parse()
.map_err(|_| format!("Invalid offset_x: {}", parts[0]))?;
let offset_y: f32 = parts[1]
.parse()
.map_err(|_| format!("Invalid offset_y: {}", parts[1]))?;
let blur_radius: f32 = parts[2]
.parse()
.map_err(|_| format!("Invalid blur_radius: {}", parts[2]))?;
let color_str = parts[3..].join(" ");
let color = Color::parse(&color_str)?;
Ok(Shadow {
offset_x,
offset_y,
blur_radius,
color,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Transform {
Scale(f32),
ScaleXY { x: f32, y: f32 },
Rotate(f32),
Translate { x: f32, y: f32 },
Matrix([f32; 6]),
Multiple(Vec<Transform>),
}
impl Transform {
pub fn parse(s: &str) -> Result<Self, String> {
let s = s.trim();
if s.starts_with("scale(") && s.ends_with(')') {
let inner = &s[6..s.len() - 1];
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() == 1 {
let value: f32 = inner
.parse()
.map_err(|_| format!("Invalid scale value: {}", s))?;
return Ok(Transform::Scale(value));
} else if parts.len() == 2 {
let x: f32 = parts[0]
.trim()
.parse()
.map_err(|_| format!("Invalid scale x: {}", parts[0]))?;
let y: f32 = parts[1]
.trim()
.parse()
.map_err(|_| format!("Invalid scale y: {}", parts[1]))?;
return Ok(Transform::ScaleXY { x, y });
}
}
if s.starts_with("rotate(") && s.ends_with(')') {
let inner = &s[7..s.len() - 1];
let value: f32 = inner
.parse()
.map_err(|_| format!("Invalid rotate value: {}", s))?;
return Ok(Transform::Rotate(value));
}
if s.starts_with("translate(") && s.ends_with(')') {
let inner = &s[10..s.len() - 1];
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() == 2 {
let x: f32 = parts[0]
.trim()
.parse()
.map_err(|_| format!("Invalid translate x: {}", parts[0]))?;
let y: f32 = parts[1]
.trim()
.parse()
.map_err(|_| format!("Invalid translate y: {}", parts[1]))?;
return Ok(Transform::Translate { x, y });
}
}
if s.starts_with("matrix(") && s.ends_with(')') {
let inner = &s[7..s.len() - 1];
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() == 6 {
let mut matrix = [0.0; 6];
for (i, p) in parts.iter().enumerate() {
matrix[i] = p
.trim()
.parse()
.map_err(|_| format!("Invalid matrix value at index {}: {}", i, p))?;
}
return Ok(Transform::Matrix(matrix));
}
}
Err(format!(
"Invalid transform format: '{}'. Expected scale(n), scale(x, y), rotate(rad), translate(x, y), or matrix(...)",
s
))
}
}