ratatui/style/
stylize.rs

1use std::fmt;
2
3use paste::paste;
4
5use crate::{
6    style::{Color, Modifier, Style},
7    text::Span,
8};
9
10/// A trait for objects that have a `Style`.
11///
12/// This trait enables generic code to be written that can interact with any object that has a
13/// `Style`. This is used by the `Stylize` trait to allow generic code to be written that can
14/// interact with any object that can be styled.
15pub trait Styled {
16    type Item;
17
18    /// Returns the style of the object.
19    fn style(&self) -> Style;
20
21    /// Sets the style of the object.
22    ///
23    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
24    /// your own type that implements [`Into<Style>`]).
25    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
26}
27
28/// A helper struct to make it easy to debug using the `Stylize` method names
29pub(crate) struct ColorDebug {
30    pub kind: ColorDebugKind,
31    pub color: Color,
32}
33
34#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
35pub(crate) enum ColorDebugKind {
36    Foreground,
37    Background,
38    #[cfg(feature = "underline-color")]
39    Underline,
40}
41
42impl fmt::Debug for ColorDebug {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        #[cfg(feature = "underline-color")]
45        let is_underline = self.kind == ColorDebugKind::Underline;
46        #[cfg(not(feature = "underline-color"))]
47        let is_underline = false;
48        if is_underline
49            || matches!(
50                self.color,
51                Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _)
52            )
53        {
54            match self.kind {
55                ColorDebugKind::Foreground => write!(f, ".fg(")?,
56                ColorDebugKind::Background => write!(f, ".bg(")?,
57                #[cfg(feature = "underline-color")]
58                ColorDebugKind::Underline => write!(f, ".underline_color(")?,
59            }
60            write!(f, "Color::{:?}", self.color)?;
61            write!(f, ")")?;
62            return Ok(());
63        }
64
65        match self.kind {
66            ColorDebugKind::Foreground => write!(f, ".")?,
67            ColorDebugKind::Background => write!(f, ".on_")?,
68            // TODO: .underline_color_xxx is not implemented on Stylize yet, but it should be
69            #[cfg(feature = "underline-color")]
70            ColorDebugKind::Underline => {
71                unreachable!("covered by the first part of the if statement")
72            }
73        }
74        match self.color {
75            Color::Black => write!(f, "black")?,
76            Color::Red => write!(f, "red")?,
77            Color::Green => write!(f, "green")?,
78            Color::Yellow => write!(f, "yellow")?,
79            Color::Blue => write!(f, "blue")?,
80            Color::Magenta => write!(f, "magenta")?,
81            Color::Cyan => write!(f, "cyan")?,
82            Color::Gray => write!(f, "gray")?,
83            Color::DarkGray => write!(f, "dark_gray")?,
84            Color::LightRed => write!(f, "light_red")?,
85            Color::LightGreen => write!(f, "light_green")?,
86            Color::LightYellow => write!(f, "light_yellow")?,
87            Color::LightBlue => write!(f, "light_blue")?,
88            Color::LightMagenta => write!(f, "light_magenta")?,
89            Color::LightCyan => write!(f, "light_cyan")?,
90            Color::White => write!(f, "white")?,
91            _ => unreachable!("covered by the first part of the if statement"),
92        }
93        write!(f, "()")
94    }
95}
96
97/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
98/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
99/// the color of the style to the corresponding color.
100///
101/// ```rust,ignore
102/// color!(black);
103///
104/// // generates
105///
106/// #[doc = "Sets the foreground color to [`black`](Color::Black)."]
107/// fn black(self) -> T {
108///     self.fg(Color::Black)
109/// }
110///
111/// #[doc = "Sets the background color to [`black`](Color::Black)."]
112/// fn on_black(self) -> T {
113///     self.bg(Color::Black)
114/// }
115/// ```
116macro_rules! color {
117    ( $color:ident ) => {
118        paste! {
119            #[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."]
120            #[must_use = concat!("`", stringify!($color), "` returns the modified style without modifying the original")]
121            fn $color(self) -> T {
122                self.fg(Color::[<$color:camel>])
123            }
124
125            #[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
126            #[must_use = concat!("`on_", stringify!($color), "` returns the modified style without modifying the original")]
127            fn [<on_ $color>](self) -> T {
128                self.bg(Color::[<$color:camel>])
129            }
130        }
131    };
132}
133
134/// Generates a method for a modifier (`bold()`, `italic()`, etc.). Each method sets the modifier
135/// of the style to the corresponding modifier.
136///
137/// # Examples
138///
139/// ```rust,ignore
140/// modifier!(bold);
141///
142/// // generates
143///
144/// #[doc = "Adds the [`BOLD`](Modifier::BOLD) modifier."]
145/// fn bold(self) -> T {
146///     self.add_modifier(Modifier::BOLD)
147/// }
148///
149/// #[doc = "Removes the [`BOLD`](Modifier::BOLD) modifier."]
150/// fn not_bold(self) -> T {
151///     self.remove_modifier(Modifier::BOLD)
152/// }
153/// ```
154macro_rules! modifier {
155    ( $modifier:ident ) => {
156        paste! {
157            #[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
158            #[must_use = concat!("`", stringify!($modifier), "` returns the modified style without modifying the original")]
159            fn [<$modifier>](self) -> T {
160                self.add_modifier(Modifier::[<$modifier:upper>])
161            }
162        }
163
164        paste! {
165            #[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
166            #[must_use = concat!("`not_", stringify!($modifier), "` returns the modified style without modifying the original")]
167            fn [<not_ $modifier>](self) -> T {
168                self.remove_modifier(Modifier::[<$modifier:upper>])
169            }
170        }
171    };
172}
173
174/// An extension trait for styling objects.
175///
176/// For any type that implements `Stylize`, the provided methods in this trait can be used to style
177/// the type further. This trait is automatically implemented for any type that implements the
178/// [`Styled`] trait which e.g.: [`String`], [`&str`], [`Span`], [`Style`] and many Widget types.
179///
180/// This results in much more ergonomic styling of text and widgets. For example, instead of
181/// writing:
182///
183/// ```rust,ignore
184/// let text = Span::styled("Hello", Style::default().fg(Color::Red).bg(Color::Blue));
185/// ```
186///
187/// You can write:
188///
189/// ```rust,ignore
190/// let text = "Hello".red().on_blue();
191/// ```
192///
193/// This trait implements a provided method for every color as both foreground and background
194/// (prefixed by `on_`), and all modifiers as both an additive and subtractive modifier (prefixed
195/// by `not_`). The `reset()` method is also provided to reset the style.
196///
197/// # Examples
198/// ```
199/// use ratatui::{
200///     style::{Color, Modifier, Style, Stylize},
201///     text::Line,
202///     widgets::{Block, Paragraph},
203/// };
204///
205/// let span = "hello".red().on_blue().bold();
206/// let line = Line::from(vec![
207///     "hello".red().on_blue().bold(),
208///     "world".green().on_yellow().not_bold(),
209/// ]);
210/// let paragraph = Paragraph::new(line).italic().underlined();
211/// let block = Block::bordered().title("Title").on_white().bold();
212/// ```
213pub trait Stylize<'a, T>: Sized {
214    #[must_use = "`bg` returns the modified style without modifying the original"]
215    fn bg<C: Into<Color>>(self, color: C) -> T;
216    #[must_use = "`fg` returns the modified style without modifying the original"]
217    fn fg<C: Into<Color>>(self, color: C) -> T;
218    #[must_use = "`reset` returns the modified style without modifying the original"]
219    fn reset(self) -> T;
220    #[must_use = "`add_modifier` returns the modified style without modifying the original"]
221    fn add_modifier(self, modifier: Modifier) -> T;
222    #[must_use = "`remove_modifier` returns the modified style without modifying the original"]
223    fn remove_modifier(self, modifier: Modifier) -> T;
224
225    color!(black);
226    color!(red);
227    color!(green);
228    color!(yellow);
229    color!(blue);
230    color!(magenta);
231    color!(cyan);
232    color!(gray);
233    color!(dark_gray);
234    color!(light_red);
235    color!(light_green);
236    color!(light_yellow);
237    color!(light_blue);
238    color!(light_magenta);
239    color!(light_cyan);
240    color!(white);
241
242    modifier!(bold);
243    modifier!(dim);
244    modifier!(italic);
245    modifier!(underlined);
246    modifier!(slow_blink);
247    modifier!(rapid_blink);
248    modifier!(reversed);
249    modifier!(hidden);
250    modifier!(crossed_out);
251}
252
253impl<'a, T, U> Stylize<'a, T> for U
254where
255    U: Styled<Item = T>,
256{
257    fn bg<C: Into<Color>>(self, color: C) -> T {
258        let style = self.style().bg(color.into());
259        self.set_style(style)
260    }
261
262    fn fg<C: Into<Color>>(self, color: C) -> T {
263        let style = self.style().fg(color.into());
264        self.set_style(style)
265    }
266
267    fn add_modifier(self, modifier: Modifier) -> T {
268        let style = self.style().add_modifier(modifier);
269        self.set_style(style)
270    }
271
272    fn remove_modifier(self, modifier: Modifier) -> T {
273        let style = self.style().remove_modifier(modifier);
274        self.set_style(style)
275    }
276
277    fn reset(self) -> T {
278        self.set_style(Style::reset())
279    }
280}
281
282impl<'a> Styled for &'a str {
283    type Item = Span<'a>;
284
285    fn style(&self) -> Style {
286        Style::default()
287    }
288
289    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
290        Span::styled(self, style)
291    }
292}
293
294impl Styled for String {
295    type Item = Span<'static>;
296
297    fn style(&self) -> Style {
298        Style::default()
299    }
300
301    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
302        Span::styled(self, style)
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use itertools::Itertools;
309    use rstest::rstest;
310
311    use super::*;
312
313    #[test]
314    fn str_styled() {
315        assert_eq!("hello".style(), Style::default());
316        assert_eq!(
317            "hello".set_style(Style::new().cyan()),
318            Span::styled("hello", Style::new().cyan())
319        );
320        assert_eq!("hello".black(), Span::from("hello").black());
321        assert_eq!("hello".red(), Span::from("hello").red());
322        assert_eq!("hello".green(), Span::from("hello").green());
323        assert_eq!("hello".yellow(), Span::from("hello").yellow());
324        assert_eq!("hello".blue(), Span::from("hello").blue());
325        assert_eq!("hello".magenta(), Span::from("hello").magenta());
326        assert_eq!("hello".cyan(), Span::from("hello").cyan());
327        assert_eq!("hello".gray(), Span::from("hello").gray());
328        assert_eq!("hello".dark_gray(), Span::from("hello").dark_gray());
329        assert_eq!("hello".light_red(), Span::from("hello").light_red());
330        assert_eq!("hello".light_green(), Span::from("hello").light_green());
331        assert_eq!("hello".light_yellow(), Span::from("hello").light_yellow());
332        assert_eq!("hello".light_blue(), Span::from("hello").light_blue());
333        assert_eq!("hello".light_magenta(), Span::from("hello").light_magenta());
334        assert_eq!("hello".light_cyan(), Span::from("hello").light_cyan());
335        assert_eq!("hello".white(), Span::from("hello").white());
336
337        assert_eq!("hello".on_black(), Span::from("hello").on_black());
338        assert_eq!("hello".on_red(), Span::from("hello").on_red());
339        assert_eq!("hello".on_green(), Span::from("hello").on_green());
340        assert_eq!("hello".on_yellow(), Span::from("hello").on_yellow());
341        assert_eq!("hello".on_blue(), Span::from("hello").on_blue());
342        assert_eq!("hello".on_magenta(), Span::from("hello").on_magenta());
343        assert_eq!("hello".on_cyan(), Span::from("hello").on_cyan());
344        assert_eq!("hello".on_gray(), Span::from("hello").on_gray());
345        assert_eq!("hello".on_dark_gray(), Span::from("hello").on_dark_gray());
346        assert_eq!("hello".on_light_red(), Span::from("hello").on_light_red());
347        assert_eq!(
348            "hello".on_light_green(),
349            Span::from("hello").on_light_green()
350        );
351        assert_eq!(
352            "hello".on_light_yellow(),
353            Span::from("hello").on_light_yellow()
354        );
355        assert_eq!("hello".on_light_blue(), Span::from("hello").on_light_blue());
356        assert_eq!(
357            "hello".on_light_magenta(),
358            Span::from("hello").on_light_magenta()
359        );
360        assert_eq!("hello".on_light_cyan(), Span::from("hello").on_light_cyan());
361        assert_eq!("hello".on_white(), Span::from("hello").on_white());
362
363        assert_eq!("hello".bold(), Span::from("hello").bold());
364        assert_eq!("hello".dim(), Span::from("hello").dim());
365        assert_eq!("hello".italic(), Span::from("hello").italic());
366        assert_eq!("hello".underlined(), Span::from("hello").underlined());
367        assert_eq!("hello".slow_blink(), Span::from("hello").slow_blink());
368        assert_eq!("hello".rapid_blink(), Span::from("hello").rapid_blink());
369        assert_eq!("hello".reversed(), Span::from("hello").reversed());
370        assert_eq!("hello".hidden(), Span::from("hello").hidden());
371        assert_eq!("hello".crossed_out(), Span::from("hello").crossed_out());
372
373        assert_eq!("hello".not_bold(), Span::from("hello").not_bold());
374        assert_eq!("hello".not_dim(), Span::from("hello").not_dim());
375        assert_eq!("hello".not_italic(), Span::from("hello").not_italic());
376        assert_eq!(
377            "hello".not_underlined(),
378            Span::from("hello").not_underlined()
379        );
380        assert_eq!(
381            "hello".not_slow_blink(),
382            Span::from("hello").not_slow_blink()
383        );
384        assert_eq!(
385            "hello".not_rapid_blink(),
386            Span::from("hello").not_rapid_blink()
387        );
388        assert_eq!("hello".not_reversed(), Span::from("hello").not_reversed());
389        assert_eq!("hello".not_hidden(), Span::from("hello").not_hidden());
390        assert_eq!(
391            "hello".not_crossed_out(),
392            Span::from("hello").not_crossed_out()
393        );
394
395        assert_eq!("hello".reset(), Span::from("hello").reset());
396    }
397
398    #[test]
399    fn string_styled() {
400        let s = String::from("hello");
401        assert_eq!(s.style(), Style::default());
402        assert_eq!(
403            s.clone().set_style(Style::new().cyan()),
404            Span::styled("hello", Style::new().cyan())
405        );
406        assert_eq!(s.clone().black(), Span::from("hello").black());
407        assert_eq!(s.clone().on_black(), Span::from("hello").on_black());
408        assert_eq!(s.clone().bold(), Span::from("hello").bold());
409        assert_eq!(s.clone().not_bold(), Span::from("hello").not_bold());
410        assert_eq!(s.clone().reset(), Span::from("hello").reset());
411    }
412
413    #[test]
414    fn temporary_string_styled() {
415        // to_string() is used to create a temporary String, which is then styled. Without the
416        // `Styled` trait impl for `String`, this would fail to compile with the error: "temporary
417        // value dropped while borrowed"
418        let s = "hello".to_string().red();
419        assert_eq!(s, Span::from("hello").red());
420
421        // format!() is used to create a temporary String inside a closure, which suffers the same
422        // issue as above without the `Styled` trait impl for `String`
423        let items = [String::from("a"), String::from("b")];
424        let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
425        assert_eq!(sss, [Span::from("aa").red(), Span::from("bb").red()]);
426    }
427
428    #[test]
429    fn reset() {
430        assert_eq!(
431            "hello".on_cyan().light_red().bold().underlined().reset(),
432            Span::styled("hello", Style::reset())
433        );
434    }
435
436    #[test]
437    fn fg() {
438        let cyan_fg = Style::default().fg(Color::Cyan);
439
440        assert_eq!("hello".cyan(), Span::styled("hello", cyan_fg));
441    }
442
443    #[test]
444    fn bg() {
445        let cyan_bg = Style::default().bg(Color::Cyan);
446
447        assert_eq!("hello".on_cyan(), Span::styled("hello", cyan_bg));
448    }
449
450    #[test]
451    fn color_modifier() {
452        let cyan_bold = Style::default()
453            .fg(Color::Cyan)
454            .add_modifier(Modifier::BOLD);
455
456        assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold));
457    }
458
459    #[test]
460    fn fg_bg() {
461        let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan);
462
463        assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg));
464    }
465
466    #[test]
467    fn repeated_attributes() {
468        let bg = Style::default().bg(Color::Cyan);
469        let fg = Style::default().fg(Color::Cyan);
470
471        // Behavior: the last one set is the definitive one
472        assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", bg));
473        assert_eq!("hello".red().cyan(), Span::styled("hello", fg));
474    }
475
476    #[test]
477    fn all_chained() {
478        let all_modifier_black = Style::default()
479            .bg(Color::Black)
480            .fg(Color::Black)
481            .add_modifier(
482                Modifier::UNDERLINED
483                    | Modifier::BOLD
484                    | Modifier::DIM
485                    | Modifier::SLOW_BLINK
486                    | Modifier::REVERSED
487                    | Modifier::CROSSED_OUT,
488            );
489        assert_eq!(
490            "hello"
491                .on_black()
492                .black()
493                .bold()
494                .underlined()
495                .dim()
496                .slow_blink()
497                .crossed_out()
498                .reversed(),
499            Span::styled("hello", all_modifier_black)
500        );
501    }
502
503    #[rstest]
504    #[case(ColorDebugKind::Foreground, Color::Black, ".black()")]
505    #[case(ColorDebugKind::Foreground, Color::Red, ".red()")]
506    #[case(ColorDebugKind::Foreground, Color::Green, ".green()")]
507    #[case(ColorDebugKind::Foreground, Color::Yellow, ".yellow()")]
508    #[case(ColorDebugKind::Foreground, Color::Blue, ".blue()")]
509    #[case(ColorDebugKind::Foreground, Color::Magenta, ".magenta()")]
510    #[case(ColorDebugKind::Foreground, Color::Cyan, ".cyan()")]
511    #[case(ColorDebugKind::Foreground, Color::Gray, ".gray()")]
512    #[case(ColorDebugKind::Foreground, Color::DarkGray, ".dark_gray()")]
513    #[case(ColorDebugKind::Foreground, Color::LightRed, ".light_red()")]
514    #[case(ColorDebugKind::Foreground, Color::LightGreen, ".light_green()")]
515    #[case(ColorDebugKind::Foreground, Color::LightYellow, ".light_yellow()")]
516    #[case(ColorDebugKind::Foreground, Color::LightBlue, ".light_blue()")]
517    #[case(ColorDebugKind::Foreground, Color::LightMagenta, ".light_magenta()")]
518    #[case(ColorDebugKind::Foreground, Color::LightCyan, ".light_cyan()")]
519    #[case(ColorDebugKind::Foreground, Color::White, ".white()")]
520    #[case(
521        ColorDebugKind::Foreground,
522        Color::Indexed(10),
523        ".fg(Color::Indexed(10))"
524    )]
525    #[case(
526        ColorDebugKind::Foreground,
527        Color::Rgb(255, 0, 0),
528        ".fg(Color::Rgb(255, 0, 0))"
529    )]
530    #[case(ColorDebugKind::Background, Color::Black, ".on_black()")]
531    #[case(ColorDebugKind::Background, Color::Red, ".on_red()")]
532    #[case(ColorDebugKind::Background, Color::Green, ".on_green()")]
533    #[case(ColorDebugKind::Background, Color::Yellow, ".on_yellow()")]
534    #[case(ColorDebugKind::Background, Color::Blue, ".on_blue()")]
535    #[case(ColorDebugKind::Background, Color::Magenta, ".on_magenta()")]
536    #[case(ColorDebugKind::Background, Color::Cyan, ".on_cyan()")]
537    #[case(ColorDebugKind::Background, Color::Gray, ".on_gray()")]
538    #[case(ColorDebugKind::Background, Color::DarkGray, ".on_dark_gray()")]
539    #[case(ColorDebugKind::Background, Color::LightRed, ".on_light_red()")]
540    #[case(ColorDebugKind::Background, Color::LightGreen, ".on_light_green()")]
541    #[case(ColorDebugKind::Background, Color::LightYellow, ".on_light_yellow()")]
542    #[case(ColorDebugKind::Background, Color::LightBlue, ".on_light_blue()")]
543    #[case(ColorDebugKind::Background, Color::LightMagenta, ".on_light_magenta()")]
544    #[case(ColorDebugKind::Background, Color::LightCyan, ".on_light_cyan()")]
545    #[case(ColorDebugKind::Background, Color::White, ".on_white()")]
546    #[case(
547        ColorDebugKind::Background,
548        Color::Indexed(10),
549        ".bg(Color::Indexed(10))"
550    )]
551    #[case(
552        ColorDebugKind::Background,
553        Color::Rgb(255, 0, 0),
554        ".bg(Color::Rgb(255, 0, 0))"
555    )]
556    #[cfg(feature = "underline-color")]
557    #[case(
558        ColorDebugKind::Underline,
559        Color::Black,
560        ".underline_color(Color::Black)"
561    )]
562    #[cfg(feature = "underline-color")]
563    #[case(ColorDebugKind::Underline, Color::Red, ".underline_color(Color::Red)")]
564    #[cfg(feature = "underline-color")]
565    #[case(
566        ColorDebugKind::Underline,
567        Color::Green,
568        ".underline_color(Color::Green)"
569    )]
570    #[cfg(feature = "underline-color")]
571    #[case(
572        ColorDebugKind::Underline,
573        Color::Yellow,
574        ".underline_color(Color::Yellow)"
575    )]
576    fn stylize_debug(#[case] kind: ColorDebugKind, #[case] color: Color, #[case] expected: &str) {
577        let debug = color.stylize_debug(kind);
578        assert_eq!(format!("{debug:?}"), expected);
579    }
580}