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}