Skip to content

Commit 8bd02fd

Browse files
authored
Extend RGBA hex color parsing to support 3-value and 4-value variants (#3295)
This PR extends our support for parsing hex color codes to `Rgba` to additionally support 3-value (`#rgb`) and 4-value (`#rgba`) formats. See [here](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color) for more details on these hex color variants. Release Notes: - N/A
2 parents 2347576 + 82861e3 commit 8bd02fd

File tree

2 files changed

+103
-30
lines changed

2 files changed

+103
-30
lines changed

crates/gpui2/src/color.rs

Lines changed: 101 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#![allow(dead_code)]
22

3+
use anyhow::bail;
34
use serde::de::{self, Deserialize, Deserializer, Visitor};
45
use std::fmt;
5-
use std::num::ParseIntError;
66

77
pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
88
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
@@ -19,7 +19,7 @@ pub fn rgba(hex: u32) -> Rgba {
1919
Rgba { r, g, b, a }
2020
}
2121

22-
#[derive(Clone, Copy, Default)]
22+
#[derive(PartialEq, Clone, Copy, Default)]
2323
pub struct Rgba {
2424
pub r: f32,
2525
pub g: f32,
@@ -70,21 +70,7 @@ impl<'de> Visitor<'de> for RgbaVisitor {
7070
}
7171

7272
fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
73-
if value.len() == 7 || value.len() == 9 {
74-
let r = u8::from_str_radix(&value[1..3], 16).unwrap() as f32 / 255.0;
75-
let g = u8::from_str_radix(&value[3..5], 16).unwrap() as f32 / 255.0;
76-
let b = u8::from_str_radix(&value[5..7], 16).unwrap() as f32 / 255.0;
77-
let a = if value.len() == 9 {
78-
u8::from_str_radix(&value[7..9], 16).unwrap() as f32 / 255.0
79-
} else {
80-
1.0
81-
};
82-
Ok(Rgba { r, g, b, a })
83-
} else {
84-
Err(E::custom(
85-
"Bad format for RGBA. Expected #rrggbb or #rrggbbaa.",
86-
))
87-
}
73+
Rgba::try_from(value).map_err(E::custom)
8874
}
8975
}
9076

@@ -125,19 +111,59 @@ impl From<Hsla> for Rgba {
125111
}
126112

127113
impl TryFrom<&'_ str> for Rgba {
128-
type Error = ParseIntError;
114+
type Error = anyhow::Error;
129115

130116
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
131-
let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0;
132-
let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0;
133-
let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0;
134-
let a = if value.len() > 7 {
135-
u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0
136-
} else {
137-
1.0
117+
const RGB: usize = "rgb".len();
118+
const RGBA: usize = "rgba".len();
119+
const RRGGBB: usize = "rrggbb".len();
120+
const RRGGBBAA: usize = "rrggbbaa".len();
121+
122+
const EXPECTED_FORMATS: &'static str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
123+
124+
let Some(("", hex)) = value.trim().split_once('#') else {
125+
bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
126+
};
127+
128+
let (r, g, b, a) = match hex.len() {
129+
RGB | RGBA => {
130+
let r = u8::from_str_radix(&hex[0..1], 16)?;
131+
let g = u8::from_str_radix(&hex[1..2], 16)?;
132+
let b = u8::from_str_radix(&hex[2..3], 16)?;
133+
let a = if hex.len() == RGBA {
134+
u8::from_str_radix(&hex[3..4], 16)?
135+
} else {
136+
0xf
137+
};
138+
139+
/// Duplicates a given hex digit.
140+
/// E.g., `0xf` -> `0xff`.
141+
const fn duplicate(value: u8) -> u8 {
142+
value << 4 | value
143+
}
144+
145+
(duplicate(r), duplicate(g), duplicate(b), duplicate(a))
146+
}
147+
RRGGBB | RRGGBBAA => {
148+
let r = u8::from_str_radix(&hex[0..2], 16)?;
149+
let g = u8::from_str_radix(&hex[2..4], 16)?;
150+
let b = u8::from_str_radix(&hex[4..6], 16)?;
151+
let a = if hex.len() == RRGGBBAA {
152+
u8::from_str_radix(&hex[6..8], 16)?
153+
} else {
154+
0xff
155+
};
156+
(r, g, b, a)
157+
}
158+
_ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
138159
};
139160

140-
Ok(Rgba { r, g, b, a })
161+
Ok(Rgba {
162+
r: r as f32 / 255.,
163+
g: g as f32 / 255.,
164+
b: b as f32 / 255.,
165+
a: a as f32 / 255.,
166+
})
141167
}
142168
}
143169

@@ -311,3 +337,52 @@ impl<'de> Deserialize<'de> for Hsla {
311337
Ok(Hsla::from(rgba))
312338
}
313339
}
340+
341+
#[cfg(test)]
342+
mod tests {
343+
use serde_json::json;
344+
345+
use super::*;
346+
347+
#[test]
348+
fn test_deserialize_three_value_hex_to_rgba() {
349+
let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
350+
351+
assert_eq!(actual, rgba(0xff0099ff))
352+
}
353+
354+
#[test]
355+
fn test_deserialize_four_value_hex_to_rgba() {
356+
let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
357+
358+
assert_eq!(actual, rgba(0xff0099ff))
359+
}
360+
361+
#[test]
362+
fn test_deserialize_six_value_hex_to_rgba() {
363+
let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
364+
365+
assert_eq!(actual, rgba(0xff0099ff))
366+
}
367+
368+
#[test]
369+
fn test_deserialize_eight_value_hex_to_rgba() {
370+
let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
371+
372+
assert_eq!(actual, rgba(0xff0099ff))
373+
}
374+
375+
#[test]
376+
fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
377+
let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
378+
379+
assert_eq!(actual, rgba(0xf5f5f5ff))
380+
}
381+
382+
#[test]
383+
fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
384+
let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
385+
386+
assert_eq!(actual, rgba(0xdeadbeef))
387+
}
388+
}

crates/theme2/src/default_colors.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::num::ParseIntError;
2-
31
use gpui::{hsla, Hsla, Rgba};
42

53
use crate::colors::{StatusColors, SystemColors, ThemeColors};
@@ -413,10 +411,10 @@ struct StaticColorScaleSet {
413411
}
414412

415413
impl TryFrom<StaticColorScaleSet> for ColorScaleSet {
416-
type Error = ParseIntError;
414+
type Error = anyhow::Error;
417415

418416
fn try_from(value: StaticColorScaleSet) -> Result<Self, Self::Error> {
419-
fn to_color_scale(scale: StaticColorScale) -> Result<ColorScale, ParseIntError> {
417+
fn to_color_scale(scale: StaticColorScale) -> Result<ColorScale, anyhow::Error> {
420418
scale
421419
.into_iter()
422420
.map(|color| Rgba::try_from(color).map(Hsla::from))

0 commit comments

Comments
 (0)