open_sesame/render/
primitives.rs

1//! Primitive rendering utilities
2
3use tiny_skia::{
4    Color as SkiaColor, FillRule, Paint, Path, PathBuilder, Pixmap, Stroke, Transform,
5};
6
7/// RGBA color representation
8#[derive(Debug, Clone, Copy)]
9pub struct Color {
10    /// Red channel (0-255)
11    pub r: u8,
12    /// Green channel (0-255)
13    pub g: u8,
14    /// Blue channel (0-255)
15    pub b: u8,
16    /// Alpha channel (0-255, where 255 is fully opaque)
17    pub a: u8,
18}
19
20impl Color {
21    /// Create a new color from RGBA values
22    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
23        Self { r, g, b, a }
24    }
25
26    /// Create a new color from RGB values (fully opaque)
27    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
28        Self { r, g, b, a: 255 }
29    }
30
31    /// Convert to tiny_skia Color
32    pub fn to_skia(self) -> SkiaColor {
33        SkiaColor::from_rgba8(self.r, self.g, self.b, self.a)
34    }
35
36    /// Create paint from this color
37    pub fn to_paint(self) -> Paint<'static> {
38        let mut paint = Paint::default();
39        paint.set_color(self.to_skia());
40        paint.anti_alias = true;
41        paint
42    }
43}
44
45/// Create a rounded rectangle path
46pub fn rounded_rect(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Option<Path> {
47    let mut pb = PathBuilder::new();
48
49    // Clamp radius to half the smaller dimension
50    let r = radius.min(width / 2.0).min(height / 2.0);
51
52    // Top-left corner
53    pb.move_to(x + r, y);
54
55    // Top edge and top-right corner
56    pb.line_to(x + width - r, y);
57    pb.quad_to(x + width, y, x + width, y + r);
58
59    // Right edge and bottom-right corner
60    pb.line_to(x + width, y + height - r);
61    pb.quad_to(x + width, y + height, x + width - r, y + height);
62
63    // Bottom edge and bottom-left corner
64    pb.line_to(x + r, y + height);
65    pb.quad_to(x, y + height, x, y + height - r);
66
67    // Left edge and back to top-left corner
68    pb.line_to(x, y + r);
69    pb.quad_to(x, y, x + r, y);
70
71    pb.close();
72    pb.finish()
73}
74
75/// Fill a rounded rectangle
76pub fn fill_rounded_rect(
77    pixmap: &mut Pixmap,
78    x: f32,
79    y: f32,
80    width: f32,
81    height: f32,
82    radius: f32,
83    color: Color,
84) {
85    if let Some(path) = rounded_rect(x, y, width, height, radius) {
86        let paint = color.to_paint();
87        pixmap.fill_path(
88            &path,
89            &paint,
90            FillRule::Winding,
91            Transform::identity(),
92            None,
93        );
94    }
95}
96
97/// Stroke a rounded rectangle
98#[allow(clippy::too_many_arguments)]
99pub fn stroke_rounded_rect(
100    pixmap: &mut Pixmap,
101    x: f32,
102    y: f32,
103    width: f32,
104    height: f32,
105    radius: f32,
106    color: Color,
107    stroke_width: f32,
108) {
109    if let Some(path) = rounded_rect(x, y, width, height, radius) {
110        let paint = color.to_paint();
111        let stroke = Stroke {
112            width: stroke_width,
113            ..Default::default()
114        };
115        pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
116    }
117}
118
119/// Fill the entire pixmap with a color
120pub fn fill_background(pixmap: &mut Pixmap, color: Color) {
121    pixmap.fill(color.to_skia());
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_color_creation() {
130        let c = Color::rgba(255, 128, 64, 200);
131        assert_eq!(c.r, 255);
132        assert_eq!(c.g, 128);
133        assert_eq!(c.b, 64);
134        assert_eq!(c.a, 200);
135    }
136
137    #[test]
138    fn test_rounded_rect_creation() {
139        let path = rounded_rect(10.0, 10.0, 100.0, 50.0, 8.0);
140        assert!(path.is_some());
141    }
142
143    #[test]
144    fn test_rounded_rect_clamped_radius() {
145        // Validates clamping when radius exceeds half the minimum dimension
146        let path = rounded_rect(0.0, 0.0, 100.0, 20.0, 50.0);
147        assert!(path.is_some());
148    }
149}