1use ratatui_core::buffer::Cell;
4use ratatui_core::style::{Color, Modifier};
5
6use crate::color::{ColorDepth, Rgb};
7use crate::geometry::Padding;
8use crate::theme::{Paint, PropValue, StyleProps};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub struct CellStyle {
13 pub fg: Option<Rgb>,
15 pub bg: Option<Rgb>,
17 pub bold: bool,
19 pub italic: bool,
21 pub underline: bool,
23 pub dim: bool,
25}
26
27impl CellStyle {
28 #[must_use]
30 pub fn fg(color: Rgb) -> Self {
31 Self { fg: Some(color), ..Self::default() }
32 }
33
34 #[must_use]
36 pub fn on(mut self, color: Rgb) -> Self {
37 self.bg = Some(color);
38 self
39 }
40
41 #[must_use]
43 pub fn with_bold(mut self, bold: bool) -> Self {
44 self.bold = bold;
45 self
46 }
47
48 #[cfg(test)]
50 pub(crate) fn apply(self, cell: &mut Cell, depth: ColorDepth) {
51 self.for_depth(depth).apply(cell);
52 }
53
54 pub(crate) fn for_depth(self, depth: ColorDepth) -> CellPaint {
56 let mut modifier = Modifier::empty();
57 modifier.set(Modifier::BOLD, self.bold);
58 modifier.set(Modifier::ITALIC, self.italic);
59 modifier.set(Modifier::UNDERLINED, self.underline);
60 modifier.set(Modifier::DIM, self.dim);
61 CellPaint { fg: self.fg.map(|fg| to_color(fg, depth)), bg: self.bg.map(|bg| to_color(bg, depth)), modifier }
62 }
63}
64
65#[derive(Debug, Clone, Copy)]
69pub(crate) struct CellPaint {
70 fg: Option<Color>,
71 bg: Option<Color>,
72 modifier: Modifier,
73}
74
75impl CellPaint {
76 pub(crate) fn apply(self, cell: &mut Cell) {
78 if let Some(fg) = self.fg {
79 cell.fg = fg;
80 }
81 if let Some(bg) = self.bg {
82 cell.bg = bg;
83 }
84 cell.modifier = self.modifier;
85 }
86}
87
88pub(crate) fn to_color(color: Rgb, depth: ColorDepth) -> Color {
90 match depth {
91 ColorDepth::TrueColor => Color::Rgb(color.r, color.g, color.b),
92 ColorDepth::Ansi256 => Color::Indexed(color.to_ansi256()),
93 ColorDepth::Ansi16 => Color::Indexed(color.to_ansi16()),
94 }
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub struct WidgetStyle {
100 props: StyleProps,
101 phase: f32,
102}
103
104impl WidgetStyle {
105 pub(crate) fn new(props: StyleProps, phase: f32) -> Self {
106 Self { props, phase }
107 }
108
109 #[must_use]
112 pub(crate) fn without(mut self, key: &str) -> Self {
113 self.props.remove(key);
114 self
115 }
116
117 #[must_use]
119 pub fn color(&self, key: &str) -> Option<Rgb> {
120 self.props.paint(key).map(|paint: Paint| paint.at(self.phase))
121 }
122
123 #[must_use]
125 pub fn flag(&self, key: &str) -> bool {
126 self.props.flag(key)
127 }
128
129 #[must_use]
131 pub fn cells(&self, key: &str) -> Option<u16> {
132 self.props.cells(key)
133 }
134
135 #[must_use]
137 pub fn word(&self, key: &str) -> Option<&'static str> {
138 self.props.word(key)
139 }
140
141 #[must_use]
143 pub fn padding(&self) -> Padding {
144 match self.props.get("padding") {
145 Some(PropValue::Pair(v, h)) => Padding::symmetric(v, h),
146 Some(PropValue::Cells(n)) => Padding::all(n),
147 _ => Padding::default(),
148 }
149 }
150
151 #[must_use]
153 pub fn text(&self) -> CellStyle {
154 CellStyle {
155 fg: self.color("fg"),
156 bg: self.color("bg"),
157 bold: self.flag("bold"),
158 italic: self.flag("italic"),
159 underline: self.flag("underline"),
160 dim: self.flag("dim"),
161 }
162 }
163
164 #[must_use]
166 pub fn is_animated(&self) -> bool {
167 self.props.is_animated()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::theme::ThemeRegistry;
175
176 #[test]
177 fn applies_colours_for_each_depth() {
178 let mut cell = Cell::default();
179 CellStyle::fg(Rgb::new(255, 0, 0))
180 .on(Rgb::new(0, 0, 0))
181 .with_bold(true)
182 .apply(&mut cell, ColorDepth::TrueColor);
183 assert_eq!(cell.fg, Color::Rgb(255, 0, 0));
184 assert!(cell.modifier.contains(Modifier::BOLD));
185 CellStyle::fg(Rgb::new(255, 0, 0)).apply(&mut cell, ColorDepth::Ansi256);
186 assert_eq!(cell.fg, Color::Indexed(196));
187 assert_eq!(cell.bg, Color::Rgb(0, 0, 0));
188 assert!(!cell.modifier.contains(Modifier::BOLD));
189 }
190
191 #[test]
192 fn resolves_theme_properties() {
193 let (theme, _) = ThemeRegistry::builtin().resolve_or_default("monochrome");
194 let style = WidgetStyle::new(theme.style("button", Some("primary"), &[]), 0.0);
195 assert_ne!(style.text().bg, theme.color("accent"));
197 assert_eq!(style.text().fg, theme.color("accent"));
198 assert!(style.text().bold);
199 assert_eq!(style.padding(), Padding::symmetric(0, 2));
200 }
201}