Skip to main content

cotis_defaults/
colors.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3// Todo Color Alpha chanel warning for 255 instead of 0 to 1
4/// RGBA color used by default Cotis configs and commands.
5///
6/// Channel values use the `0..=255` range stored as `f32`. Renderers usually
7/// normalize by dividing by `255.0`.
8#[derive(Debug, Clone, Copy, PartialEq)]
9#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10#[repr(C)]
11pub struct Color {
12    /// Red channel in `0..=255`.
13    pub r: f32,
14    /// Green channel in `0..=255`.
15    pub g: f32,
16    /// Blue channel in `0..=255`.
17    pub b: f32,
18    /// Alpha channel in `0..=255`.
19    pub a: f32,
20}
21
22impl Color {
23    /// Creates an opaque color from RGB channels.
24    ///
25    /// Sets alpha to `255.0`.
26    ///
27    /// # Examples
28    ///
29    /// ```rust
30    /// use cotis_defaults::colors::Color;
31    ///
32    /// let color = Color::rgb(128.0, 64.0, 32.0);
33    /// assert_eq!(color.a, 255.0);
34    /// ```
35    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
36        Self { r, g, b, a: 255.0 }
37    }
38    /// Creates a color from RGBA channels.
39    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
40        Self { r, g, b, a }
41    }
42
43    /// Creates an opaque color from `u8` RGB channels.
44    pub const fn u_rgb(r: u8, g: u8, b: u8) -> Self {
45        Self::rgb(r as _, g as _, b as _)
46    }
47    /// Creates a color from `u8` RGBA channels.
48    pub const fn u_rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
49        Self::rgba(r as _, g as _, b as _, a as _)
50    }
51}
52
53impl From<(f32, f32, f32)> for Color {
54    fn from(value: (f32, f32, f32)) -> Self {
55        Self::rgb(value.0, value.1, value.2)
56    }
57}
58impl From<(f32, f32, f32, f32)> for Color {
59    fn from(value: (f32, f32, f32, f32)) -> Self {
60        Self::rgba(value.0, value.1, value.2, value.3)
61    }
62}
63
64impl From<(u8, u8, u8)> for Color {
65    fn from(value: (u8, u8, u8)) -> Self {
66        Self::u_rgb(value.0, value.1, value.2)
67    }
68}
69impl From<(u8, u8, u8, u8)> for Color {
70    fn from(value: (u8, u8, u8, u8)) -> Self {
71        Self::u_rgba(value.0, value.1, value.2, value.3)
72    }
73}
74
75/// Attachment point used by gradient positioning.
76#[derive(Debug, Clone, Copy, PartialEq)]
77#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
78pub enum ColorAttachmentPoint {
79    TopLeft,
80    TopCenter,
81    TopRight,
82    CenterLeft,
83    CenterCenter,
84    CenterRight,
85    BottomLeft,
86    BottomCenter,
87    BottomRight,
88}
89
90/// Position for gradient control points.
91#[derive(Debug, Clone, Copy, PartialEq)]
92#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
93pub struct ColorPos {
94    /// X coordinate (usually normalized).
95    pub x: f32,
96    /// Y coordinate (usually normalized).
97    pub y: f32,
98    /// Anchor used to interpret this position.
99    pub attachment_point: ColorAttachmentPoint,
100}
101
102/// A color and its position along a gradient axis (typically 0.0–1.0).
103#[derive(Debug, Clone, Copy, PartialEq)]
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
105pub struct GradientStop {
106    /// Position in the gradient domain.
107    pub offset: f32,
108    /// Color at this stop.
109    pub color: Color,
110}
111
112/// Linear gradient along the line from `start` to `end` in normalized coordinates (0–1).
113#[derive(Debug, Clone, PartialEq)]
114#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
115pub struct LinearGradient {
116    /// Gradient line start point.
117    pub start: ColorPos,
118    /// Gradient line end point.
119    pub end: ColorPos,
120    /// Stops sampled along the line.
121    pub stops: Vec<GradientStop>,
122}
123
124/// Radial gradient centered at `center` with the given `radius` in normalized coordinates.
125#[derive(Debug, Clone, PartialEq)]
126#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
127pub struct RadialGradient {
128    /// Gradient center.
129    pub center: ColorPos,
130    /// Radius in normalized units.
131    pub radius: f32,
132    /// Gradient color stops.
133    pub stops: Vec<GradientStop>,
134}
135
136/// Rounded rectangle gradient in normalized coordinates (0-1).
137///
138/// `position` is the top-left corner of the rectangle. `width` and `height` define its size.
139/// `corner_radius` is the radius for all corners.
140#[derive(Debug, Clone, PartialEq)]
141#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
142pub struct RoundedRectGradient {
143    /// Top-left position of rounded rectangle.
144    pub position: ColorPos,
145    /// Rectangle width in normalized units.
146    pub width: f32,
147    /// Rectangle height in normalized units.
148    pub height: f32,
149    /// Shared corner radius.
150    pub corner_radius: f32,
151    /// Gradient color stops.
152    pub stops: Vec<GradientStop>,
153}
154
155/// One layer in a [`LayeredColor`]: a solid color or a gradient.
156#[derive(Debug, Clone, PartialEq)]
157#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
158pub enum ColorLayer {
159    /// Solid color fill.
160    Solid(Color),
161    /// Linear gradient fill.
162    Linear(LinearGradient),
163    /// Radial gradient fill.
164    Radial(RadialGradient),
165    /// Rounded-rectangle gradient fill.
166    RoundedRect(RoundedRectGradient),
167    /// Nested multi-layer fill.
168    Layered(LayeredColor),
169}
170
171/// Stack of paints drawn in order.
172#[derive(Debug, Clone, PartialEq)]
173#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
174pub struct LayeredColor {
175    /// Layers rendered in declaration order.
176    pub layers: Vec<ColorLayer>,
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn rgb_sets_opaque_alpha() {
185        let color = Color::rgb(10.0, 20.0, 30.0);
186        assert_eq!(color.a, 255.0);
187        assert_eq!(color.r, 10.0);
188    }
189
190    #[test]
191    fn from_tuple_conversions_set_channels() {
192        let from_float: Color = (1.0, 2.0, 3.0).into();
193        assert_eq!(from_float, Color::rgb(1.0, 2.0, 3.0));
194
195        let from_bytes: Color = (4, 5, 6).into();
196        assert_eq!(from_bytes, Color::u_rgb(4, 5, 6));
197    }
198}