|
| 1 | +use std::{fmt::Display, num::ParseIntError, str::FromStr}; |
| 2 | + |
| 3 | +use thiserror::Error; |
| 4 | + |
1 | 5 | /// A color in the `sRGB` color space.
|
| 6 | +/// |
| 7 | +/// # String Representation |
| 8 | +/// |
| 9 | +/// A color can be represented in either of the following valid formats: `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`. |
| 10 | +/// Both uppercase and lowercase letters are supported. |
| 11 | +/// |
| 12 | +/// If `a` (transparency) is not specified, `1.0` (completely opaque) would be used by default. |
2 | 13 | #[derive(Debug, Clone, Copy, PartialEq, Default)]
|
3 | 14 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
4 | 15 | pub struct Color {
|
@@ -108,41 +119,7 @@ impl Color {
|
108 | 119 | ///
|
109 | 120 | /// [`color!`]: crate::color!
|
110 | 121 | pub fn parse(s: &str) -> Option<Color> {
|
111 |
| - let hex = s.strip_prefix('#').unwrap_or(s); |
112 |
| - |
113 |
| - let parse_channel = |from: usize, to: usize| { |
114 |
| - let num = |
115 |
| - usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0; |
116 |
| - |
117 |
| - // If we only got half a byte (one letter), expand it into a full byte (two letters) |
118 |
| - Some(if from == to { num + num * 16.0 } else { num }) |
119 |
| - }; |
120 |
| - |
121 |
| - Some(match hex.len() { |
122 |
| - 3 => Color::from_rgb( |
123 |
| - parse_channel(0, 0)?, |
124 |
| - parse_channel(1, 1)?, |
125 |
| - parse_channel(2, 2)?, |
126 |
| - ), |
127 |
| - 4 => Color::from_rgba( |
128 |
| - parse_channel(0, 0)?, |
129 |
| - parse_channel(1, 1)?, |
130 |
| - parse_channel(2, 2)?, |
131 |
| - parse_channel(3, 3)?, |
132 |
| - ), |
133 |
| - 6 => Color::from_rgb( |
134 |
| - parse_channel(0, 1)?, |
135 |
| - parse_channel(2, 3)?, |
136 |
| - parse_channel(4, 5)?, |
137 |
| - ), |
138 |
| - 8 => Color::from_rgba( |
139 |
| - parse_channel(0, 1)?, |
140 |
| - parse_channel(2, 3)?, |
141 |
| - parse_channel(4, 5)?, |
142 |
| - parse_channel(6, 7)?, |
143 |
| - ), |
144 |
| - _ => None?, |
145 |
| - }) |
| 122 | + s.parse().ok() |
146 | 123 | }
|
147 | 124 |
|
148 | 125 | /// Converts the [`Color`] into its RGBA8 equivalent.
|
@@ -209,6 +186,76 @@ impl From<[f32; 4]> for Color {
|
209 | 186 | }
|
210 | 187 | }
|
211 | 188 |
|
| 189 | +/// An error which can be returned when parsing color from an RGB hexadecimal string. |
| 190 | +/// |
| 191 | +/// See [`Color`] for specifications for the string. |
| 192 | +#[derive(Debug, Error)] |
| 193 | +pub enum ParseColorError { |
| 194 | + /// The string could not be parsed to valid integers. |
| 195 | + #[error(transparent)] |
| 196 | + ParseIntError(#[from] ParseIntError), |
| 197 | + /// The string is of invalid length. |
| 198 | + #[error( |
| 199 | + "expected hex string of length 3, 4, 6 or 8 excluding optional prefix '#', found {0}" |
| 200 | + )] |
| 201 | + InvalidLength(usize), |
| 202 | +} |
| 203 | + |
| 204 | +impl FromStr for Color { |
| 205 | + type Err = ParseColorError; |
| 206 | + |
| 207 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 208 | + let hex = s.strip_prefix('#').unwrap_or(s); |
| 209 | + |
| 210 | + let parse_channel = |
| 211 | + |from: usize, to: usize| -> Result<f32, ParseIntError> { |
| 212 | + let num = |
| 213 | + usize::from_str_radix(&hex[from..=to], 16)? as f32 / 255.0; |
| 214 | + |
| 215 | + // If we only got half a byte (one letter), expand it into a full byte (two letters) |
| 216 | + Ok(if from == to { num + num * 16.0 } else { num }) |
| 217 | + }; |
| 218 | + |
| 219 | + let val = match hex.len() { |
| 220 | + 3 => Color::from_rgb( |
| 221 | + parse_channel(0, 0)?, |
| 222 | + parse_channel(1, 1)?, |
| 223 | + parse_channel(2, 2)?, |
| 224 | + ), |
| 225 | + 4 => Color::from_rgba( |
| 226 | + parse_channel(0, 0)?, |
| 227 | + parse_channel(1, 1)?, |
| 228 | + parse_channel(2, 2)?, |
| 229 | + parse_channel(3, 3)?, |
| 230 | + ), |
| 231 | + 6 => Color::from_rgb( |
| 232 | + parse_channel(0, 1)?, |
| 233 | + parse_channel(2, 3)?, |
| 234 | + parse_channel(4, 5)?, |
| 235 | + ), |
| 236 | + 8 => Color::from_rgba( |
| 237 | + parse_channel(0, 1)?, |
| 238 | + parse_channel(2, 3)?, |
| 239 | + parse_channel(4, 5)?, |
| 240 | + parse_channel(6, 7)?, |
| 241 | + ), |
| 242 | + _ => return Err(ParseColorError::InvalidLength(hex.len())), |
| 243 | + }; |
| 244 | + |
| 245 | + Ok(val) |
| 246 | + } |
| 247 | +} |
| 248 | + |
| 249 | +impl Display for Color { |
| 250 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 251 | + let [r, g, b, a] = self.into_rgba8(); |
| 252 | + if self.a == 1.0 { |
| 253 | + return write!(f, "#{r:02x}{g:02x}{b:02x}"); |
| 254 | + } |
| 255 | + write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}") |
| 256 | + } |
| 257 | +} |
| 258 | + |
212 | 259 | /// Creates a [`Color`] with shorter and cleaner syntax.
|
213 | 260 | ///
|
214 | 261 | /// # Examples
|
|
0 commit comments