Skip to main content

retroglyph_core/color/
style.rs

1//! Text styling: foreground and background color.
2
3use super::Color;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7/// A style consisting of foreground and background color.
8///
9/// # Examples
10///
11/// ```
12/// use retroglyph_core::color::{Color, Style};
13///
14/// let style = Style::new().fg(Color::GREEN).bg(Color::BLACK);
15/// assert_eq!(style.foreground(), Color::GREEN);
16/// assert_eq!(style.background(), Color::BLACK);
17/// ```
18#[doc(alias = "attr")] // curses-family attribute sets
19pub struct Style {
20    /// Foreground color.
21    ///
22    /// Colors the cell's glyph. A cell that a pixel backend draws as a *sprite* is the one
23    /// exception: a sprite is composited from its own pixels and `fg` does not tint it. See
24    /// [`Surface::put_span`](crate::surface::Surface::put_span).
25    pub(crate) fg: Color,
26    /// Background color.
27    ///
28    /// Fills the cell behind the glyph. Behind a sprite it is still painted, so it shows through
29    /// the sprite's transparent pixels.
30    pub(crate) bg: Color,
31}
32
33impl Style {
34    /// Creates a new style with default values.
35    #[must_use]
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Sets the foreground color, which colors the cell's glyph.
41    ///
42    /// Does not tint a sprite: on a pixel backend, a cell whose glyph resolves to a sprite is
43    /// composited from the sprite's own pixels and ignores this color entirely. The same cell
44    /// drawn by a cell backend falls back to its glyph and *is* colored by it, so one value can
45    /// read very differently across backends. See
46    /// [`Surface::put_span`](crate::surface::Surface::put_span).
47    #[must_use]
48    pub const fn fg(mut self, color: Color) -> Self {
49        self.fg = color;
50        self
51    }
52
53    /// Sets the background color.
54    #[must_use]
55    pub const fn bg(mut self, color: Color) -> Self {
56        self.bg = color;
57        self
58    }
59
60    /// Returns the foreground color.
61    #[must_use]
62    pub const fn foreground(&self) -> Color {
63        self.fg
64    }
65
66    /// Returns the background color.
67    #[must_use]
68    pub const fn background(&self) -> Color {
69        self.bg
70    }
71
72    /// Overlays another style onto this one, only if fields in `other` are non-default.
73    ///
74    /// `Color::Default` in `other` means "unset", not "reset to default": a field left at
75    /// `Color::Default` is skipped, and `self`'s existing value for that field is kept. This
76    /// mirrors ratatui's `Style::patch` convention, so `Style::new().fg(Color::Default)` is a
77    /// no-op when patched onto anything, and there is no way to use `patch` to explicitly clear a
78    /// field back to `Color::Default`; use [`Style::reset_fg`](crate::color::Style::reset_fg) or [`Style::reset_bg`](crate::color::Style::reset_bg) for that.
79    ///
80    /// ```
81    /// use retroglyph_core::color::{Color, Style};
82    ///
83    /// let base = Style::new().fg(Color::RED).bg(Color::BLUE);
84    ///
85    /// // Patching with a default `fg` leaves `base`'s red foreground untouched.
86    /// let patched = base.patch(Style::new().bg(Color::GREEN));
87    /// assert_eq!(patched.foreground(), Color::RED);
88    /// assert_eq!(patched.background(), Color::GREEN);
89    /// ```
90    #[must_use]
91    pub fn patch(mut self, other: Self) -> Self {
92        if other.fg != Color::Default {
93            self.fg = other.fg;
94        }
95        if other.bg != Color::Default {
96            self.bg = other.bg;
97        }
98        self
99    }
100
101    /// Resets the foreground color to `Color::Default`.
102    ///
103    /// Unlike [`Style::patch`](crate::color::Style::patch), which treats `Color::Default` as "leave unset", this explicitly
104    /// clears the field. Use this when a caller needs to undo a previously patched-in foreground
105    /// color rather than merge in a new one.
106    #[must_use]
107    pub const fn reset_fg(mut self) -> Self {
108        self.fg = Color::Default;
109        self
110    }
111
112    /// Resets the background color to `Color::Default`.
113    ///
114    /// Unlike [`Style::patch`](crate::color::Style::patch), which treats `Color::Default` as "leave unset", this explicitly
115    /// clears the field. Use this when a caller needs to undo a previously patched-in background
116    /// color rather than merge in a new one.
117    #[must_use]
118    pub const fn reset_bg(mut self) -> Self {
119        self.bg = Color::Default;
120        self
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_style_builder() {
130        let s = Style::new().fg(Color::RED).bg(Color::BLUE);
131        assert_eq!(s.foreground(), Color::RED);
132        assert_eq!(s.background(), Color::BLUE);
133    }
134
135    #[test]
136    fn test_patch_keeps_non_default_fields() {
137        let base = Style::new().fg(Color::RED).bg(Color::BLUE);
138        let patched = base.patch(Style::new().fg(Color::GREEN));
139        assert_eq!(patched.foreground(), Color::GREEN);
140        assert_eq!(patched.background(), Color::BLUE);
141    }
142
143    #[test]
144    fn test_patch_cannot_reset_a_field_to_default() {
145        let base = Style::new().fg(Color::RED).bg(Color::BLUE);
146        let patched = base.patch(Style::new());
147        assert_eq!(patched.foreground(), Color::RED);
148        assert_eq!(patched.background(), Color::BLUE);
149    }
150
151    #[test]
152    fn test_reset_fg_and_reset_bg_clear_to_default() {
153        let s = Style::new().fg(Color::RED).bg(Color::BLUE);
154        assert_eq!(s.reset_fg().foreground(), Color::Default);
155        assert_eq!(s.reset_bg().background(), Color::Default);
156    }
157
158    #[cfg(feature = "serde")]
159    #[test]
160    fn test_serializes_and_deserializes() {
161        let style = Style::new().fg(Color::RED).bg(Color::Indexed(200));
162        let json = serde_json::to_string(&style).expect("serialize");
163        assert_eq!(json, r#"{"fg":"red","bg":"200"}"#);
164        assert_eq!(
165            serde_json::from_str::<Style>(&json).expect("deserialize"),
166            style
167        );
168    }
169}