tinterm/
color.rs

1/// Represents an RGB color with red, green, and blue components.
2///
3/// Each component is a value between 0 and 255, where:
4/// - 0 represents no intensity
5/// - 255 represents full intensity
6///
7/// # Examples
8///
9/// ```
10/// use tinterm::Color;
11///
12/// // Create a red color
13/// let red = Color::new(255, 0, 0);
14///
15/// // Use predefined colors
16/// let blue = Color::BLUE;
17///
18/// // Create from hex string
19/// let orange = Color::from_hex("#FF8000").unwrap();
20/// ```
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Color {
23    /// Red component (0-255)
24    pub r: u8,
25    /// Green component (0-255)
26    pub g: u8,
27    /// Blue component (0-255)
28    pub b: u8,
29}
30
31impl Color {
32    /// Creates a new Color with the specified RGB values.
33    ///
34    /// # Arguments
35    ///
36    /// * `r` - Red component (0-255)
37    /// * `g` - Green component (0-255)
38    /// * `b` - Blue component (0-255)
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use tinterm::Color;
44    ///
45    /// let purple = Color::new(128, 0, 128);
46    /// ```
47    pub fn new(r: u8, g: u8, b: u8) -> Self {
48        Color { r, g, b }
49    }
50
51    /// Creates a new Color with the specified RGB values.
52    ///
53    /// This is an alias for [`Color::new`].
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use tinterm::Color;
59    ///
60    /// let cyan = Color::rgb(0, 255, 255);
61    /// ```
62    pub fn rgb(r: u8, g: u8, b: u8) -> Self {
63        Color { r, g, b }
64    }
65
66    pub(crate) fn interpolate(&self, other: &Color, t: f32) -> Color {
67        Color {
68            r: (self.r as f32 + (other.r as f32 - self.r as f32) * t) as u8,
69            g: (self.g as f32 + (other.g as f32 - self.g as f32) * t) as u8,
70            b: (self.b as f32 + (other.b as f32 - self.b as f32) * t) as u8,
71        }
72    }
73
74    /// Creates a new Color from a hexadecimal color string.
75    ///
76    /// Supports both 6-digit (e.g., "#FF0000" or "FF0000") and 3-digit (e.g., "#F00" or "F00") formats.
77    /// The '#' prefix is optional.
78    ///
79    /// # Arguments
80    ///
81    /// * `hex` - A hex color string
82    ///
83    /// # Returns
84    ///
85    /// * `Ok(Color)` - If the hex string is valid
86    /// * `Err(&str)` - If the hex string is invalid
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use tinterm::Color;
92    ///
93    /// let red = Color::from_hex("#FF0000").unwrap();
94    /// let green = Color::from_hex("00FF00").unwrap();
95    /// let blue = Color::from_hex("#00F").unwrap();
96    /// ```
97    pub fn from_hex(hex: &str) -> Result<Self, &'static str> {
98        // Remove '#' if present
99        let hex = hex.trim_start_matches('#');
100
101        // Validate hex length
102        if hex.len() != 6 && hex.len() != 3 {
103            return Err(
104                "Invalid hex color length. Expected 6 or 3 characters (e.g., FF0000 or F00)",
105            );
106        }
107
108        // Validate hex characters
109        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
110            return Err("Invalid hex color format. Expected hexadecimal characters (0-9, A-F)");
111        }
112
113        let (r, g, b) = if hex.len() == 3 {
114            // Convert 3-digit format to 6-digit by duplicating each digit
115            let r = u8::from_str_radix(
116                &format!(
117                    "{}{}",
118                    hex.chars().nth(0).unwrap(),
119                    hex.chars().nth(0).unwrap()
120                ),
121                16,
122            )
123            .map_err(|_| "Failed to parse red component")?;
124            let g = u8::from_str_radix(
125                &format!(
126                    "{}{}",
127                    hex.chars().nth(1).unwrap(),
128                    hex.chars().nth(1).unwrap()
129                ),
130                16,
131            )
132            .map_err(|_| "Failed to parse green component")?;
133            let b = u8::from_str_radix(
134                &format!(
135                    "{}{}",
136                    hex.chars().nth(2).unwrap(),
137                    hex.chars().nth(2).unwrap()
138                ),
139                16,
140            )
141            .map_err(|_| "Failed to parse blue component")?;
142            (r, g, b)
143        } else {
144            // Parse 6-digit format
145            let r =
146                u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Failed to parse red component")?;
147            let g = u8::from_str_radix(&hex[2..4], 16)
148                .map_err(|_| "Failed to parse green component")?;
149            let b =
150                u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Failed to parse blue component")?;
151            (r, g, b)
152        };
153
154        Ok(Color { r, g, b })
155    }
156
157    /// Converts the color to a hexadecimal string representation.
158    ///
159    /// Returns a 6-digit uppercase hex string with '#' prefix.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use tinterm::Color;
165    ///
166    /// let red = Color::new(255, 0, 0);
167    /// assert_eq!(red.to_hex(), "#FF0000");
168    /// ```
169    pub fn to_hex(&self) -> String {
170        format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
171    }
172}