1use std::collections::HashMap;
4
5use ratatui_core::buffer::{Buffer, Cell};
6use ratatui_core::style::{Color, Modifier};
7
8use crate::color::{ColorDepth, Rgb};
9use crate::geometry::Padding;
10use crate::theme::{Paint, PropValue, StyleProps};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub struct CellStyle {
15 pub fg: Option<Rgb>,
17 pub bg: Option<Rgb>,
19 pub bold: bool,
21 pub italic: bool,
23 pub underline: bool,
25 pub dim: bool,
27}
28
29impl CellStyle {
30 #[must_use]
32 pub fn fg(color: Rgb) -> Self {
33 Self { fg: Some(color), ..Self::default() }
34 }
35
36 #[must_use]
38 pub fn on(mut self, color: Rgb) -> Self {
39 self.bg = Some(color);
40 self
41 }
42
43 #[must_use]
45 pub fn with_bold(mut self, bold: bool) -> Self {
46 self.bold = bold;
47 self
48 }
49
50 #[cfg(test)]
52 pub(crate) fn apply(self, cell: &mut Cell) {
53 self.paint().apply(cell);
54 }
55
56 pub(crate) fn paint(self) -> CellPaint {
58 let mut modifier = Modifier::empty();
59 modifier.set(Modifier::BOLD, self.bold);
60 modifier.set(Modifier::ITALIC, self.italic);
61 modifier.set(Modifier::UNDERLINED, self.underline);
62 modifier.set(Modifier::DIM, self.dim);
63 CellPaint { fg: self.fg.map(to_color), bg: self.bg.map(to_color), modifier }
64 }
65}
66
67#[derive(Debug, Clone, Copy)]
70pub(crate) struct CellPaint {
71 fg: Option<Color>,
72 bg: Option<Color>,
73 modifier: Modifier,
74}
75
76impl CellPaint {
77 pub(crate) fn apply(self, cell: &mut Cell) {
79 if let Some(fg) = self.fg {
80 cell.fg = fg;
81 }
82 if let Some(bg) = self.bg {
83 cell.bg = bg;
84 }
85 cell.modifier = self.modifier;
86 }
87}
88
89pub(crate) fn to_color(color: Rgb) -> Color {
96 Color::Rgb(color.r, color.g, color.b)
97}
98
99pub(crate) fn reduce(buf: &mut Buffer, depth: ColorDepth, ground: Rgb) {
107 match depth {
108 ColorDepth::TrueColor => {}
109 ColorDepth::Ansi256 => reduce_with(buf, Rgb::to_ansi256, Rgb::to_ansi256_text),
110 ColorDepth::Ansi16 => {
111 reduce_with(buf, |tone| tone.to_ansi16_on(ground), |text, bg| text.to_ansi16_text(bg, ground));
112 }
113 }
114}
115
116fn reduce_with(buf: &mut Buffer, tone: impl Fn(Rgb) -> u8, text: impl Fn(Rgb, Rgb) -> u8) {
119 let mut tones: HashMap<Rgb, u8> = HashMap::new();
122 let mut texts: HashMap<(Rgb, Rgb), u8> = HashMap::new();
123 let mut last: Option<(CellColours, (Color, Color))> = None;
124 for cell in &mut buf.content {
125 let (bg, fg) = (rgb(cell.bg), rgb(cell.fg));
126 if bg.is_none() && fg.is_none() {
127 continue;
128 }
129 let glyph = fg.is_some() && bg.is_some() && !cell.symbol().trim().is_empty();
130 let key = (cell.fg, cell.bg, glyph);
131 if let Some((seen, (fg, bg))) = last
132 && seen == key
133 {
134 (cell.fg, cell.bg) = (fg, bg);
135 continue;
136 }
137 if let Some(fg) = fg {
138 let index = match bg {
139 Some(bg) if glyph => *texts.entry((fg, bg)).or_insert_with(|| text(fg, bg)),
140 _ => *tones.entry(fg).or_insert_with(|| tone(fg)),
141 };
142 cell.fg = Color::Indexed(index);
143 }
144 if let Some(bg) = bg {
145 cell.bg = Color::Indexed(*tones.entry(bg).or_insert_with(|| tone(bg)));
146 }
147 last = Some((key, (cell.fg, cell.bg)));
148 }
149}
150
151type CellColours = (Color, Color, bool);
153
154fn rgb(color: Color) -> Option<Rgb> {
156 match color {
157 Color::Rgb(r, g, b) => Some(Rgb::new(r, g, b)),
158 _ => None,
159 }
160}
161
162#[derive(Debug, Clone, PartialEq)]
164pub struct WidgetStyle {
165 props: StyleProps,
166 phase: f32,
167}
168
169impl WidgetStyle {
170 pub(crate) fn new(props: StyleProps, phase: f32) -> Self {
171 Self { props, phase }
172 }
173
174 #[must_use]
177 pub(crate) fn without(mut self, key: &str) -> Self {
178 self.props.remove(key);
179 self
180 }
181
182 #[must_use]
184 pub fn color(&self, key: &str) -> Option<Rgb> {
185 self.props.paint(key).map(|paint: Paint| paint.at(self.phase))
186 }
187
188 #[must_use]
190 pub fn flag(&self, key: &str) -> bool {
191 self.props.flag(key)
192 }
193
194 #[must_use]
196 pub fn cells(&self, key: &str) -> Option<u16> {
197 self.props.cells(key)
198 }
199
200 #[must_use]
202 pub fn word(&self, key: &str) -> Option<&'static str> {
203 self.props.word(key)
204 }
205
206 #[must_use]
208 pub fn padding(&self) -> Padding {
209 match self.props.get("padding") {
210 Some(PropValue::Pair(v, h)) => Padding::symmetric(v, h),
211 Some(PropValue::Cells(n)) => Padding::all(n),
212 _ => Padding::default(),
213 }
214 }
215
216 #[must_use]
218 pub fn text(&self) -> CellStyle {
219 CellStyle {
220 fg: self.color("fg"),
221 bg: self.color("bg"),
222 bold: self.flag("bold"),
223 italic: self.flag("italic"),
224 underline: self.flag("underline"),
225 dim: self.flag("dim"),
226 }
227 }
228
229 #[must_use]
231 pub fn is_animated(&self) -> bool {
232 self.props.is_animated()
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::theme::ThemeRegistry;
240
241 #[test]
242 fn applies_colours_and_modifiers() {
243 let mut cell = Cell::default();
244 CellStyle::fg(Rgb::new(255, 0, 0)).on(Rgb::new(0, 0, 0)).with_bold(true).apply(&mut cell);
245 assert_eq!(cell.fg, Color::Rgb(255, 0, 0));
246 assert!(cell.modifier.contains(Modifier::BOLD));
247 CellStyle::fg(Rgb::new(0, 0, 255)).apply(&mut cell);
248 assert_eq!(cell.fg, Color::Rgb(0, 0, 255));
249 assert_eq!(cell.bg, Color::Rgb(0, 0, 0), "a style without a background keeps the one there");
250 assert!(!cell.modifier.contains(Modifier::BOLD));
251 }
252
253 #[test]
254 fn a_frame_is_reduced_to_its_palette_once_painted() {
255 let ground = Rgb::new(12, 12, 14);
256 let painted = || {
257 let mut buf = Buffer::empty(ratatui_core::layout::Rect::new(0, 0, 3, 1));
258 CellStyle::fg(Rgb::new(255, 0, 0)).on(Rgb::new(0, 0, 0)).apply(&mut buf.content[0]);
259 buf.content[0].set_symbol("a");
260 buf.content[1].set_bg(Color::Indexed(4));
261 buf.content[2].set_bg(Color::Rgb(128, 128, 128));
262 buf
263 };
264 let mut buf = painted();
265 reduce(&mut buf, ColorDepth::TrueColor, ground);
266 assert_eq!(buf, painted(), "true colour is sent as painted");
267 reduce(&mut buf, ColorDepth::Ansi256, ground);
268 assert_eq!((buf.content[0].fg, buf.content[0].bg), (Color::Indexed(196), Color::Indexed(16)));
269 assert_eq!(buf.content[1].bg, Color::Indexed(4), "a palette colour is left as it is");
270 assert_eq!(buf.content[2].bg, Color::Indexed(244));
271 let mut buf = painted();
272 reduce(&mut buf, ColorDepth::Ansi16, ground);
273 assert_eq!((buf.content[0].fg, buf.content[0].bg), (Color::Indexed(9), Color::Indexed(0)));
274 assert_eq!(buf.content[2].bg, Color::Indexed(8));
275 }
276
277 #[test]
278 fn resolves_theme_properties() {
279 let (theme, _) = ThemeRegistry::builtin().resolve_or_default("monochrome");
280 let style = WidgetStyle::new(theme.style("button", Some("primary"), &[]), 0.0);
281 assert_ne!(style.text().bg, theme.color("accent"));
283 assert_eq!(style.text().fg, theme.color("accent"));
284 assert!(style.text().bold);
285 assert_eq!(style.padding(), Padding::symmetric(0, 2));
286 }
287}