Skip to main content

anathema_widgets/widget/
style.rs

1use std::str::FromStr;
2
3use anathema_state::{Color, Hex};
4use anathema_value_resolver::ValueKind;
5
6/// The style for a cell in a [`crate::Buffer`]
7/// A style is applied to ever single cell in a [`crate::Buffer`].
8///
9/// Styles do not cascade (and don't behave like CSS).
10/// So giving a style to a parent widget does not automatically apply it to the child.
11///
12/// The following template would draw a red border with white text inside:
13///
14/// ```text
15/// border [foreground: red]:
16///     text: "hi"
17/// ```
18///
19/// In the following example, if the condition is ever true, and then false the text `is_false`
20/// will be rendered with a red foreground.
21///
22/// The way to reset the foreground is to apply `Color::Reset` to the text.
23///
24/// ```text
25/// if [cond: {{ is_true }}]:
26///     text [foreground: red]: "is true"
27/// else:
28///     text: "is false"
29/// ```
30#[derive(Debug, PartialEq, Eq, Copy, Clone)]
31pub struct Style {
32    /// Foreground colour.
33    pub fg: Option<Color>,
34    /// Background colour.
35    pub bg: Option<Color>,
36    /// Attributes.
37    pub attributes: Attributes,
38}
39
40impl Style {
41    /// Create a new instance of a `Style`:
42    pub const fn new() -> Self {
43        Self {
44            fg: None,
45            bg: None,
46            attributes: Attributes::empty(),
47        }
48    }
49
50    /// Create an instance of `Style` from `CellAttributes`.
51    pub fn from_cell_attribs(attributes: &anathema_value_resolver::Attributes<'_>) -> Self {
52        let mut style = Self::new();
53
54        if let Some(color) = attributes.get("foreground") {
55            let color = match color {
56                ValueKind::Hex(Hex { r, g, b }) => Color::from((*r, *g, *b)),
57                ValueKind::Str(cow) => Color::from_str(cow.as_ref()).unwrap_or(Color::Reset),
58                ValueKind::Int(ansi) => Color::AnsiVal(*ansi as u8),
59                ValueKind::Color(c) => *c,
60                _ => Color::Reset,
61            };
62
63            style.fg = Some(color)
64        }
65
66        if let Some(color) = attributes.get("background") {
67            let color = match color {
68                ValueKind::Color(color) => *color,
69                ValueKind::Hex(Hex { r, g, b }) => Color::from((*r, *g, *b)),
70                ValueKind::Str(cow) => Color::from_str(cow.as_ref()).unwrap_or(Color::Reset),
71                ValueKind::Int(ansi) => Color::AnsiVal(*ansi as u8),
72                _ => Color::Reset,
73            };
74
75            style.bg = Some(color)
76        }
77
78        if let Some(true) = attributes.get_as::<bool>("bold") {
79            style.attributes |= Attributes::BOLD;
80        } else if let Some(false) = attributes.get_as::<bool>("bold") {
81            style.attributes |= Attributes::NORMAL;
82        }
83
84        if let Some(true) = attributes.get_as::<bool>("dim") {
85            style.attributes |= Attributes::DIM;
86        } else if let Some(false) = attributes.get_as::<bool>("dim") {
87            style.attributes |= Attributes::NOT_DIM;
88        }
89
90        if let Some(true) = attributes.get_as::<bool>("italic") {
91            style.attributes |= Attributes::ITALIC;
92        } else if let Some(false) = attributes.get_as::<bool>("italic") {
93            style.attributes |= Attributes::NOT_ITALIC;
94        }
95
96        if let Some(true) = attributes.get_as::<bool>("underline") {
97            style.attributes |= Attributes::UNDERLINED;
98        } else if let Some(false) = attributes.get_as::<bool>("underline") {
99            style.attributes |= Attributes::NOT_UNDERLINED;
100        }
101
102        if let Some(true) = attributes.get_as::<bool>("crossed_out") {
103            style.attributes |= Attributes::CROSSED_OUT;
104        } else if let Some(false) = attributes.get_as::<bool>("crossed_out") {
105            style.attributes |= Attributes::NOT_CROSSED_OUT;
106        }
107
108        if let Some(true) = attributes.get_as::<bool>("overline") {
109            style.attributes |= Attributes::OVERLINED;
110        } else if let Some(false) = attributes.get_as::<bool>("overline") {
111            style.attributes |= Attributes::NOT_OVERLINED;
112        }
113
114        if let Some(true) = attributes.get_as::<bool>("inverse") {
115            style.attributes |= Attributes::REVERSED;
116        } else if let Some(false) = attributes.get_as::<bool>("inverse") {
117            style.attributes |= Attributes::NOT_REVERSED;
118        }
119
120        style
121    }
122
123    /// Set the foreground colour
124    pub fn set_fg(&mut self, fg: Color) {
125        self.fg = Some(fg);
126    }
127
128    /// Set the background colour
129    pub fn set_bg(&mut self, bg: Color) {
130        self.bg = Some(bg);
131    }
132
133    /// Set the style to bold
134    pub fn set_bold(&mut self, bold: bool) {
135        if bold {
136            self.attributes |= Attributes::BOLD;
137        } else {
138            self.attributes &= !Attributes::BOLD;
139        }
140    }
141
142    /// Set the style to italic
143    pub fn set_italic(&mut self, italic: bool) {
144        if italic {
145            self.attributes |= Attributes::ITALIC;
146        } else {
147            self.attributes &= !Attributes::ITALIC;
148        }
149    }
150
151    /// Make the cell dim
152    pub fn set_dim(&mut self, dim: bool) {
153        if dim {
154            self.attributes |= Attributes::DIM;
155        } else {
156            self.attributes &= !Attributes::DIM;
157        }
158    }
159
160    /// Make the cell underlined as long as it's supported
161    pub fn set_underlined(&mut self, underlined: bool) {
162        if underlined {
163            self.attributes |= Attributes::UNDERLINED;
164        } else {
165            self.attributes &= !Attributes::UNDERLINED;
166        }
167    }
168
169    /// Make the cell overlined as long as it's supported
170    pub fn set_overlined(&mut self, overlined: bool) {
171        if overlined {
172            self.attributes |= Attributes::OVERLINED;
173        } else {
174            self.attributes &= !Attributes::OVERLINED;
175        }
176    }
177
178    /// Make the cell crossed out as long as it's supported
179    pub fn set_crossed_out(&mut self, crossed_out: bool) {
180        if crossed_out {
181            self.attributes |= Attributes::CROSSED_OUT;
182        } else {
183            self.attributes &= !Attributes::CROSSED_OUT;
184        }
185    }
186
187    /// Invert the foreground and background
188    pub fn set_reversed(&mut self, inverse: bool) {
189        if inverse {
190            self.attributes |= Attributes::REVERSED;
191        } else {
192            self.attributes &= !Attributes::REVERSED;
193        }
194    }
195
196    /// Reset the style
197    pub fn reset() -> Self {
198        let mut style = Self::new();
199        style.fg = Some(Color::Reset);
200        style.bg = Some(Color::Reset);
201        style
202    }
203
204    /// Merge two styles:
205    /// if `self` has no foreground the foreground from the other style is copied to self.
206    /// if `self` has no background the background from the other style is copied to self.
207    pub fn merge(&mut self, other: Style) {
208        if let (None, Some(fg)) = (self.fg, other.fg) {
209            self.fg = Some(fg);
210        }
211
212        if let (None, Some(bg)) = (self.bg, other.bg) {
213            self.bg = Some(bg);
214        }
215
216        self.attributes |= other.attributes;
217    }
218}
219
220bitflags::bitflags! {
221    /// Style attributes
222    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
223    pub struct Attributes: u16 {
224        // Turn style on
225        /// Make the characters bold
226        const BOLD =            0b0000_0000_0000_0001;
227        /// Make the characters dim
228        const DIM =             0b0000_0000_0000_0010;
229        /// Make the characters italic
230        const ITALIC =          0b0000_0000_0000_0100;
231        /// Make the characters underlined
232        const UNDERLINED =      0b0000_0000_0000_1000;
233        /// Make the characters crossed out
234        const CROSSED_OUT =     0b0000_0000_0001_0000;
235        /// Make the characters overlined
236        const OVERLINED =       0b0000_0000_0010_0000;
237        /// Make the characters inverse
238        const REVERSED =        0b0000_0000_0100_0000;
239
240        // Turn style off
241        /// Make the characters not bold
242        const NORMAL =          0b0000_0000_1000_0000;
243        /// Make the characters not dim
244        const NOT_DIM =         0b0000_0001_0000_0000;
245        /// Make the characters not italic
246        const NOT_ITALIC =      0b0000_0010_0000_0000;
247        /// Make the characters not underlined
248        const NOT_UNDERLINED =  0b0000_0100_0000_0000;
249        /// Make the characters not crossed out
250        const NOT_CROSSED_OUT = 0b0000_1000_0000_0000;
251        /// Make the characters not overlined
252        const NOT_OVERLINED =   0b0001_0000_0000_0000;
253        /// Make the characters not inverse
254        const NOT_REVERSED =    0b0010_0000_0000_0000;
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn merging_styles() {
264        let mut right = Style::new();
265        right.set_fg(Color::Green);
266        right.set_bg(Color::Blue);
267
268        let mut left = Style::new();
269        left.set_fg(Color::Red);
270
271        left.merge(right);
272
273        assert_eq!(left.fg.unwrap(), Color::Red);
274        assert_eq!(left.bg.unwrap(), Color::Blue);
275    }
276}