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..f1100dd8b5 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, } @@ -45,6 +47,12 @@ impl Cell { self } + #[cfg(feature = "crossterm")] + pub fn set_underline_color(&mut self, color: Color) -> &mut Cell { + self.underline_color = color; + self + } + pub fn set_style(&mut self, style: Style) -> &mut Cell { if let Some(c) = style.fg { self.fg = c; @@ -52,11 +60,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 +91,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 +105,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 +134,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 +589,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 +615,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 +654,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 a4519efa64..b8df43e2ff 100644 --- a/src/style.rs +++ b/src/style.rs @@ -200,7 +200,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)); @@ -211,7 +213,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(), @@ -237,6 +241,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(), /// }, @@ -248,6 +254,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, } @@ -263,6 +271,8 @@ impl Style { Style { fg: None, bg: None, + #[cfg(feature = "crossterm")] + underline_color: None, add_modifier: Modifier::empty(), sub_modifier: Modifier::empty(), } @@ -273,6 +283,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(), } @@ -308,6 +320,22 @@ impl Style { self } + /// Changes the underline color. The text must be underlined with a modifier for this to work. + /// + /// ## 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. @@ -365,6 +393,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 0f3fc5251c..cbfe2aac4f 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 9f5d9939b0..e59144940a 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, });