Skip to content

Commit aed59ba

Browse files
authored
Merge pull request #2486 from vladh/add-color-from-hex
Add `Color::parse`
2 parents 2eb1cc5 + 7901d47 commit aed59ba

1 file changed

Lines changed: 112 additions & 15 deletions

File tree

core/src/color.rs

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,53 @@ impl Color {
108108
}
109109
}
110110

111+
/// Parses a [`Color`] from a hex string.
112+
///
113+
/// Supported formats are `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`.
114+
/// The starting "#" is optional. Both uppercase and lowercase are supported.
115+
///
116+
/// If you have a static color string, using the [`color!`] macro should be preferred
117+
/// since it leverages hexadecimal literal notation and arithmetic directly.
118+
///
119+
/// [`color!`]: crate::color!
120+
pub fn parse(s: &str) -> Option<Color> {
121+
let hex = s.strip_prefix('#').unwrap_or(s);
122+
123+
let parse_channel = |from: usize, to: usize| {
124+
let num =
125+
usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0;
126+
127+
// If we only got half a byte (one letter), expand it into a full byte (two letters)
128+
Some(if from == to { num + num * 16.0 } else { num })
129+
};
130+
131+
Some(match hex.len() {
132+
3 => Color::from_rgb(
133+
parse_channel(0, 0)?,
134+
parse_channel(1, 1)?,
135+
parse_channel(2, 2)?,
136+
),
137+
4 => Color::from_rgba(
138+
parse_channel(0, 0)?,
139+
parse_channel(1, 1)?,
140+
parse_channel(2, 2)?,
141+
parse_channel(3, 3)?,
142+
),
143+
6 => Color::from_rgb(
144+
parse_channel(0, 1)?,
145+
parse_channel(2, 3)?,
146+
parse_channel(4, 5)?,
147+
),
148+
8 => Color::from_rgba(
149+
parse_channel(0, 1)?,
150+
parse_channel(2, 3)?,
151+
parse_channel(4, 5)?,
152+
parse_channel(6, 7)?,
153+
),
154+
_ => None?,
155+
})
156+
}
157+
111158
/// Converts the [`Color`] into its RGBA8 equivalent.
112159
#[must_use]
113160
pub fn into_rgba8(self) -> [u8; 4] {
@@ -178,34 +225,65 @@ impl From<[f32; 4]> for Color {
178225
///
179226
/// ```
180227
/// # use iced_core::{Color, color};
181-
/// assert_eq!(color!(0, 0, 0), Color::from_rgb(0., 0., 0.));
182-
/// assert_eq!(color!(0, 0, 0, 0.), Color::from_rgba(0., 0., 0., 0.));
183-
/// assert_eq!(color!(0xffffff), Color::from_rgb(1., 1., 1.));
184-
/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1., 1., 1., 0.));
228+
/// assert_eq!(color!(0, 0, 0), Color::BLACK);
229+
/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT);
230+
/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0));
231+
/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0));
232+
/// assert_eq!(color!(0x123), Color::from_rgba8(0x11, 0x22, 0x33, 1.0));
233+
/// assert_eq!(color!(0x123), color!(0x112233));
185234
/// ```
186235
#[macro_export]
187236
macro_rules! color {
188237
($r:expr, $g:expr, $b:expr) => {
189238
color!($r, $g, $b, 1.0)
190239
};
191-
($r:expr, $g:expr, $b:expr, $a:expr) => {
192-
$crate::Color {
193-
r: $r as f32 / 255.0,
194-
g: $g as f32 / 255.0,
195-
b: $b as f32 / 255.0,
196-
a: $a,
240+
($r:expr, $g:expr, $b:expr, $a:expr) => {{
241+
let r = $r as f32 / 255.0;
242+
let g = $g as f32 / 255.0;
243+
let b = $b as f32 / 255.0;
244+
245+
#[allow(clippy::manual_range_contains)]
246+
{
247+
debug_assert!(
248+
r >= 0.0 && r <= 1.0,
249+
"R channel must be in [0, 255] range."
250+
);
251+
debug_assert!(
252+
g >= 0.0 && g <= 1.0,
253+
"G channel must be in [0, 255] range."
254+
);
255+
debug_assert!(
256+
b >= 0.0 && b <= 1.0,
257+
"B channel must be in [0, 255] range."
258+
);
197259
}
198-
};
260+
261+
$crate::Color { r, g, b, a: $a }
262+
}};
199263
($hex:expr) => {{
200264
color!($hex, 1.0)
201265
}};
202266
($hex:expr, $a:expr) => {{
203267
let hex = $hex as u32;
204-
let r = (hex & 0xff0000) >> 16;
205-
let g = (hex & 0xff00) >> 8;
206-
let b = (hex & 0xff);
207268

208-
color!(r, g, b, $a)
269+
if hex <= 0xfff {
270+
let r = (hex & 0xf00) >> 8;
271+
let g = (hex & 0x0f0) >> 4;
272+
let b = (hex & 0x00f);
273+
274+
color!((r << 4 | r), (g << 4 | g), (b << 4 | b), $a)
275+
} else {
276+
debug_assert!(
277+
hex <= 0xffffff,
278+
"color! value must not exceed 0xffffff"
279+
);
280+
281+
let r = (hex & 0xff0000) >> 16;
282+
let g = (hex & 0xff00) >> 8;
283+
let b = (hex & 0xff);
284+
285+
color!(r, g, b, $a)
286+
}
209287
}};
210288
}
211289

@@ -273,4 +351,23 @@ mod tests {
273351
assert_relative_eq!(result.b, 0.3);
274352
assert_relative_eq!(result.a, 1.0);
275353
}
354+
355+
#[test]
356+
fn parse() {
357+
let tests = [
358+
("#ff0000", [255, 0, 0, 255]),
359+
("00ff0080", [0, 255, 0, 128]),
360+
("#F80", [255, 136, 0, 255]),
361+
("#00f1", [0, 0, 255, 17]),
362+
];
363+
364+
for (arg, expected) in tests {
365+
assert_eq!(
366+
Color::parse(arg).expect("color must parse").into_rgba8(),
367+
expected
368+
);
369+
}
370+
371+
assert!(Color::parse("invalid").is_none());
372+
}
276373
}

0 commit comments

Comments
 (0)