use rich::measure::Measurement;
use rich::{Color, Console, ConsoleOptions, Renderable, Segment, Style, Text};
use crate::palette::gradient;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum GradientDirection {
#[default]
Horizontal,
Vertical,
Diagonal,
}
#[derive(Clone, Debug)]
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct GradientText {
text: Text,
stops: Vec<Color>,
direction: GradientDirection,
background: bool,
}
impl GradientText {
pub fn new(text: Text, stops: impl IntoIterator<Item = Color>) -> Self {
GradientText {
text,
stops: stops.into_iter().collect(),
direction: GradientDirection::Horizontal,
background: false,
}
}
pub fn direction(mut self, direction: GradientDirection) -> Self {
self.direction = direction;
self
}
pub fn background(mut self, background: bool) -> Self {
self.background = background;
self
}
pub fn text(&self) -> &Text {
&self.text
}
}
fn share(index: usize, count: usize) -> f64 {
if count < 2 {
0.0
} else {
index as f64 / (count - 1) as f64
}
}
impl Renderable for GradientText {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let system = match console.color_system() {
Some(system) if !console.no_color() && !self.stops.is_empty() => system,
_ => return self.text.rich_render(console, options),
};
let lines = console.render_lines(&self.text, options, false);
let widest = lines
.iter()
.map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
.max()
.unwrap_or(0);
let rows = lines.len();
let mut out: Vec<Segment> = Vec::new();
let mut last: Option<(Option<Style>, String)> = None;
for (row, line) in lines.into_iter().enumerate() {
if row > 0 {
out.push(Segment::line());
last = None;
}
let mut col = 0;
for segment in line {
if segment.control {
out.push(segment);
last = None;
continue;
}
for c in segment.text.chars() {
let t = match self.direction {
GradientDirection::Horizontal => share(col, widest),
GradientDirection::Vertical => share(row, rows),
GradientDirection::Diagonal => {
(share(col, widest) + share(row, rows)) / 2.0
}
};
col += rich::cells::char_cell_width(c);
let colour = gradient(&self.stops, t).downgrade(system);
let paint = if self.background {
Style::from_color(None, Some(colour))
} else {
Style::from_color(Some(colour), None)
};
let key = (segment.style.clone(), paint.ansi_codes(system));
match out.last_mut() {
Some(run) if last.as_ref() == Some(&key) => run.text.push(c),
_ => {
let style = match &segment.style {
Some(own) => own.combine(&paint),
None => paint,
};
out.push(Segment::new(c.to_string(), Some(style)));
last = Some(key);
}
}
}
}
}
out
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.text.measure(console, options)
}
}
impl crate::a11y::AccessibleText for GradientText {
fn accessible_text(&self, _width: usize) -> String {
self.text.plain().to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shares_run_from_the_first_to_the_last() {
assert_eq!(share(0, 5), 0.0);
assert_eq!(share(4, 5), 1.0);
assert_eq!(share(2, 5), 0.5);
assert_eq!(share(0, 1), 0.0);
}
}