Skip to main content

tsu_editor/
color.rs

1#![allow(clippy::cast_sign_loss)]
2#![allow(clippy::cast_possible_truncation)]
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8/// The internal representation of a hexadecimal RGB/RGBA color
9///
10/// This internal representation serves as an intermediate representation of "color". It can be
11/// converted to and from `crossterm::style::Color::Rgb`, although it is lossy because crossterm
12/// does not have an Rgba analogue
13pub enum Color {
14    /// A standard hexadecimal color representation parameterized as red, green, and blue
15    Rgb { r: u8, g: u8, b: u8 },
16    /// A hexadecimal color representation parameterized as red, green, blue, and alpha for
17    /// transparency
18    Rgba { r: u8, g: u8, b: u8, a: u8 },
19}
20
21impl Default for Color {
22    /// Generates a `Color` with default values
23    ///
24    /// # Returns
25    /// - A default `Color`, which in this case is #000000, or black
26    fn default() -> Self {
27        Color::Rgb { r: 0, g: 0, b: 0 }
28    }
29}
30
31impl From<Color> for crossterm::style::Color {
32    /// Provides the logic for converting our custom `Color` to a `crossterm::style::Color`
33    ///
34    /// # Arguments
35    /// - `color`: An instance of our custom `Color` struct
36    ///
37    /// # Returns
38    /// - An instance of `crossterm::style::Color` derived from the custom `Color`
39    fn from(color: Color) -> Self {
40        match color {
41            Color::Rgb { r, g, b } | Color::Rgba { r, g, b, a: _ } => {
42                crossterm::style::Color::Rgb { r, g, b }
43            }
44        }
45    }
46}
47
48impl fmt::Display for Color {
49    /// Provides formatting for printing the contents of `Color` to the console
50    ///
51    /// # Arguments
52    /// - `f`: Mutable reference to a `std::fmt::Formatter`
53    ///
54    /// # Returns
55    /// - The result of the formatted string
56    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57        match self {
58            Color::Rgb { r, g, b } => write!(f, "#{r:02x}{g:02x}{b:02x}"),
59            Color::Rgba { r, g, b, a } => write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}"),
60        }
61    }
62}
63
64/// Attempt to parse a hexadecimal string into a `Color`
65///
66/// # Arguments
67/// - `s`: A hexadecimal string representation of a color, i.e., "#DEADBEEF"
68///
69/// # Returns
70/// - If successfully parsed, a `Color::Rgb` or `Color::Rgba`
71///
72/// # Errors
73/// Can return a `ParseIntError` if it fails to convert any of the color channel strings into a
74/// base 16 number
75pub fn parse_rgb(s: &str) -> anyhow::Result<Color> {
76    if !s.starts_with('#') {
77        anyhow::bail!("Invalid hex string: {s}");
78    }
79
80    let hex = s.trim_start_matches('#');
81    let len = hex.len();
82
83    if len != 6 && len != 8 {
84        anyhow::bail!("Hex string must be in the format #RRGGBB or #RRGGBBAA instead of {s}");
85    }
86
87    let red = u8::from_str_radix(&hex[0..2], 16)?;
88    let green = u8::from_str_radix(&hex[2..4], 16)?;
89    let blue = u8::from_str_radix(&hex[4..6], 16)?;
90
91    if len == 8 {
92        let alpha = u8::from_str_radix(&hex[6..8], 16)?;
93        Ok(Color::Rgba {
94            r: red,
95            g: green,
96            b: blue,
97            a: alpha,
98        })
99    } else {
100        Ok(Color::Rgb {
101            r: red,
102            g: green,
103            b: blue,
104        })
105    }
106}
107
108#[must_use]
109/// Takes a foreground and background color and executes a blending algorithm for color smoothing
110///
111/// # Arguments
112/// - `foreground`: The `Color` of the text
113/// - `background`: The `Color` of the terminal background
114///
115/// # Returns
116/// - The calculated blended color
117pub fn blend_color(foreground: Color, background: Color) -> Color {
118    match (foreground, background) {
119        (
120            Color::Rgba { r, g, b, a },
121            Color::Rgb {
122                r: background_r,
123                g: background_g,
124                b: background_b,
125            },
126        ) => {
127            let alpha = f32::from(a) / 255.0;
128            let inv_alpha = 1.0 - alpha;
129
130            let r = (f32::from(r) * alpha + f32::from(background_r) * inv_alpha) as u8;
131            let g = (f32::from(g) * alpha + f32::from(background_g) * inv_alpha) as u8;
132            let b = (f32::from(b) * alpha + f32::from(background_b) * inv_alpha) as u8;
133
134            Color::Rgb { r, g, b }
135        }
136        _ => foreground,
137    }
138}
139
140#[cfg(test)]
141mod test {
142
143    use super::*;
144
145    #[test]
146    fn test_parse_rgb_simple() {
147        let hex_str = "#000000";
148        assert_eq!(parse_rgb(hex_str).unwrap(), Color::Rgb { r: 0, g: 0, b: 0 });
149    }
150
151    #[test]
152    fn test_parse_rgb_complex() {
153        let hex_str = "#d35a87";
154        assert_eq!(
155            parse_rgb(hex_str).unwrap(),
156            Color::Rgb {
157                r: 0xd3,
158                g: 0x5a,
159                b: 0x87
160            }
161        );
162    }
163}