Skip to content

Commit 5f9347f

Browse files
committed
implement FromStr and Display for Color
1 parent 7a97773 commit 5f9347f

File tree

1 file changed

+82
-35
lines changed

1 file changed

+82
-35
lines changed

core/src/color.rs

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1+
use std::{fmt::Display, num::ParseIntError, str::FromStr};
2+
3+
use thiserror::Error;
4+
15
/// 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.
213
#[derive(Debug, Clone, Copy, PartialEq, Default)]
314
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
415
pub struct Color {
@@ -108,41 +119,7 @@ impl Color {
108119
///
109120
/// [`color!`]: crate::color!
110121
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()
146123
}
147124

148125
/// Converts the [`Color`] into its RGBA8 equivalent.
@@ -209,6 +186,76 @@ impl From<[f32; 4]> for Color {
209186
}
210187
}
211188

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+
212259
/// Creates a [`Color`] with shorter and cleaner syntax.
213260
///
214261
/// # Examples

0 commit comments

Comments
 (0)