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}