use serde::{Deserialize, Serialize};
#[cfg(feature = "utoipa")]
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
pub struct GradientStop {
pub position: f32,
pub color: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
pub struct Gradient {
pub angle: f32,
pub stops: Vec<GradientStop>,
}
impl Gradient {
pub fn validate(&self) -> crate::Result<()> {
if !(0.0..std::f32::consts::TAU).contains(&self.angle) {
return Err(crate::Error::InvalidField {
field: "angle".to_string(),
message: "Gradient angle must be in radians, between 0 and 2 * PI".to_string(),
});
}
if self.stops.is_empty() {
return Err(crate::Error::InvalidField {
field: "stops".to_string(),
message: "Gradient must have at least one stop".to_string(),
});
}
if self.stops.len() > 8 {
return Err(crate::Error::InvalidField {
field: "stops".to_string(),
message: "Gradient may only have at most 8 stops".to_string(),
});
}
let mut last = 0.0;
for stop in &self.stops {
if stop.position < 0.0 || stop.position > 1.0 {
return Err(crate::Error::InvalidField {
field: "stops".to_string(),
message: "Gradient stop position must be between 0 and 1".to_string(),
});
}
if stop.position < last {
return Err(crate::Error::InvalidField {
field: "stops".to_string(),
message: "Gradient stops must be sorted by position".to_string(),
});
}
last = stop.position;
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ExtendedColor {
Solid {
color: u32,
},
Gradient(Gradient),
}
impl ExtendedColor {
pub fn validate(&self) -> crate::Result<()> {
match self {
Self::Solid { .. } => Ok(()),
Self::Gradient(gradient) => gradient.validate(),
}
}
}
#[cfg(feature = "db")]
#[derive(sqlx::Type, Copy, Clone, Debug)]
#[sqlx(type_name = "gradient_stop")]
pub(crate) struct DbGradientStop {
position: f32,
color: i32,
}
#[cfg(feature = "db")]
#[derive(sqlx::Type, Clone, Debug)]
#[sqlx(type_name = "gradient_type")]
pub(crate) struct DbGradient {
angle: f32,
stops: Vec<DbGradientStop>,
}
impl ExtendedColor {
#[must_use]
#[cfg(feature = "db")]
pub(crate) fn from_db(color: Option<i32>, gradient: Option<&DbGradient>) -> Option<Self> {
match (color, gradient) {
(_, Some(gradient)) => {
let stops = gradient
.stops
.iter()
.map(|s| GradientStop {
position: s.position,
color: s.color as u32,
})
.collect();
Some(Self::Gradient(Gradient {
angle: gradient.angle,
stops,
}))
}
(Some(color), _) => Some(Self::Solid {
color: color as u32,
}),
_ => None,
}
}
#[must_use]
#[cfg(feature = "db")]
pub(crate) fn to_db(&self) -> (Option<i32>, Option<DbGradient>) {
match self {
Self::Solid { color } => (Some(*color as i32), None),
Self::Gradient(gradient) => {
let stops = gradient
.stops
.iter()
.map(|s| DbGradientStop {
position: s.position,
color: s.color as i32,
})
.collect();
(
None,
Some(DbGradient {
angle: gradient.angle,
stops,
}),
)
}
}
}
}