From 9b13cef59b6d76cd898f23e823ab6d6148b7ba5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mano=20S=C3=A9gransan?= Date: Sat, 15 Jul 2023 11:57:15 +0200 Subject: [PATCH] feat(style): Enable setting the underline color for crossterm (#308) (#310) This commit adds the underline_color() function to the Style and Cell structs. This enables setting the underline color of text on the crossterm backend. This is a no-op for the termion and termwiz backends as they do not support this feature. --- src/backend/crossterm.rs | 9 +++++- src/buffer.rs | 68 +++++++++++++++++++++++++++++++++++++--- src/style.rs | 42 +++++++++++++++++++++++-- src/text.rs | 8 +++++ src/widgets/gauge.rs | 4 +++ 5 files changed, 124 insertions(+), 7 deletions(-) diff --git a/src/backend/crossterm.rs b/src/backend/crossterm.rs index 7a6cb969aa..297e0b27bc 100644 --- a/src/backend/crossterm.rs +++ b/src/backend/crossterm.rs @@ -12,7 +12,7 @@ use crossterm::{ execute, queue, style::{ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, - SetForegroundColor, + SetForegroundColor, SetUnderlineColor, }, terminal::{self, Clear}, }; @@ -81,6 +81,7 @@ where { let mut fg = Color::Reset; let mut bg = Color::Reset; + let mut underline_color = Color::Reset; let mut modifier = Modifier::empty(); let mut last_pos: Option<(u16, u16)> = None; for (x, y, cell) in content { @@ -107,6 +108,11 @@ where map_error(queue!(self.buffer, SetBackgroundColor(color)))?; bg = cell.bg; } + if cell.underline_color != underline_color { + let color = CColor::from(cell.underline_color); + map_error(queue!(self.buffer, SetUnderlineColor(color)))?; + underline_color = cell.underline_color; + } map_error(queue!(self.buffer, Print(&cell.symbol)))?; } @@ -115,6 +121,7 @@ where self.buffer, SetForegroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset), + SetUnderlineColor(CColor::Reset), SetAttribute(CAttribute::Reset) )) } diff --git a/src/buffer.rs b/src/buffer.rs index f99569285d..708937fb85 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -19,6 +19,8 @@ pub struct Cell { pub symbol: String, pub fg: Color, pub bg: Color, + #[cfg(feature = "crossterm")] + pub underline_color: Color, pub modifier: Modifier, } @@ -52,11 +54,25 @@ impl Cell { if let Some(c) = style.bg { self.bg = c; } + #[cfg(feature = "crossterm")] + if let Some(c) = style.underline_color { + self.underline_color = c; + } self.modifier.insert(style.add_modifier); self.modifier.remove(style.sub_modifier); self } + #[cfg(feature = "crossterm")] + pub fn style(&self) -> Style { + Style::default() + .fg(self.fg) + .bg(self.bg) + .underline_color(self.underline_color) + .add_modifier(self.modifier) + } + + #[cfg(not(feature = "crossterm"))] pub fn style(&self) -> Style { Style::default() .fg(self.fg) @@ -69,6 +85,10 @@ impl Cell { self.symbol.push(' '); self.fg = Color::Reset; self.bg = Color::Reset; + #[cfg(feature = "crossterm")] + { + self.underline_color = Color::Reset; + } self.modifier = Modifier::empty(); } } @@ -79,6 +99,8 @@ impl Default for Cell { symbol: " ".into(), fg: Color::Reset, bg: Color::Reset, + #[cfg(feature = "crossterm")] + underline_color: Color::Reset, modifier: Modifier::empty(), } } @@ -106,6 +128,8 @@ impl Default for Cell { /// symbol: String::from("r"), /// fg: Color::Red, /// bg: Color::White, +/// #[cfg(feature = "crossterm")] +/// underline_color: Color::Reset, /// modifier: Modifier::empty() /// }); /// buf.get_mut(5, 0).set_char('x'); @@ -559,10 +583,21 @@ impl Debug for Buffer { overwritten.push((x, &c.symbol)); } skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1); - let style = (c.fg, c.bg, c.modifier); - if last_style != Some(style) { - last_style = Some(style); - styles.push((x, y, c.fg, c.bg, c.modifier)); + #[cfg(feature = "crossterm")] + { + let style = (c.fg, c.bg, c.underline_color, c.modifier); + if last_style != Some(style) { + last_style = Some(style); + styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier)); + } + } + #[cfg(not(feature = "crossterm"))] + { + let style = (c.fg, c.bg, c.modifier); + if last_style != Some(style) { + last_style = Some(style); + styles.push((x, y, c.fg, c.bg, c.modifier)); + } } } if !overwritten.is_empty() { @@ -574,6 +609,12 @@ impl Debug for Buffer { } f.write_str(" ],\n styles: [\n")?; for s in styles { + #[cfg(feature = "crossterm")] + f.write_fmt(format_args!( + " x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n", + s.0, s.1, s.2, s.3, s.4, s.5 + ))?; + #[cfg(not(feature = "crossterm"))] f.write_fmt(format_args!( " x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n", s.0, s.1, s.2, s.3, s.4 @@ -607,6 +648,25 @@ mod tests { .bg(Color::Yellow) .add_modifier(Modifier::BOLD), ); + #[cfg(feature = "crossterm")] + assert_eq!( + format!("{buf:?}"), + indoc::indoc!( + " + Buffer { + area: Rect { x: 0, y: 0, width: 12, height: 2 }, + content: [ + \"Hello World!\", + \"G'day World!\", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD, + ] + }" + ) + ); + #[cfg(not(feature = "crossterm"))] assert_eq!( format!("{buf:?}"), indoc::indoc!( diff --git a/src/style.rs b/src/style.rs index 0988ae5afd..8c0282127e 100644 --- a/src/style.rs +++ b/src/style.rs @@ -198,7 +198,9 @@ impl fmt::Debug for Modifier { /// # use ratatui::layout::Rect; /// let styles = [ /// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC), -/// Style::default().bg(Color::Red), +/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED), +/// #[cfg(feature = "crossterm")] +/// Style::default().underline_color(Color::Green), /// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC), /// ]; /// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1)); @@ -209,7 +211,9 @@ impl fmt::Debug for Modifier { /// Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Red), -/// add_modifier: Modifier::BOLD, +/// #[cfg(feature = "crossterm")] +/// underline_color: Some(Color::Green), +/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED, /// sub_modifier: Modifier::empty(), /// }, /// buffer.get(0, 0).style(), @@ -235,6 +239,8 @@ impl fmt::Debug for Modifier { /// Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Reset), +/// #[cfg(feature = "crossterm")] +/// underline_color: Some(Color::Reset), /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -246,6 +252,8 @@ impl fmt::Debug for Modifier { pub struct Style { pub fg: Option, pub bg: Option, + #[cfg(feature = "crossterm")] + pub underline_color: Option, pub add_modifier: Modifier, pub sub_modifier: Modifier, } @@ -272,6 +280,8 @@ impl Style { Style { fg: None, bg: None, + #[cfg(feature = "crossterm")] + underline_color: None, add_modifier: Modifier::empty(), sub_modifier: Modifier::empty(), } @@ -282,6 +292,8 @@ impl Style { Style { fg: Some(Color::Reset), bg: Some(Color::Reset), + #[cfg(feature = "crossterm")] + underline_color: Some(Color::Reset), add_modifier: Modifier::empty(), sub_modifier: Modifier::all(), } @@ -317,6 +329,27 @@ impl Style { self } + /// Changes the underline color. The text must be underlined with a modifier for this to work. + /// + /// This uses a non-standard ANSI escape sequence. It is supported by most terminal emulators, + /// but is only implemented in the crossterm backend. + /// + /// See [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) code `58` and `59` for more information. + /// + /// ## Examples + /// + /// ```rust + /// # use ratatui::style::{Color, Modifier, Style}; + /// let style = Style::default().underline_color(Color::Blue).add_modifier(Modifier::UNDERLINED); + /// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED); + /// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED)); + /// ``` + #[cfg(feature = "crossterm")] + pub const fn underline_color(mut self, color: Color) -> Style { + self.underline_color = Some(color); + self + } + /// Changes the text emphasis. /// /// When applied, it adds the given modifier to the `Style` modifiers. @@ -374,6 +407,11 @@ impl Style { self.fg = other.fg.or(self.fg); self.bg = other.bg.or(self.bg); + #[cfg(feature = "crossterm")] + { + self.underline_color = other.underline_color.or(self.underline_color); + } + self.add_modifier.remove(other.sub_modifier); self.add_modifier.insert(other.add_modifier); self.sub_modifier.remove(other.add_modifier); diff --git a/src/text.rs b/src/text.rs index 7cace4d5f7..c4f9b1a7ab 100644 --- a/src/text.rs +++ b/src/text.rs @@ -141,6 +141,8 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// #[cfg(feature = "crossterm")] + /// underline_color: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -150,6 +152,8 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// #[cfg(feature = "crossterm")] + /// underline_color: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -159,6 +163,8 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// #[cfg(feature = "crossterm")] + /// underline_color: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -168,6 +174,8 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// #[cfg(feature = "crossterm")] + /// underline_color: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 92cb59d07f..217283fbd1 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -279,6 +279,8 @@ impl<'a> Widget for LineGauge<'a> { .set_style(Style { fg: self.gauge_style.fg, bg: None, + #[cfg(feature = "crossterm")] + underline_color: self.gauge_style.underline_color, add_modifier: self.gauge_style.add_modifier, sub_modifier: self.gauge_style.sub_modifier, }); @@ -289,6 +291,8 @@ impl<'a> Widget for LineGauge<'a> { .set_style(Style { fg: self.gauge_style.bg, bg: None, + #[cfg(feature = "crossterm")] + underline_color: self.gauge_style.underline_color, add_modifier: self.gauge_style.add_modifier, sub_modifier: self.gauge_style.sub_modifier, });