diff --git a/src/terminal/element.rs b/src/terminal/element.rs index 32a76f8b..676f8b5a 100644 --- a/src/terminal/element.rs +++ b/src/terminal/element.rs @@ -19,6 +19,8 @@ use gpui_component::ActiveTheme as _; use super::view::{TerminalView, should_show_context_menu}; use crate::core::config::Config; +mod osc8_underline; + const DIM_OPACITY: f32 = 0.66; /// How much of the text's own colour a link's underline keeps before the @@ -54,6 +56,7 @@ pub(super) struct RenderCell { match_hit: bool, match_current: bool, link_hover: bool, + osc8_dots: bool, } impl Default for RenderCell { @@ -74,6 +77,7 @@ impl Default for RenderCell { match_hit: false, match_current: false, link_hover: false, + osc8_dots: false, } } } @@ -252,6 +256,7 @@ fn snapshot_cell( if flags.contains(Flags::DIM) { rc.fg = dim_fg(rc.fg, bgc, colors.legible_dim); } + osc8_underline::mark(&mut rc, cell); rc } @@ -1708,8 +1713,10 @@ fn invert_cursor_cell( cell.bg = colors.caret; cell.draw_bg = true; cell.fg = ink; + // Keep the underline's own alpha: an OSC 8 link's held-back dots stay + // faint under the caret rather than turning into a full-strength line. if let Some(under) = cell.underline_color.as_mut() { - *under = ink; + *under = ink.opacity(under.a); } // A wide character's trailing spacer is absorbed into the lead cell's // background run by `paint_backgrounds`, so the fill already covers both @@ -2004,6 +2011,7 @@ impl TerminalElement { let row = grid_row as usize * cols; for col in start.column.0..=end.column.0.min(cols.saturating_sub(1)) { let cell = &mut buf[row + col]; + osc8_underline::unmark(cell); cell.link_hover = true; cell.underline_color = link_underline_color(cell, link.armed); } @@ -3882,7 +3890,7 @@ mod tests { assert_eq!(a.as_ref(), "界"); } - fn test_colors() -> PaintColors { + pub(super) fn test_colors() -> PaintColors { let fg = Rgb { r: 10, g: 10, diff --git a/src/terminal/element/osc8_underline.rs b/src/terminal/element/osc8_underline.rs new file mode 100644 index 00000000..afc40971 --- /dev/null +++ b/src/terminal/element/osc8_underline.rs @@ -0,0 +1,125 @@ +//! An OSC 8 link's resting decoration: iTerm2's faint dotted underline, so a +//! `#1234` that is a link reads as one before the pointer finds it. It rides +//! the SGR 4:4 dotted painter, so it costs what an app's own dotted run does. + +use alacritty_terminal::term::cell::Cell; + +use super::{HELD_BACK_LINK_ALPHA, RenderCell, UnderlineKind}; + +/// A cell the application underlined, or gave an underline colour, keeps +/// its own decoration. +pub(super) fn mark(rc: &mut RenderCell, cell: &Cell) { + if rc.underline == UnderlineKind::None + && rc.underline_color.is_none() + && cell.hyperlink().is_some() + { + rc.underline = UnderlineKind::Dotted; + rc.underline_color = Some(rc.fg.opacity(HELD_BACK_LINK_ALPHA)); + rc.osc8_dots = true; + } +} + +/// Under the pointer the dots give way to the hovered link's solid line. +pub(super) fn unmark(rc: &mut RenderCell) { + if std::mem::take(&mut rc.osc8_dots) { + rc.underline = UnderlineKind::None; + rc.underline_color = None; + } +} + +#[cfg(test)] +mod tests { + use alacritty_terminal::index::{Column, Line, Point}; + use alacritty_terminal::term::cell::{Flags, Hyperlink}; + + use alacritty_terminal::vte::ansi::Rgb; + + use super::super::{ + GlyphStyle, PaintColors, invert_cursor_cell, snapshot_cell, tests::test_colors, + }; + use super::*; + + fn linked(flags: Flags) -> Cell { + let mut cell = Cell { + c: '#', + flags, + ..Cell::default() + }; + cell.set_hyperlink(Some(Hyperlink::new( + None::<&str>, + "https://example.com/3328".to_string(), + ))); + cell + } + + fn paint_fixture() -> ([Rgb; 256], PaintColors) { + ([Rgb { r: 0, g: 0, b: 0 }; 256], test_colors()) + } + + #[test] + fn a_hyperlink_rests_under_faint_dots() { + let (palette, colors) = paint_fixture(); + let rc = snapshot_cell( + &linked(Flags::empty()), + Point::new(Line(0), Column(0)), + &palette, + &colors, + None, + ); + assert_eq!(rc.underline, UnderlineKind::Dotted); + assert_eq!( + rc.underline_color, + Some(rc.fg.opacity(HELD_BACK_LINK_ALPHA)) + ); + } + + #[test] + fn an_sgr_underlined_link_keeps_its_own_line_only() { + let (palette, colors) = paint_fixture(); + let rc = snapshot_cell( + &linked(Flags::UNDERLINE), + Point::new(Line(0), Column(0)), + &palette, + &colors, + None, + ); + assert_eq!(rc.underline, UnderlineKind::Single); + assert_eq!(rc.underline_color, None); + assert!(!rc.osc8_dots); + } + + #[test] + fn hovering_swaps_the_dots_for_a_solid_line() { + let (palette, colors) = paint_fixture(); + let mut rc = snapshot_cell( + &linked(Flags::empty()), + Point::new(Line(0), Column(0)), + &palette, + &colors, + None, + ); + unmark(&mut rc); + rc.link_hover = true; + assert_eq!(rc.underline, UnderlineKind::None); + let solid = GlyphStyle::of(&rc) + .underline_style() + .expect("a solid underline"); + assert!(!solid.wavy); + } + + #[test] + fn the_dots_stay_faint_under_a_block_cursor() { + let (palette, colors) = paint_fixture(); + let rc = snapshot_cell( + &linked(Flags::empty()), + Point::new(Line(0), Column(0)), + &palette, + &colors, + None, + ); + let mut buf = vec![rc]; + invert_cursor_cell(&mut buf, 1, 0, 0, &colors); + let under = buf[0].underline_color.expect("the dots keep a colour"); + assert_eq!(under.a, buf[0].fg.a * HELD_BACK_LINK_ALPHA); + } +}