use crate::element::{Component, Element};
use crate::style::{Color, Modifier, Style};
#[derive(Debug, Clone, Copy)]
pub struct ColorStop {
pub position: f32,
pub color: Color,
}
impl ColorStop {
pub fn new(position: f32, color: Color) -> Self {
Self {
position: position.clamp(0.0, 1.0),
color,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GradientPreset {
#[default]
Rainbow,
Sunset,
Ocean,
Forest,
Fire,
Ice,
PurpleHaze,
Grayscale,
Neon,
Mint,
}
impl GradientPreset {
pub fn stops(&self) -> Vec<ColorStop> {
match self {
GradientPreset::Rainbow => vec![
ColorStop::new(0.0, Color::Red),
ColorStop::new(0.17, Color::Rgb(255, 165, 0)), ColorStop::new(0.33, Color::Yellow),
ColorStop::new(0.5, Color::Green),
ColorStop::new(0.67, Color::Cyan),
ColorStop::new(0.83, Color::Blue),
ColorStop::new(1.0, Color::Magenta),
],
GradientPreset::Sunset => vec![
ColorStop::new(0.0, Color::Red),
ColorStop::new(0.5, Color::Rgb(255, 165, 0)), ColorStop::new(1.0, Color::Yellow),
],
GradientPreset::Ocean => vec![
ColorStop::new(0.0, Color::Cyan),
ColorStop::new(0.5, Color::Blue),
ColorStop::new(1.0, Color::Rgb(0, 0, 139)), ],
GradientPreset::Forest => vec![
ColorStop::new(0.0, Color::Rgb(144, 238, 144)), ColorStop::new(0.5, Color::Green),
ColorStop::new(1.0, Color::Rgb(0, 100, 0)), ],
GradientPreset::Fire => vec![
ColorStop::new(0.0, Color::Yellow),
ColorStop::new(0.5, Color::Rgb(255, 165, 0)), ColorStop::new(1.0, Color::Red),
],
GradientPreset::Ice => vec![
ColorStop::new(0.0, Color::White),
ColorStop::new(0.5, Color::Cyan),
ColorStop::new(1.0, Color::Blue),
],
GradientPreset::PurpleHaze => vec![
ColorStop::new(0.0, Color::Magenta),
ColorStop::new(0.5, Color::Rgb(128, 0, 128)), ColorStop::new(1.0, Color::Blue),
],
GradientPreset::Grayscale => vec![
ColorStop::new(0.0, Color::White),
ColorStop::new(0.5, Color::Gray),
ColorStop::new(1.0, Color::DarkGray),
],
GradientPreset::Neon => vec![
ColorStop::new(0.0, Color::Cyan),
ColorStop::new(1.0, Color::Magenta),
],
GradientPreset::Mint => vec![
ColorStop::new(0.0, Color::Cyan),
ColorStop::new(1.0, Color::Green),
],
}
}
}
#[derive(Debug, Clone)]
pub struct GradientProps {
pub content: String,
pub preset: Option<GradientPreset>,
pub stops: Vec<ColorStop>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
}
impl Default for GradientProps {
fn default() -> Self {
Self {
content: String::new(),
preset: Some(GradientPreset::Rainbow),
stops: Vec::new(),
bold: false,
italic: false,
underline: false,
}
}
}
impl GradientProps {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
..Default::default()
}
}
#[must_use]
pub fn preset(mut self, preset: GradientPreset) -> Self {
self.preset = Some(preset);
self.stops.clear();
self
}
#[must_use]
pub fn stop(mut self, position: f32, color: Color) -> Self {
self.preset = None;
self.stops.push(ColorStop::new(position, color));
self
}
#[must_use]
pub fn stops(mut self, stops: Vec<ColorStop>) -> Self {
self.preset = None;
self.stops = stops;
self
}
#[must_use]
pub fn two_colors(mut self, start: Color, end: Color) -> Self {
self.preset = None;
self.stops = vec![ColorStop::new(0.0, start), ColorStop::new(1.0, end)];
self
}
#[must_use]
pub fn three_colors(mut self, start: Color, middle: Color, end: Color) -> Self {
self.preset = None;
self.stops = vec![
ColorStop::new(0.0, start),
ColorStop::new(0.5, middle),
ColorStop::new(1.0, end),
];
self
}
#[must_use]
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
#[must_use]
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
#[must_use]
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
fn effective_stops(&self) -> Vec<ColorStop> {
if !self.stops.is_empty() {
let mut stops = self.stops.clone();
stops.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
stops
} else if let Some(preset) = self.preset {
preset.stops()
} else {
vec![ColorStop::new(0.0, Color::White)]
}
}
fn interpolate_color(stops: &[ColorStop], position: f32) -> Color {
if stops.is_empty() {
return Color::White;
}
if stops.len() == 1 {
return stops[0].color;
}
let position = position.clamp(0.0, 1.0);
let mut lower = &stops[0];
let mut upper = &stops[stops.len() - 1];
for i in 0..stops.len() - 1 {
if position >= stops[i].position && position <= stops[i + 1].position {
lower = &stops[i];
upper = &stops[i + 1];
break;
}
}
let range = upper.position - lower.position;
let t = if range > 0.0 {
(position - lower.position) / range
} else {
0.0
};
Self::lerp_color(lower.color, upper.color, t)
}
fn lerp_color(a: Color, b: Color, t: f32) -> Color {
let (r1, g1, b1) = Self::color_to_rgb(a);
let (r2, g2, b2) = Self::color_to_rgb(b);
let r = (r1 as f32 + (r2 as f32 - r1 as f32) * t) as u8;
let g = (g1 as f32 + (g2 as f32 - g1 as f32) * t) as u8;
let b_val = (b1 as f32 + (b2 as f32 - b1 as f32) * t) as u8;
Color::Rgb(r, g, b_val)
}
fn color_to_rgb(color: Color) -> (u8, u8, u8) {
match color {
Color::Rgb(r, g, b) => (r, g, b),
Color::Red => (255, 0, 0),
Color::Green => (0, 255, 0),
Color::Blue => (0, 0, 255),
Color::Yellow => (255, 255, 0),
Color::Cyan => (0, 255, 255),
Color::Magenta => (255, 0, 255),
Color::White => (255, 255, 255),
Color::Black => (0, 0, 0),
Color::Gray => (128, 128, 128),
Color::DarkGray => (64, 64, 64),
Color::LightRed => (255, 128, 128),
Color::LightGreen => (128, 255, 128),
Color::LightBlue => (128, 128, 255),
Color::LightYellow => (255, 255, 128),
Color::LightCyan => (128, 255, 255),
Color::LightMagenta => (255, 128, 255),
Color::Indexed(idx) => {
if idx < 16 {
match idx {
0 => (0, 0, 0),
1 => (128, 0, 0),
2 => (0, 128, 0),
3 => (128, 128, 0),
4 => (0, 0, 128),
5 => (128, 0, 128),
6 => (0, 128, 128),
7 => (192, 192, 192),
8 => (128, 128, 128),
9 => (255, 0, 0),
10 => (0, 255, 0),
11 => (255, 255, 0),
12 => (0, 0, 255),
13 => (255, 0, 255),
14 => (0, 255, 255),
15 => (255, 255, 255),
_ => (128, 128, 128),
}
} else if idx < 232 {
let idx = idx - 16;
let r = (idx / 36) * 51;
let g = ((idx % 36) / 6) * 51;
let b = (idx % 6) * 51;
(r, g, b)
} else {
let gray = (idx - 232) * 10 + 8;
(gray, gray, gray)
}
}
Color::Reset => (255, 255, 255), }
}
fn base_modifiers(&self) -> Modifier {
let mut modifiers = Modifier::empty();
if self.bold {
modifiers |= Modifier::BOLD;
}
if self.italic {
modifiers |= Modifier::ITALIC;
}
if self.underline {
modifiers |= Modifier::UNDERLINED;
}
modifiers
}
}
pub struct Gradient;
impl Component for Gradient {
type Props = GradientProps;
fn render(props: &Self::Props) -> Element {
let chars: Vec<char> = props.content.chars().collect();
let len = chars.len();
if len == 0 {
return Element::text("");
}
let stops = props.effective_stops();
let base_modifiers = props.base_modifiers();
let children: Vec<Element> = chars
.into_iter()
.enumerate()
.map(|(i, ch)| {
let position = if len > 1 {
i as f32 / (len - 1) as f32
} else {
0.0
};
let color = GradientProps::interpolate_color(&stops, position);
let style = Style::new().fg(color).add_modifier(base_modifiers);
Element::styled_text(ch.to_string(), style)
})
.collect();
Element::Fragment(children)
}
}
pub fn gradient(content: impl Into<String>) -> String {
content.into()
}
pub fn gradient_preset(content: impl Into<String>, preset: GradientPreset) -> GradientProps {
GradientProps::new(content).preset(preset)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_stop_new() {
let stop = ColorStop::new(0.5, Color::Red);
assert_eq!(stop.position, 0.5);
assert_eq!(stop.color, Color::Red);
}
#[test]
fn test_color_stop_clamp() {
let stop = ColorStop::new(1.5, Color::Red);
assert_eq!(stop.position, 1.0);
let stop = ColorStop::new(-0.5, Color::Blue);
assert_eq!(stop.position, 0.0);
}
#[test]
fn test_gradient_preset_rainbow() {
let stops = GradientPreset::Rainbow.stops();
assert_eq!(stops.len(), 7);
assert_eq!(stops[0].color, Color::Red);
}
#[test]
fn test_gradient_preset_sunset() {
let stops = GradientPreset::Sunset.stops();
assert_eq!(stops.len(), 3);
assert_eq!(stops[0].color, Color::Red);
}
#[test]
fn test_gradient_props_new() {
let props = GradientProps::new("test");
assert_eq!(props.content, "test");
assert_eq!(props.preset, Some(GradientPreset::Rainbow));
}
#[test]
fn test_gradient_props_preset() {
let props = GradientProps::new("test").preset(GradientPreset::Ocean);
assert_eq!(props.preset, Some(GradientPreset::Ocean));
}
#[test]
fn test_gradient_props_two_colors() {
let props = GradientProps::new("test").two_colors(Color::Red, Color::Blue);
assert!(props.preset.is_none());
assert_eq!(props.stops.len(), 2);
}
#[test]
fn test_gradient_props_three_colors() {
let props = GradientProps::new("test").three_colors(Color::Red, Color::Green, Color::Blue);
assert!(props.preset.is_none());
assert_eq!(props.stops.len(), 3);
}
#[test]
fn test_gradient_props_builder() {
let props = GradientProps::new("test").bold().italic().underline();
assert!(props.bold);
assert!(props.italic);
assert!(props.underline);
}
#[test]
fn test_gradient_interpolate_simple() {
let stops = vec![
ColorStop::new(0.0, Color::Rgb(0, 0, 0)),
ColorStop::new(1.0, Color::Rgb(255, 255, 255)),
];
let mid = GradientProps::interpolate_color(&stops, 0.5);
if let Color::Rgb(r, g, b) = mid {
assert!(r > 100 && r < 150); assert!(g > 100 && g < 150);
assert!(b > 100 && b < 150);
} else {
panic!("Expected RGB color");
}
}
#[test]
fn test_gradient_interpolate_edge() {
let stops = vec![
ColorStop::new(0.0, Color::Red),
ColorStop::new(1.0, Color::Blue),
];
let start = GradientProps::interpolate_color(&stops, 0.0);
if let Color::Rgb(r, _, _) = start {
assert_eq!(r, 255);
}
let end = GradientProps::interpolate_color(&stops, 1.0);
if let Color::Rgb(_, _, b) = end {
assert_eq!(b, 255);
}
}
#[test]
fn test_gradient_component_render() {
let props = GradientProps::new("Hi");
let elem = Gradient::render(&props);
match elem {
Element::Fragment(children) => {
assert_eq!(children.len(), 2);
}
_ => panic!("Expected Fragment"),
}
}
#[test]
fn test_gradient_component_render_empty() {
let props = GradientProps::new("");
let elem = Gradient::render(&props);
assert!(elem.is_text());
}
#[test]
fn test_gradient_helper() {
let text = gradient("Hello");
assert_eq!(text, "Hello");
}
#[test]
fn test_gradient_preset_helper() {
let props = gradient_preset("Test", GradientPreset::Fire);
assert_eq!(props.content, "Test");
assert_eq!(props.preset, Some(GradientPreset::Fire));
}
#[test]
fn test_color_to_rgb() {
assert_eq!(GradientProps::color_to_rgb(Color::Red), (255, 0, 0));
assert_eq!(GradientProps::color_to_rgb(Color::Green), (0, 255, 0));
assert_eq!(GradientProps::color_to_rgb(Color::Blue), (0, 0, 255));
assert_eq!(GradientProps::color_to_rgb(Color::White), (255, 255, 255));
assert_eq!(GradientProps::color_to_rgb(Color::Black), (0, 0, 0));
}
}