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