Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support different kinds of underline rendering #3015

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 17 additions & 13 deletions book/src/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix
Each line in the theme file is specified as below:

```toml
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
key = { fg = "#ffffff", bg = "#000000", underline = "#ff0000", modifiers = ["bold", "italic", "undercurled"] }
```

where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline color (only meaningful if an underline modifier is enabled), and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults.

To specify only the foreground color:

Expand Down Expand Up @@ -77,17 +77,21 @@ The following values may be used as modifiers.

Less common modifiers might not be supported by your terminal emulator.

| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `slow_blink` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed_out` |
| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `undercurled` |
| `underdashed` |
| `underdotted` |
| `double-underlined` |
| `slow_blink` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed_out` |

### Scopes

Expand Down
1 change: 1 addition & 0 deletions helix-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.9"
crossterm = { version = "0.25", optional = true }
cxterminfo = "0.2"
serde = { version = "1", "optional" = true, features = ["derive"]}
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
helix-core = { version = "0.6", path = "../helix-core" }
69 changes: 64 additions & 5 deletions helix-tui/src/backend/crossterm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,56 @@ use crossterm::{
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor,
SetForegroundColor, SetUnderlineColor,
},
terminal::{self, Clear, ClearType},
};
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
use std::io::{self, Write};

fn vte_version() -> Option<usize> {
std::env::var("VTE_VERSION").ok()?.parse().ok()
}

/// Describes terminal capabilities like extended underline, truecolor, etc.
#[derive(Copy, Clone, Debug, Default)]
struct Capabilities {
/// Support for undercurled, underdashed, etc.
has_extended_underlines: bool,
}

impl Capabilities {
/// Detect capabilities from the terminfo database located based
/// on the $TERM environment variable. If detection fails, returns
/// a default value where no capability is supported.
pub fn from_env_or_default() -> Self {
match cxterminfo::terminfo::TermInfo::from_env() {
Err(_) => Capabilities::default(),
Ok(t) => Capabilities {
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
// Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines
has_extended_underlines: t.get_ext_string("Smulx").is_some()
|| *t.get_ext_bool("Su").unwrap_or(&false)
|| vte_version() >= Some(5102),
},
}
}
}

pub struct CrosstermBackend<W: Write> {
buffer: W,
capabilities: Capabilities,
}

impl<W> CrosstermBackend<W>
where
W: Write,
{
pub fn new(buffer: W) -> CrosstermBackend<W> {
CrosstermBackend { buffer }
CrosstermBackend {
buffer,
capabilities: Capabilities::from_env_or_default(),
}
}
}

Expand All @@ -47,6 +80,7 @@ where
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut underline = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content {
Expand All @@ -60,7 +94,7 @@ where
from: modifier,
to: cell.modifier,
};
diff.queue(&mut self.buffer)?;
diff.queue(&mut self.buffer, self.capabilities)?;
modifier = cell.modifier;
}
if cell.fg != fg {
Expand All @@ -73,6 +107,11 @@ where
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
bg = cell.bg;
}
if cell.underline != underline {
let color = CColor::from(cell.underline);
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
underline = cell.underline;
}

map_error(queue!(self.buffer, Print(&cell.symbol)))?;
}
Expand Down Expand Up @@ -135,7 +174,7 @@ struct ModifierDiff {
}

impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
fn queue<W>(&self, mut w: W, caps: Capabilities) -> io::Result<()>
where
W: io::Write,
{
Expand All @@ -153,7 +192,7 @@ impl ModifierDiff {
if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
}
if removed.contains(Modifier::UNDERLINED) {
if removed.intersects(Modifier::ANY_UNDERLINE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be that we have to emit NoUnderline followed by underline if the cell changes from one underline to a different type in the next one?

map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
if removed.contains(Modifier::DIM) {
Expand All @@ -166,6 +205,14 @@ impl ModifierDiff {
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
}

let queue_styled_underline = |styled_underline, w: &mut W| -> io::Result<()> {
let underline = match caps.has_extended_underlines {
true => styled_underline,
false => CAttribute::Underlined,
};
map_error(queue!(w, SetAttribute(underline)))
};

let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
Expand All @@ -179,6 +226,18 @@ impl ModifierDiff {
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
}
if added.contains(Modifier::UNDERCURLED) {
queue_styled_underline(CAttribute::Undercurled, &mut w)?;
}
if added.contains(Modifier::UNDERDOTTED) {
queue_styled_underline(CAttribute::Underdotted, &mut w)?;
}
if added.contains(Modifier::UNDERDASHED) {
queue_styled_underline(CAttribute::Underdashed, &mut w)?;
}
if added.contains(Modifier::DOUBLE_UNDERLINED) {
queue_styled_underline(CAttribute::DoubleUnderlined, &mut w)?;
}
if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
}
Expand Down
10 changes: 9 additions & 1 deletion helix-tui/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct Cell {
pub symbol: String,
pub fg: Color,
pub bg: Color,
pub underline: Color,
pub modifier: Modifier,
}

Expand Down Expand Up @@ -44,6 +45,9 @@ impl Cell {
if let Some(c) = style.bg {
self.bg = c;
}
if let Some(c) = style.underline {
self.underline = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
Expand All @@ -53,6 +57,7 @@ impl Cell {
Style::default()
.fg(self.fg)
.bg(self.bg)
.underline(self.underline)
.add_modifier(self.modifier)
}

Expand All @@ -61,6 +66,7 @@ impl Cell {
self.symbol.push(' ');
self.fg = Color::Reset;
self.bg = Color::Reset;
self.underline = Color::Reset;
self.modifier = Modifier::empty();
}
}
Expand All @@ -71,6 +77,7 @@ impl Default for Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
underline: Color::Reset,
modifier: Modifier::empty(),
}
}
Expand All @@ -97,7 +104,8 @@ impl Default for Cell {
/// symbol: String::from("r"),
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// underline: Color::Reset,
/// modifier: Modifier::empty(),
/// });
/// buf[(5, 0)].set_char('x');
/// assert_eq!(buf[(5, 0)].symbol, "x");
Expand Down
4 changes: 4 additions & 0 deletions helix-tui/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
Expand All @@ -143,6 +144,7 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
Expand All @@ -152,6 +154,7 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
Expand All @@ -161,6 +164,7 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
Expand Down
Loading