Skip to content

Commit

Permalink
Add support for dashed and dotted underlines
Browse files Browse the repository at this point in the history
This finishes implementation of underline styles provided by
`CSI 4 : [1-5] m` escape sequence.
  • Loading branch information
kchibisov committed Feb 14, 2022
1 parent 774eb03 commit ed5dbc1
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 80 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -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

Expand Down
127 changes: 101 additions & 26 deletions alacritty/res/rect.f.glsl
Expand Up @@ -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);
// 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);

// 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));
}
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;
}

// The result is an alpha mask on a rect, which leaves only curve opaque.
FragColor = vec4(color.xyz, alpha);
// 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;
}
}
112 changes: 63 additions & 49 deletions alacritty/src/renderer/rects.rs
Expand Up @@ -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 }
}
}

Expand All @@ -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<RenderRect> {
let mut rects = Vec::new();
Expand All @@ -64,7 +75,7 @@ impl RenderLine {
end: Point<usize>,
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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -224,8 +248,7 @@ pub struct RectRenderer {

program: RectShaderProgram,

rect_vertices: Vec<Vertex>,
curl_vertices: Vec<Vertex>,
vertices: [Vec<Vertex>; 4],
}

impl RectRenderer {
Expand Down Expand Up @@ -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<RenderRect>) {
Expand All @@ -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);
// Upload accumulated undercurl vertices.
gl::BufferData(
gl::ARRAY_BUFFER,
(self.curl_vertices.len() * mem::size_of::<Vertex>()) as isize,
self.curl_vertices.as_ptr() as *const _,
gl::STREAM_DRAW,
);
// 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;
}

// Draw all vertices as list of triangles.
gl::DrawArrays(gl::TRIANGLES, 0, self.curl_vertices.len() as i32);
}
self.program.set_rect_kind(rect_kind as u8);

if !self.rect_vertices.is_empty() {
self.program.set_undercurl(false);
// Upload accumulated rect vertices.
// Upload accumulated undercurl vertices.
gl::BufferData(
gl::ARRAY_BUFFER,
(self.rect_vertices.len() * mem::size_of::<Vertex>()) as isize,
self.rect_vertices.as_ptr() as *const _,
(vertices.len() * mem::size_of::<Vertex>()) as isize,
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.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
})
}
Expand All @@ -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);
}
}
Expand Down

0 comments on commit ed5dbc1

Please sign in to comment.