facet_pretty/
color.rs

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