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
This commit is contained in:
Marshall Bowers
2023-11-09 18:27:47 -05:00
committed by GitHub
2 changed files with 103 additions and 30 deletions
+101 -26
View File
@@ -1,8 +1,8 @@
#![allow(dead_code)]
use anyhow::bail;
use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::fmt;
use std::num::ParseIntError;
pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
@@ -19,7 +19,7 @@ pub fn rgba(hex: u32) -> Rgba {
Rgba { r, g, b, a }
}
#[derive(Clone, Copy, Default)]
#[derive(PartialEq, Clone, Copy, Default)]
pub struct Rgba {
pub r: f32,
pub g: f32,
@@ -70,21 +70,7 @@ impl<'de> Visitor<'de> for RgbaVisitor {
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
if value.len() == 7 || value.len() == 9 {
let r = u8::from_str_radix(&value[1..3], 16).unwrap() as f32 / 255.0;
let g = u8::from_str_radix(&value[3..5], 16).unwrap() as f32 / 255.0;
let b = u8::from_str_radix(&value[5..7], 16).unwrap() as f32 / 255.0;
let a = if value.len() == 9 {
u8::from_str_radix(&value[7..9], 16).unwrap() as f32 / 255.0
} else {
1.0
};
Ok(Rgba { r, g, b, a })
} else {
Err(E::custom(
"Bad format for RGBA. Expected #rrggbb or #rrggbbaa.",
))
}
Rgba::try_from(value).map_err(E::custom)
}
}
@@ -125,19 +111,59 @@ impl From<Hsla> for Rgba {
}
impl TryFrom<&'_ str> for Rgba {
type Error = ParseIntError;
type Error = anyhow::Error;
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0;
let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0;
let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0;
let a = if value.len() > 7 {
u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0
} else {
1.0
const RGB: usize = "rgb".len();
const RGBA: usize = "rgba".len();
const RRGGBB: usize = "rrggbb".len();
const RRGGBBAA: usize = "rrggbbaa".len();
const EXPECTED_FORMATS: &'static str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
let Some(("", hex)) = value.trim().split_once('#') else {
bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
};
Ok(Rgba { r, g, b, a })
let (r, g, b, a) = match hex.len() {
RGB | RGBA => {
let r = u8::from_str_radix(&hex[0..1], 16)?;
let g = u8::from_str_radix(&hex[1..2], 16)?;
let b = u8::from_str_radix(&hex[2..3], 16)?;
let a = if hex.len() == RGBA {
u8::from_str_radix(&hex[3..4], 16)?
} else {
0xf
};
/// Duplicates a given hex digit.
/// E.g., `0xf` -> `0xff`.
const fn duplicate(value: u8) -> u8 {
value << 4 | value
}
(duplicate(r), duplicate(g), duplicate(b), duplicate(a))
}
RRGGBB | RRGGBBAA => {
let r = u8::from_str_radix(&hex[0..2], 16)?;
let g = u8::from_str_radix(&hex[2..4], 16)?;
let b = u8::from_str_radix(&hex[4..6], 16)?;
let a = if hex.len() == RRGGBBAA {
u8::from_str_radix(&hex[6..8], 16)?
} else {
0xff
};
(r, g, b, a)
}
_ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
};
Ok(Rgba {
r: r as f32 / 255.,
g: g as f32 / 255.,
b: b as f32 / 255.,
a: a as f32 / 255.,
})
}
}
@@ -311,3 +337,52 @@ impl<'de> Deserialize<'de> for Hsla {
Ok(Hsla::from(rgba))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_deserialize_three_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_four_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_six_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_eight_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
assert_eq!(actual, rgba(0xf5f5f5ff))
}
#[test]
fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
assert_eq!(actual, rgba(0xdeadbeef))
}
}
+2 -4
View File
@@ -1,5 +1,3 @@
use std::num::ParseIntError;
use gpui::{hsla, Hsla, Rgba};
use crate::colors::{StatusColors, SystemColors, ThemeColors};
@@ -413,10 +411,10 @@ struct StaticColorScaleSet {
}
impl TryFrom<StaticColorScaleSet> for ColorScaleSet {
type Error = ParseIntError;
type Error = anyhow::Error;
fn try_from(value: StaticColorScaleSet) -> Result<Self, Self::Error> {
fn to_color_scale(scale: StaticColorScale) -> Result<ColorScale, ParseIntError> {
fn to_color_scale(scale: StaticColorScale) -> Result<ColorScale, anyhow::Error> {
scale
.into_iter()
.map(|color| Rgba::try_from(color).map(Hsla::from))