Skip to main content

dotloom_render/
color.rs

1//! Colors and themes.
2
3use serde::{Deserialize, Serialize};
4
5/// Packed `0xRRGGBBAA` color (the scene contract's `Rgba`).
6pub type Rgba = u32;
7
8/// Unpack to straight-alpha components in `0..=1`.
9#[must_use]
10pub fn unpack(c: Rgba) -> [f32; 4] {
11    [
12        ((c >> 24) & 0xff) as f32 / 255.0,
13        ((c >> 16) & 0xff) as f32 / 255.0,
14        ((c >> 8) & 0xff) as f32 / 255.0,
15        (c & 0xff) as f32 / 255.0,
16    ]
17}
18
19/// Pack straight-alpha components.
20#[must_use]
21pub fn pack(c: [f32; 4]) -> Rgba {
22    let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u32;
23    (q(c[0]) << 24) | (q(c[1]) << 16) | (q(c[2]) << 8) | q(c[3])
24}
25
26/// Multiply the alpha channel.
27#[must_use]
28pub fn with_alpha(c: Rgba, factor: f32) -> Rgba {
29    let mut v = unpack(c);
30    v[3] *= factor;
31    pack(v)
32}
33
34/// Convert to the vertex format: premultiplied RGBA8, little-endian `[r, g, b, a]`.
35#[must_use]
36pub fn premul_bytes(c: Rgba) -> u32 {
37    let [r, g, b, a] = unpack(c);
38    let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u32;
39    q(r * a) | (q(g * a) << 8) | (q(b * a) << 16) | (q(a) << 24)
40}
41
42/// Parse `#rgb`, `#rrggbb` or `#rrggbbaa`.
43#[must_use]
44pub fn parse_hex(s: &str) -> Option<Rgba> {
45    let h = s.strip_prefix('#')?;
46    let v = u32::from_str_radix(h, 16).ok()?;
47    match h.len() {
48        3 => {
49            let (r, g, b) = ((v >> 8) & 0xf, (v >> 4) & 0xf, v & 0xf);
50            Some(((r * 17) << 24) | ((g * 17) << 16) | ((b * 17) << 8) | 0xff)
51        }
52        6 => Some((v << 8) | 0xff),
53        8 => Some(v),
54        _ => None,
55    }
56}
57
58/// Renderer colors. Scene color `0` means [`Theme::foreground`].
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase", default)]
61pub struct Theme {
62    /// Canvas background.
63    pub background: Rgba,
64    /// Default stroke/text color (scene color `0`).
65    pub foreground: Rgba,
66    /// Selected items.
67    pub selection: Rgba,
68    /// Hovered item.
69    pub hover: Rgba,
70    /// Items with constraint problems.
71    pub problem: Rgba,
72    /// Minor grid lines.
73    pub grid_minor: Rgba,
74    /// Major grid lines.
75    pub grid_major: Rgba,
76    /// Grid axes through the origin.
77    pub grid_axis: Rgba,
78    /// Snap markers and selection grips.
79    pub marker: Rgba,
80    /// Window selection rectangle.
81    pub marquee_window: Rgba,
82    /// Crossing selection rectangle.
83    pub marquee_crossing: Rgba,
84    /// Construction guides.
85    pub guide: Rgba,
86    /// In-progress drawing preview.
87    pub sketch: Rgba,
88}
89
90impl Default for Theme {
91    fn default() -> Self {
92        Self::light()
93    }
94}
95
96impl Theme {
97    /// Light theme (dark ink on white).
98    #[must_use]
99    pub const fn light() -> Self {
100        Self {
101            background: 0xffff_ffff,
102            foreground: 0x1f23_28ff,
103            selection: 0x0a66_d9ff,
104            hover: 0x4d94_ffff,
105            problem: 0xd12d_2dff,
106            grid_minor: 0xe9ec_f0ff,
107            grid_major: 0xd3d8_deff,
108            grid_axis: 0xb0b8_c2ff,
109            marker: 0xe0_6c00ff,
110            marquee_window: 0x0a66_d9ff,
111            marquee_crossing: 0x1e9e_4aff,
112            guide: 0x8a63_d2ff,
113            sketch: 0x0a66_d9ff,
114        }
115    }
116
117    /// Dark theme (light ink on near-black).
118    #[must_use]
119    pub const fn dark() -> Self {
120        Self {
121            background: 0x1518_1cff,
122            foreground: 0xe6ea_eeff,
123            selection: 0x5aa2_ffff,
124            hover: 0x8bbf_ffff,
125            problem: 0xff6b_6bff,
126            grid_minor: 0x2025_2bff,
127            grid_major: 0x2c33_3bff,
128            grid_axis: 0x4a54_60ff,
129            marker: 0xffa6_33ff,
130            marquee_window: 0x5aa2_ffff,
131            marquee_crossing: 0x4cd3_7aff,
132            guide: 0xb59c_ffff,
133            sketch: 0x5aa2_ffff,
134        }
135    }
136
137    /// Resolve a scene color (`0` = foreground).
138    #[must_use]
139    pub fn resolve(&self, c: Rgba) -> Rgba {
140        if c == 0 { self.foreground } else { c }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn pack_roundtrip_and_hex() {
150        assert_eq!(pack(unpack(0x1234_56ff)), 0x1234_56ff);
151        assert_eq!(parse_hex("#fff"), Some(0xffff_ffff));
152        assert_eq!(parse_hex("#102030"), Some(0x1020_30ff));
153        assert_eq!(parse_hex("#10203040"), Some(0x1020_3040));
154        assert_eq!(parse_hex("102030"), None);
155    }
156
157    #[test]
158    fn premultiplied_layout() {
159        // 50% red: r = 0.5 after premultiplication, stored first.
160        let v = premul_bytes(0xff00_0080);
161        assert_eq!(v & 0xff, 128);
162        assert_eq!(v >> 24, 128);
163        assert_eq!((v >> 8) & 0xff, 0);
164    }
165}