From ed5dbc11183030367c9a510a9706f6791b54430f Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Mon, 14 Feb 2022 19:10:13 +0300 Subject: [PATCH] Add support for dashed and dotted underlines This finishes implementation of underline styles provided by `CSI 4 : [1-5] m` escape sequence. --- CHANGELOG.md | 2 +- alacritty/res/rect.f.glsl | 133 ++++++++++++++++++++++------ alacritty/src/renderer/rects.rs | 114 +++++++++++++----------- alacritty_terminal/src/ansi.rs | 6 ++ alacritty_terminal/src/term/cell.rs | 10 ++- alacritty_terminal/src/term/mod.rs | 8 ++ 6 files changed, 189 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83aa152e..60bb7301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Option `font.builtin_box_drawing` to disable the built-in font for drawing box characters - Track and report surface damage information to Wayland compositors -- Escape sequence for undercurl (`CSI 4 : 3 m`) +- Escape sequence for undercurl, dotted and dashed underlines (`CSI 4 : [3-5] m`) ### Changed diff --git a/alacritty/res/rect.f.glsl b/alacritty/res/rect.f.glsl index ab1d9dcd..9af76ae4 100644 --- a/alacritty/res/rect.f.glsl +++ b/alacritty/res/rect.f.glsl @@ -9,46 +9,121 @@ flat in vec4 color; out vec4 FragColor; -uniform int isUndercurl; +uniform int rectKind; uniform float cellWidth; uniform float cellHeight; uniform float paddingY; uniform float paddingX; -uniform float undercurlThickness; +uniform float underlinePosition; +uniform float underlineThickness; + uniform float undercurlPosition; +#define UNDERCURL 1 +#define DOTTED 2 +#define DASHED 3 + #define PI 3.1415926538 -void main() -{ - if (isUndercurl == 0) { - FragColor = color; - return; - } +vec4 draw_undercurl(int x, int y) { + // We use `undercurlPosition` as an amplitude, since it's half of the descent + // value. + float undercurl = + -1. * undercurlPosition / 2. * cos(float(x) * 2 * PI / cellWidth) + + cellHeight - undercurlPosition; - int x = int(gl_FragCoord.x - paddingX) % int(cellWidth); - int y = int(gl_FragCoord.y - paddingY) % int(cellHeight); + float undercurlTop = undercurl + max((underlineThickness - 1), 0); + float undercurlBottom = undercurl - max((underlineThickness - 1), 0); - // We use `undercurlPosition` as amplitude, since it's half of the descent - // value. - float undercurl = -1. * undercurlPosition / 2. - * cos(float(x) * 2 * PI / float(cellWidth)) - + cellHeight - undercurlPosition; + // Compute resulted alpha based on distance from `gl_FragCoord.y` to the + // cosine curve. + float alpha = 1.; + if (y > undercurlTop || y < undercurlBottom) { + alpha = 1. - min(abs(undercurlTop - y), abs(undercurlBottom - y)); + } - // We subtract one, since curve is already 1px thick. - float undercurl_top = undercurl + max((undercurlThickness - 1), 0); - float undercurl_bottom = undercurl - max((undercurlThickness - 1), 0); - - - // Compute resulted alpha based on distance from `gl_FragCoord.y` to the - // cosine curve. - float alpha = 1.; - if (y > undercurl_top || y < undercurl_bottom) { - alpha = 1. - min(abs(undercurl_top - y), abs(undercurl_bottom - y)); - } - - // The result is an alpha mask on a rect, which leaves only curve opaque. - FragColor = vec4(color.xyz, alpha); + // The result is an alpha mask on a rect, which leaves only curve opaque. + return vec4(color.rgb, alpha); +} + +// When the dot size increases we can use AA to make spacing look even and the +// dots rounded. +vec4 draw_dotted_aliased(float x, float y) { + int dotNumber = int(x / underlineThickness); + + float radius = underlineThickness / 2.; + float centerY = cellHeight - underlinePosition; + + float leftCenter = (dotNumber - dotNumber % 2) * underlineThickness + radius; + float rightCenter = leftCenter + 2 * underlineThickness; + + float distanceLeft = sqrt(pow(x - leftCenter, 2) + pow(y - centerY, 2)); + float distanceRight = sqrt(pow(x - rightCenter, 2) + pow(y - centerY, 2)); + + float alpha = max(1 - (min(distanceLeft, distanceRight) - radius), 0); + return vec4(color.rgb, alpha); +} + +/// Draw dotted line when dot is just a single pixel. +vec4 draw_dotted(int x, int y) { + int cellEven = 0; + + // Since the size of the dot and its gap combined is 2px we should ensure that + // spacing will be even. If the cellWidth is even it'll work since we start + // with dot and end with gap. However if cellWidth is odd, the cell will start + // and end with a dot, creating a dash. To resolve this issue, we invert the + // pattern every two cells. + if (int(cellWidth) % 2 != 0) { + cellEven = int((gl_FragCoord.x - paddingX) / cellWidth) % 2; + } + + // Since we use the entire descent area for dotted underlines, we limit its + // height to a single pixel so we don't draw bars instead of dots. + float alpha = 1. - abs(round(cellHeight - underlinePosition) - y); + if (x % 2 != cellEven) { + alpha = 0; + } + + return vec4(color.rgb, alpha); +} + +vec4 draw_dashed(int x) { + // Since dashes of adjacent cells connect with each other our dash length is + // half of the desired total length. + int halfDashLen = int(cellWidth) / 4; + + float alpha = 1.; + + // Check if `x` coordinate is where we should draw gap. + if (x > halfDashLen && x < cellWidth - halfDashLen - 1) { + alpha = 0.; + } + + return vec4(color.rgb, alpha); +} + +void main() { + int x = int(gl_FragCoord.x - paddingX) % int(cellWidth); + int y = int(gl_FragCoord.y - paddingY) % int(cellHeight); + + switch (rectKind) { + case UNDERCURL: + FragColor = draw_undercurl(x, y); + break; + case DOTTED: + if (underlineThickness < 2) { + FragColor = draw_dotted(x, y); + } else { + FragColor = draw_dotted_aliased(x, y); + } + break; + case DASHED: + FragColor = draw_dashed(x); + break; + default: + FragColor = color; + break; + } } diff --git a/alacritty/src/renderer/rects.rs b/alacritty/src/renderer/rects.rs index e9eb107f..5f9d13e3 100644 --- a/alacritty/src/renderer/rects.rs +++ b/alacritty/src/renderer/rects.rs @@ -23,12 +23,12 @@ pub struct RenderRect { pub height: f32, pub color: Rgb, pub alpha: f32, - pub is_undercurl: bool, + pub kind: RectKind, } impl RenderRect { pub fn new(x: f32, y: f32, width: f32, height: f32, color: Rgb, alpha: f32) -> Self { - RenderRect { x, y, width, height, color, alpha, is_undercurl: false } + RenderRect { kind: RectKind::Normal, x, y, width, height, color, alpha } } } @@ -39,6 +39,17 @@ pub struct RenderLine { pub color: Rgb, } +// NOTE: These flags must be in sync with their usage in the rect.*.glsl shaders. +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RectKind { + Normal = 0, + Undercurl = 1, + DottedUnderline = 2, + DashedUnderline = 3, + NumKinds = 4, +} + impl RenderLine { pub fn rects(&self, flag: Flags, metrics: &Metrics, size: &SizeInfo) -> Vec { let mut rects = Vec::new(); @@ -64,7 +75,7 @@ impl RenderLine { end: Point, color: Rgb, ) { - let (position, thickness) = match flag { + let (position, thickness, ty) = match flag { Flags::DOUBLE_UNDERLINE => { // Position underlines so each one has 50% of descent available. let top_pos = 0.25 * metrics.descent; @@ -80,18 +91,29 @@ impl RenderLine { color, )); - (bottom_pos, metrics.underline_thickness) + (bottom_pos, metrics.underline_thickness, RectKind::Normal) }, // Make undercurl occupy the entire descent area. - Flags::UNDERCURL => (metrics.descent, metrics.descent.abs()), - Flags::UNDERLINE => (metrics.underline_position, metrics.underline_thickness), - Flags::STRIKEOUT => (metrics.strikeout_position, metrics.strikeout_thickness), + Flags::UNDERCURL => (metrics.descent, metrics.descent.abs(), RectKind::Undercurl), + Flags::UNDERLINE => { + (metrics.underline_position, metrics.underline_thickness, RectKind::Normal) + }, + // Make dotted occupy the entire descent area. + Flags::DOTTED_UNDERLINE => { + (metrics.descent, metrics.descent.abs(), RectKind::DottedUnderline) + }, + Flags::DASHED_UNDERLINE => { + (metrics.underline_position, metrics.underline_thickness, RectKind::DashedUnderline) + }, + Flags::STRIKEOUT => { + (metrics.strikeout_position, metrics.strikeout_thickness, RectKind::Normal) + }, _ => unimplemented!("Invalid flag for cell line drawing specified"), }; let mut rect = Self::create_rect(size, metrics.descent, start, end, position, thickness, color); - rect.is_undercurl = flag == Flags::UNDERCURL; + rect.kind = ty; rects.push(rect); } @@ -161,6 +183,8 @@ impl RenderLines { self.update_flag(cell, Flags::DOUBLE_UNDERLINE); self.update_flag(cell, Flags::STRIKEOUT); self.update_flag(cell, Flags::UNDERCURL); + self.update_flag(cell, Flags::DOTTED_UNDERLINE); + self.update_flag(cell, Flags::DASHED_UNDERLINE); } /// Update the lines for a specific flag. @@ -224,8 +248,7 @@ pub struct RectRenderer { program: RectShaderProgram, - rect_vertices: Vec, - curl_vertices: Vec, + vertices: [Vec; 4], } impl RectRenderer { @@ -274,7 +297,7 @@ impl RectRenderer { gl::BindBuffer(gl::ARRAY_BUFFER, 0); } - Ok(Self { vao, vbo, program, rect_vertices: Vec::new(), curl_vertices: Vec::new() }) + Ok(Self { vao, vbo, program, vertices: Default::default() }) } pub fn draw(&mut self, size_info: &SizeInfo, metrics: &Metrics, rects: Vec) { @@ -293,43 +316,32 @@ impl RectRenderer { let half_height = size_info.height() / 2.; // Build rect vertices vector. - self.rect_vertices.clear(); - self.curl_vertices.clear(); + self.vertices.iter_mut().for_each(|vertices| vertices.clear()); for rect in &rects { - if rect.is_undercurl { - Self::add_rect(&mut self.curl_vertices, half_width, half_height, rect); - } else { - Self::add_rect(&mut self.rect_vertices, half_width, half_height, rect); - } + Self::add_rect(&mut self.vertices[rect.kind as usize], half_width, half_height, rect); } unsafe { - if !self.curl_vertices.is_empty() { - self.program.set_undercurl(true); + // We iterate in reverse order to draw plain rects at the end, since we want visual + // bell or damage rects be above the lines. + for rect_kind in (RectKind::Normal as u8..RectKind::NumKinds as u8).rev() { + let vertices = &mut self.vertices[rect_kind as usize]; + if vertices.is_empty() { + continue; + } + + self.program.set_rect_kind(rect_kind as u8); + // Upload accumulated undercurl vertices. gl::BufferData( gl::ARRAY_BUFFER, - (self.curl_vertices.len() * mem::size_of::()) as isize, - self.curl_vertices.as_ptr() as *const _, + (vertices.len() * mem::size_of::()) as isize, + vertices.as_ptr() as *const _, gl::STREAM_DRAW, ); // Draw all vertices as list of triangles. - gl::DrawArrays(gl::TRIANGLES, 0, self.curl_vertices.len() as i32); - } - - if !self.rect_vertices.is_empty() { - self.program.set_undercurl(false); - // Upload accumulated rect vertices. - gl::BufferData( - gl::ARRAY_BUFFER, - (self.rect_vertices.len() * mem::size_of::()) as isize, - self.rect_vertices.as_ptr() as *const _, - gl::STREAM_DRAW, - ); - - // Draw all vertices as list of triangles. - gl::DrawArrays(gl::TRIANGLES, 0, self.rect_vertices.len() as i32); + gl::DrawArrays(gl::TRIANGLES, 0, vertices.len() as i32); } // Disable program. @@ -384,10 +396,8 @@ pub struct RectShaderProgram { /// Shader program. program: ShaderProgram, - /// Undercurl flag. - /// - /// Rect rendering has two modes; one for normal filled rects, and other for undercurls. - u_is_undercurl: GLint, + /// Kind of rect we're drawing. + u_rect_kind: GLint, /// Cell width. u_cell_width: GLint, @@ -399,8 +409,11 @@ pub struct RectShaderProgram { u_padding_x: GLint, u_padding_y: GLint, - /// Undercurl thickness. - u_undercurl_thickness: GLint, + /// Underline position. + u_underline_position: GLint, + + /// Underline thickness. + u_underline_thickness: GLint, /// Undercurl position. u_undercurl_position: GLint, @@ -411,13 +424,14 @@ impl RectShaderProgram { let program = ShaderProgram::new(RECT_SHADER_V, RECT_SHADER_F)?; Ok(Self { - u_is_undercurl: program.get_uniform_location(cstr!("isUndercurl"))?, + u_rect_kind: program.get_uniform_location(cstr!("rectKind"))?, u_cell_width: program.get_uniform_location(cstr!("cellWidth"))?, u_cell_height: program.get_uniform_location(cstr!("cellHeight"))?, u_padding_x: program.get_uniform_location(cstr!("paddingX"))?, u_padding_y: program.get_uniform_location(cstr!("paddingY"))?, + u_underline_position: program.get_uniform_location(cstr!("underlinePosition"))?, + u_underline_thickness: program.get_uniform_location(cstr!("underlineThickness"))?, u_undercurl_position: program.get_uniform_location(cstr!("undercurlPosition"))?, - u_undercurl_thickness: program.get_uniform_location(cstr!("undercurlThickness"))?, program, }) } @@ -426,22 +440,22 @@ impl RectShaderProgram { self.program.id() } - fn set_undercurl(&self, is_undercurl: bool) { - let value = if is_undercurl { 1 } else { 0 }; - + fn set_rect_kind(&self, ty: u8) { unsafe { - gl::Uniform1i(self.u_is_undercurl, value); + gl::Uniform1i(self.u_rect_kind, ty as i32); } } pub fn update_uniforms(&self, size_info: &SizeInfo, metrics: &Metrics) { let position = (0.5 * metrics.descent).abs(); + let underline_position = metrics.descent.abs() - metrics.underline_position.abs(); unsafe { gl::Uniform1f(self.u_cell_width, size_info.cell_width()); gl::Uniform1f(self.u_cell_height, size_info.cell_height()); gl::Uniform1f(self.u_padding_y, size_info.padding_y()); gl::Uniform1f(self.u_padding_x, size_info.padding_x()); - gl::Uniform1f(self.u_undercurl_thickness, metrics.underline_thickness); + gl::Uniform1f(self.u_underline_position, underline_position); + gl::Uniform1f(self.u_underline_thickness, metrics.underline_thickness); gl::Uniform1f(self.u_undercurl_position, position); } } diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs index 91dd3540..8475685d 100644 --- a/alacritty_terminal/src/ansi.rs +++ b/alacritty_terminal/src/ansi.rs @@ -772,6 +772,10 @@ pub enum Attr { DoubleUnderline, /// Undercurled text. Undercurl, + /// Dotted underlined text. + DottedUnderline, + /// Dashed underlined text. + DashedUnderline, /// Blink cursor slowly. BlinkSlow, /// Blink cursor fast. @@ -1332,6 +1336,8 @@ fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec> { [4, 0] => Some(Attr::CancelUnderline), [4, 2] => Some(Attr::DoubleUnderline), [4, 3] => Some(Attr::Undercurl), + [4, 4] => Some(Attr::DottedUnderline), + [4, 5] => Some(Attr::DashedUnderline), [4, ..] => Some(Attr::Underline), [5] => Some(Attr::BlinkSlow), [6] => Some(Attr::BlinkFast), diff --git a/alacritty_terminal/src/term/cell.rs b/alacritty_terminal/src/term/cell.rs index 49d25eb3..2af1fbac 100644 --- a/alacritty_terminal/src/term/cell.rs +++ b/alacritty_terminal/src/term/cell.rs @@ -25,7 +25,11 @@ bitflags! { const LEADING_WIDE_CHAR_SPACER = 0b0000_0100_0000_0000; const DOUBLE_UNDERLINE = 0b0000_1000_0000_0000; const UNDERCURL = 0b0001_0000_0000_0000; - const ALL_UNDERLINES = Self::UNDERLINE.bits | Self::DOUBLE_UNDERLINE.bits | Self::UNDERCURL.bits; + const DOTTED_UNDERLINE = 0b0010_0000_0000_0000; + const DASHED_UNDERLINE = 0b0100_0000_0000_0000; + const ALL_UNDERLINES = Self::UNDERLINE.bits | Self::DOUBLE_UNDERLINE.bits + | Self::UNDERCURL.bits | Self::DOTTED_UNDERLINE.bits + | Self::DASHED_UNDERLINE.bits; } } @@ -119,9 +123,7 @@ impl GridCell for Cell { && self.fg == Color::Named(NamedColor::Foreground) && !self.flags.intersects( Flags::INVERSE - | Flags::UNDERLINE - | Flags::DOUBLE_UNDERLINE - | Flags::UNDERCURL + | Flags::ALL_UNDERLINES | Flags::STRIKEOUT | Flags::WRAPLINE | Flags::WIDE_CHAR_SPACER diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 4d30602b..5ad1b4ad 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -1850,6 +1850,14 @@ impl Handler for Term { cursor.template.flags.remove(Flags::ALL_UNDERLINES); cursor.template.flags.insert(Flags::UNDERCURL); }, + Attr::DottedUnderline => { + cursor.template.flags.remove(Flags::ALL_UNDERLINES); + cursor.template.flags.insert(Flags::DOTTED_UNDERLINE); + }, + Attr::DashedUnderline => { + cursor.template.flags.remove(Flags::ALL_UNDERLINES); + cursor.template.flags.insert(Flags::DASHED_UNDERLINE); + }, Attr::CancelUnderline => cursor.template.flags.remove(Flags::ALL_UNDERLINES), Attr::Hidden => cursor.template.flags.insert(Flags::HIDDEN), Attr::CancelHidden => cursor.template.flags.remove(Flags::HIDDEN),