facet_pretty/
color.rs

1//! Color generation utilities for pretty-printing
2
3use std::hash::{DefaultHasher, Hash, Hasher};
4
5/// RGB color representation
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct RGB {
8    /// Red component (0-255)
9    pub r: u8,
10    /// Green component (0-255)
11    pub g: u8,
12    /// Blue component (0-255)
13    pub b: u8,
14}
15
16impl RGB {
17    /// Create a new RGB color
18    pub fn new(r: u8, g: u8, b: u8) -> Self {
19        Self { r, g, b }
20    }
21
22    /// Write the RGB color as ANSI foreground color code to the formatter
23    pub fn write_fg<W: core::fmt::Write>(&self, f: &mut W) -> core::fmt::Result {
24        write!(f, "\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
25    }
26
27    /// Write the RGB color as ANSI background color code to the formatter
28    pub fn write_bg<W: core::fmt::Write>(&self, f: &mut W) -> core::fmt::Result {
29        write!(f, "\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
30    }
31}
32
33/// A color generator that produces unique colors based on a hash value
34pub struct ColorGenerator {
35    base_hue: f32,
36    saturation: f32,
37    lightness: f32,
38}
39
40impl Default for ColorGenerator {
41    fn default() -> Self {
42        Self {
43            base_hue: 210.0,
44            saturation: 0.7,
45            lightness: 0.6,
46        }
47    }
48}
49
50impl ColorGenerator {
51    /// Create a new color generator with default settings
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Set the base hue (0-360)
57    pub fn with_base_hue(mut self, hue: f32) -> Self {
58        self.base_hue = hue;
59        self
60    }
61
62    /// Set the saturation (0.0-1.0)
63    pub fn with_saturation(mut self, saturation: f32) -> Self {
64        self.saturation = saturation.clamp(0.0, 1.0);
65        self
66    }
67
68    /// Set the lightness (0.0-1.0)
69    pub fn with_lightness(mut self, lightness: f32) -> Self {
70        self.lightness = lightness.clamp(0.0, 1.0);
71        self
72    }
73
74    /// Generate an RGB color based on a hash value
75    pub fn generate_color(&self, hash: u64) -> RGB {
76        // Use the hash to generate a hue offset
77        let hue_offset = (hash % 360) as f32;
78        let hue = (self.base_hue + hue_offset) % 360.0;
79
80        // Convert HSL to RGB
81        self.hsl_to_rgb(hue, self.saturation, self.lightness)
82    }
83
84    /// Generate an RGB color based on a hashable value
85    pub fn generate_color_for<T: Hash>(&self, value: &T) -> RGB {
86        let mut hasher = DefaultHasher::new();
87        value.hash(&mut hasher);
88        let hash = hasher.finish();
89        self.generate_color(hash)
90    }
91
92    /// Convert HSL color values to RGB
93    fn hsl_to_rgb(&self, h: f32, s: f32, l: f32) -> RGB {
94        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
95        let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
96        let m = l - c / 2.0;
97
98        let (r, g, b) = match h as u32 {
99            0..=59 => (c, x, 0.0),
100            60..=119 => (x, c, 0.0),
101            120..=179 => (0.0, c, x),
102            180..=239 => (0.0, x, c),
103            240..=299 => (x, 0.0, c),
104            _ => (c, 0.0, x),
105        };
106
107        RGB::new(
108            ((r + m) * 255.0) as u8,
109            ((g + m) * 255.0) as u8,
110            ((b + m) * 255.0) as u8,
111        )
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_color_generator_default() {
121        let generator = ColorGenerator::default();
122        assert_eq!(generator.base_hue, 210.0);
123        assert_eq!(generator.saturation, 0.7);
124        assert_eq!(generator.lightness, 0.6);
125    }
126
127    #[test]
128    fn test_color_generator_with_methods() {
129        let generator = ColorGenerator::new()
130            .with_base_hue(180.0)
131            .with_saturation(0.5)
132            .with_lightness(0.7);
133
134        assert_eq!(generator.base_hue, 180.0);
135        assert_eq!(generator.saturation, 0.5);
136        assert_eq!(generator.lightness, 0.7);
137    }
138
139    #[test]
140    fn test_saturation_clamping() {
141        let generator = ColorGenerator::new().with_saturation(1.5);
142        assert_eq!(generator.saturation, 1.0);
143
144        let generator = ColorGenerator::new().with_saturation(-0.5);
145        assert_eq!(generator.saturation, 0.0);
146    }
147
148    #[test]
149    fn test_lightness_clamping() {
150        let generator = ColorGenerator::new().with_lightness(1.5);
151        assert_eq!(generator.lightness, 1.0);
152
153        let generator = ColorGenerator::new().with_lightness(-0.5);
154        assert_eq!(generator.lightness, 0.0);
155    }
156
157    #[test]
158    fn test_generate_color() {
159        let generator = ColorGenerator::default();
160
161        // Same hash should produce same color
162        let color1 = generator.generate_color(42);
163        let color2 = generator.generate_color(42);
164        assert_eq!(color1, color2);
165
166        // Different hashes should produce different colors
167        let color3 = generator.generate_color(100);
168        assert_ne!(color1, color3);
169    }
170
171    #[test]
172    fn test_generate_color_for() {
173        let generator = ColorGenerator::default();
174
175        // Same value should produce same color
176        let color1 = generator.generate_color_for(&"test");
177        let color2 = generator.generate_color_for(&"test");
178        assert_eq!(color1, color2);
179
180        // Different values should produce different colors
181        let color3 = generator.generate_color_for(&"other");
182        assert_ne!(color1, color3);
183    }
184}