use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::convert::TryFrom;
use core::{error, fmt};
use crate::{linspace, BlendMode, CSSGradientParser, Color};
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum GradientBuilderError {
InvalidHtmlColors(Vec<String>),
InvalidCssGradient,
InvalidDomain,
InvalidStops,
}
impl fmt::Display for GradientBuilderError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::InvalidHtmlColors(ref colors) => {
write!(
f,
"invalid html colors: {}",
colors
.iter()
.map(|x| format!("'{x}'"))
.collect::<Vec<String>>()
.join(", ")
)
}
Self::InvalidCssGradient => f.write_str("invalid css gradient"),
Self::InvalidDomain => f.write_str("invalid domain"),
Self::InvalidStops => f.write_str("invalid stops"),
}
}
}
impl error::Error for GradientBuilderError {}
#[cfg_attr(
feature = "named-colors",
doc = r##"
## Using web color format string
```
# use std::error::Error;
use colorgrad::Gradient;
# fn main() -> Result<(), Box<dyn Error>> {
let grad = colorgrad::GradientBuilder::new()
.html_colors(&["deeppink", "gold", "seagreen"])
.domain(&[0.0, 100.0])
.mode(colorgrad::BlendMode::Rgb)
.build::<colorgrad::LinearGradient>()?;
assert_eq!(grad.at(0.0).to_rgba8(), [255, 20, 147, 255]);
assert_eq!(grad.at(100.0).to_rgba8(), [46, 139, 87, 255]);
# Ok(())
# }
```"##
)]
#[derive(Debug, Clone)]
pub struct GradientBuilder {
pub(crate) colors: Vec<Color>,
pub(crate) positions: Vec<f32>,
pub(crate) mode: BlendMode,
invalid_html_colors: Vec<String>,
invalid_css_gradient: bool,
clean: bool,
}
impl GradientBuilder {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
colors: Vec::new(),
positions: Vec::new(),
mode: BlendMode::Rgb,
invalid_html_colors: Vec::new(),
invalid_css_gradient: false,
clean: false,
}
}
pub fn colors<'a>(&'a mut self, colors: &[Color]) -> &'a mut Self {
for c in colors {
self.colors.push(c.clone());
}
self.clean = false;
self
}
pub fn html_colors<'a, S: AsRef<str> + ToString>(
&'a mut self,
html_colors: &[S],
) -> &'a mut Self {
for s in html_colors {
if let Ok(c) = csscolorparser::parse(s.as_ref()) {
self.colors.push(c);
} else {
self.invalid_html_colors.push(s.to_string());
}
}
self.clean = false;
self
}
pub fn domain<'a>(&'a mut self, positions: &[f32]) -> &'a mut Self {
self.positions = positions.to_vec();
self.clean = false;
self
}
pub fn mode(&mut self, mode: BlendMode) -> &mut Self {
self.mode = mode;
self
}
pub fn css<'a>(&'a mut self, s: &str) -> &'a mut Self {
let [dmin, dmax] = if self.positions.len() == 2 && self.positions[0] < self.positions[1] {
[self.positions[0], self.positions[1]]
} else {
[0.0, 1.0]
};
let mut cgp = CSSGradientParser::new();
cgp.set_domain(dmin, dmax);
cgp.set_mode(self.mode);
if let Some((colors, positions)) = cgp.parse(s) {
self.invalid_css_gradient = false;
self.colors = colors;
self.positions = positions;
} else {
self.invalid_css_gradient = true;
}
self.clean = false;
self
}
pub fn reset(&mut self) -> &mut Self {
self.colors.clear();
self.positions.clear();
self.mode = BlendMode::Rgb;
self.invalid_html_colors.clear();
self.invalid_css_gradient = false;
self.clean = false;
self
}
#[doc(hidden)]
pub fn get_colors(&self) -> &[Color] {
&self.colors
}
#[doc(hidden)]
pub fn get_positions(&self) -> &[f32] {
&self.positions
}
pub fn build<'a, T>(&'a mut self) -> Result<T, T::Error>
where
T: TryFrom<&'a mut Self, Error = GradientBuilderError>,
{
T::try_from(self)
}
pub(crate) fn prepare_build(&mut self) -> Result<(), GradientBuilderError> {
if self.clean {
return Ok(());
}
if !self.invalid_html_colors.is_empty() {
return Err(GradientBuilderError::InvalidHtmlColors(
self.invalid_html_colors.clone(),
));
}
if self.invalid_css_gradient {
return Err(GradientBuilderError::InvalidCssGradient);
}
let colors = if self.colors.is_empty() {
vec![
Color::new(0.0, 0.0, 0.0, 1.0),
Color::new(1.0, 1.0, 1.0, 1.0),
]
} else if self.colors.len() == 1 {
vec![self.colors[0].clone(), self.colors[0].clone()]
} else {
self.colors.to_vec()
};
let positions = if self.positions.is_empty() {
linspace(0.0, 1.0, colors.len()).collect()
} else if self.positions.len() == colors.len() {
for p in self.positions.windows(2) {
if p[0] > p[1] {
return Err(GradientBuilderError::InvalidDomain);
}
}
self.positions.to_vec()
} else if self.positions.len() == 2 {
if self.positions[0] >= self.positions[1] {
return Err(GradientBuilderError::InvalidDomain);
}
linspace(self.positions[0], self.positions[1], colors.len()).collect()
} else {
return Err(GradientBuilderError::InvalidDomain);
};
self.colors.clear();
self.positions.clear();
let mut prev = positions[0];
let last_idx = positions.len() - 1;
for (i, (pos, col)) in positions.iter().zip(colors.iter()).enumerate() {
let next = if i == last_idx {
positions[last_idx]
} else {
positions[i + 1]
};
if (pos - prev) + (next - pos) < f32::EPSILON {
} else {
self.positions.push(*pos);
self.colors.push(col.clone());
}
prev = *pos;
}
if self.colors.len() < 2 {
return Err(GradientBuilderError::InvalidStops);
}
self.clean = true;
Ok(())
}
}