use std::{
f32::consts::FRAC_PI_2,
fmt,
};
use freya_engine::prelude::*;
use torin::{
prelude::Measure,
size::Rect,
};
use crate::{
DisplayColor,
ExtSplit,
Parse,
ParseError,
};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct GradientStop {
pub color: Color,
pub offset: f32,
}
impl Parse for GradientStop {
fn parse(value: &str) -> Result<Self, ParseError> {
let mut split = value.split_ascii_whitespace_excluding_group('(', ')');
let color_str = split.next().ok_or(ParseError)?;
let offset_str = split.next().ok_or(ParseError)?.trim();
if !offset_str.ends_with('%') || split.next().is_some() {
return Err(ParseError);
}
let offset = offset_str
.replacen('%', "", 1)
.parse::<f32>()
.map_err(|_| ParseError)?
/ 100.0;
Ok(GradientStop {
color: Color::parse(color_str).map_err(|_| ParseError)?,
offset,
})
}
}
impl fmt::Display for GradientStop {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
_ = self.color.fmt_rgb(f);
write!(f, " {}%", self.offset * 100.0)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LinearGradient {
pub stops: Vec<GradientStop>,
pub angle: f32,
}
impl LinearGradient {
pub fn into_shader(&self, bounds: Rect<f32, Measure>) -> Option<Shader> {
let colors: Vec<Color> = self.stops.iter().map(|stop| stop.color).collect();
let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
let (dy, dx) = (self.angle.to_radians() + FRAC_PI_2).sin_cos();
let farthest_corner = Point::new(
if dx > 0.0 { bounds.width() } else { 0.0 },
if dy > 0.0 { bounds.height() } else { 0.0 },
);
let delta = farthest_corner - Point::new(bounds.width(), bounds.height()) / 2.0;
let u = delta.x * dy - delta.y * dx;
let endpoint = farthest_corner + Point::new(-u * dy, u * dx);
let origin = Point::new(bounds.min_x(), bounds.min_y());
Shader::linear_gradient(
(
Point::new(bounds.width(), bounds.height()) - endpoint + origin,
endpoint + origin,
),
GradientShaderColors::Colors(&colors[..]),
Some(&offsets[..]),
TileMode::Clamp,
None,
None,
)
}
}
impl Parse for LinearGradient {
fn parse(value: &str) -> Result<Self, ParseError> {
if !value.starts_with("linear-gradient(") || !value.ends_with(')') {
return Err(ParseError);
}
let mut gradient = LinearGradient::default();
let mut value = value.replacen("linear-gradient(", "", 1);
value.remove(value.rfind(')').ok_or(ParseError)?);
let mut split = value.split_excluding_group(',', '(', ')');
let angle_or_first_stop = split.next().ok_or(ParseError)?.trim();
if angle_or_first_stop.ends_with("deg") {
if let Ok(angle) = angle_or_first_stop.replacen("deg", "", 1).parse::<f32>() {
gradient.angle = angle;
}
} else {
gradient
.stops
.push(GradientStop::parse(angle_or_first_stop)?);
}
for stop in split {
gradient.stops.push(GradientStop::parse(stop)?);
}
Ok(gradient)
}
}
impl fmt::Display for LinearGradient {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"linear-gradient({}deg, {})",
self.angle,
self.stops
.iter()
.map(|stop| stop.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RadialGradient {
pub stops: Vec<GradientStop>,
}
impl RadialGradient {
pub fn into_shader(&self, bounds: Rect<f32, Measure>) -> Option<Shader> {
let colors: Vec<Color> = self.stops.iter().map(|stop| stop.color).collect();
let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
let center = bounds.center();
Shader::radial_gradient(
Point::new(center.x, center.y),
bounds.width().max(bounds.height()) / 2.0,
GradientShaderColors::Colors(&colors[..]),
Some(&offsets[..]),
TileMode::Clamp,
None,
None,
)
}
}
impl Parse for RadialGradient {
fn parse(value: &str) -> Result<Self, ParseError> {
if !value.starts_with("radial-gradient(") || !value.ends_with(')') {
return Err(ParseError);
}
let mut gradient = RadialGradient::default();
let mut value = value.replacen("radial-gradient(", "", 1);
value.remove(value.rfind(')').ok_or(ParseError)?);
for stop in value.split_excluding_group(',', '(', ')') {
gradient.stops.push(GradientStop::parse(stop)?);
}
Ok(gradient)
}
}
impl fmt::Display for RadialGradient {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"radial-gradient({})",
self.stops
.iter()
.map(|stop| stop.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ConicGradient {
pub stops: Vec<GradientStop>,
pub angles: Option<(f32, f32)>,
pub angle: Option<f32>,
}
impl ConicGradient {
pub fn into_shader(&self, bounds: Rect<f32, Measure>) -> Option<Shader> {
let colors: Vec<Color> = self.stops.iter().map(|stop| stop.color).collect();
let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
let center = bounds.center();
let matrix =
Matrix::rotate_deg_pivot(-90.0 + self.angle.unwrap_or(0.0), (center.x, center.y));
Shader::sweep_gradient(
(center.x, center.y),
GradientShaderColors::Colors(&colors[..]),
Some(&offsets[..]),
TileMode::Clamp,
self.angles,
None,
Some(&matrix),
)
}
}
impl Parse for ConicGradient {
fn parse(value: &str) -> Result<Self, ParseError> {
if !value.starts_with("conic-gradient(") || !value.ends_with(')') {
return Err(ParseError);
}
let mut gradient = ConicGradient::default();
let mut value = value.replacen("conic-gradient(", "", 1);
value.remove(value.rfind(')').ok_or(ParseError)?);
let mut split = value.split_excluding_group(',', '(', ')');
let angle_or_first_stop = split.next().ok_or(ParseError)?.trim();
if angle_or_first_stop.ends_with("deg") {
if let Ok(angle) = angle_or_first_stop.replacen("deg", "", 1).parse::<f32>() {
gradient.angle = Some(angle);
}
} else {
gradient
.stops
.push(GradientStop::parse(angle_or_first_stop).map_err(|_| ParseError)?);
}
if let Some(angles_or_second_stop) = split.next().map(str::trim) {
if angles_or_second_stop.starts_with("from ") && angles_or_second_stop.ends_with("deg")
{
if let Some(start) = angles_or_second_stop
.find("deg")
.and_then(|index| angles_or_second_stop.get(5..index))
.and_then(|slice| slice.parse::<f32>().ok())
{
let end = angles_or_second_stop
.find(" to ")
.and_then(|index| angles_or_second_stop.get(index + 4..))
.and_then(|slice| slice.find("deg").and_then(|index| slice.get(0..index)))
.and_then(|slice| slice.parse::<f32>().ok())
.unwrap_or(360.0);
gradient.angles = Some((start, end));
}
} else {
gradient
.stops
.push(GradientStop::parse(angles_or_second_stop)?);
}
}
for stop in split {
gradient.stops.push(GradientStop::parse(stop)?);
}
Ok(gradient)
}
}
impl fmt::Display for ConicGradient {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "conic-gradient(")?;
if let Some(angle) = self.angle {
write!(f, "{angle}deg, ")?;
}
if let Some((start, end)) = self.angles {
write!(f, "from {start}deg to {end}deg, ")?;
}
write!(
f,
"{})",
self.stops
.iter()
.map(|stop| stop.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}