Merge pull request #58 from mitchellh/underline-styles

Underline styles: singe, double, dashed, dotted, curly
This commit is contained in:
Mitchell Hashimoto
2022-11-27 16:23:21 -08:00
committed by GitHub
16 changed files with 415 additions and 106 deletions

View File

@ -289,13 +289,6 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window {
// Pre-calculate our initial cell size ourselves.
const cell_size = try renderer.CellSize.init(alloc, font_group);
// Setup our box font
font_group.group.sprite = font.sprite.Face{
.width = @floatToInt(u32, cell_size.width),
.height = @floatToInt(u32, cell_size.height),
.thickness = 2,
};
// Convert our padding from points to pixels
const padding_x = (@intToFloat(f32, config.@"window-padding-x") * x_dpi) / 72;
const padding_y = (@intToFloat(f32, config.@"window-padding-y") * y_dpi) / 72;

View File

@ -11,6 +11,7 @@ pub const Glyph = @import("Glyph.zig");
pub const Library = @import("Library.zig");
pub const Shaper = @import("Shaper.zig");
pub const sprite = @import("sprite.zig");
pub const Sprite = sprite.Sprite;
pub const Descriptor = discovery.Descriptor;
pub const Discover = discovery.Discover;
@ -60,6 +61,9 @@ pub const Presentation = enum(u1) {
emoji = 1, // U+FEOF
};
/// A FontIndex that can be used to use the sprite font directly.
pub const sprite_index = Group.FontIndex.initSpecial(.sprite);
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -11,10 +11,14 @@ pub const Face = @import("sprite/Face.zig");
/// Area of Unicode.
pub const Sprite = enum(u32) {
// Start 1 above the maximum Unicode codepoint.
const start: u32 = std.math.maxInt(u21) + 1;
const end: u32 = std.math.maxInt(u32);
pub const start: u32 = std.math.maxInt(u21) + 1;
pub const end: u32 = std.math.maxInt(u32);
underline = start,
underline_double = start + 1,
underline_dotted = start + 2,
underline_dashed = start + 3,
underline_curly = start + 4,
// Note: we don't currently put the box drawing glyphs in here because
// there are a LOT and I'm lazy. What I want to do is spend more time

View File

@ -117,13 +117,13 @@ pub fn writeAtlas(self: *Canvas, alloc: Allocator, atlas: *Atlas) !Atlas.Region
/// Draw and fill a rectangle. This is the main primitive for drawing
/// lines as well (which are just generally skinny rectangles...)
pub fn rect(self: *Canvas, v: Rect, color: pixman.Color) void {
pub fn rect(self: *Canvas, v: Rect, color: Color) void {
const boxes = &[_]pixman.Box32{
.{
.x1 = @intCast(i32, v.x),
.y1 = @intCast(i32, v.y),
.x2 = @intCast(i32, v.width),
.y2 = @intCast(i32, v.height),
.x2 = @intCast(i32, v.x + v.width),
.y2 = @intCast(i32, v.y + v.height),
},
};

View File

@ -17,8 +17,10 @@ const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const Sprite = font.sprite.Sprite;
const Atlas = @import("../../Atlas.zig");
const Box = @import("Box.zig");
const underline = @import("underline.zig");
/// The cell width and height.
width: u32,
@ -28,6 +30,9 @@ height: u32,
/// want to do any DPI scaling, it is expected to be done earlier.
thickness: u32,
/// The position fo the underline.
underline_position: u32 = 0,
/// Returns true if the codepoint exists in our sprite font.
pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool {
// We ignore presentation. No matter what presentation is requested
@ -47,25 +52,45 @@ pub fn renderGlyph(
if (std.debug.runtime_safety) assert(self.hasCodepoint(cp, null));
// Safe to ".?" because of the above assertion.
switch (Kind.init(cp).?) {
.box => {
return switch (Kind.init(cp).?) {
.box => box: {
const f: Box = .{
.width = self.width,
.height = self.height,
.thickness = self.thickness,
};
return try f.renderGlyph(alloc, atlas, cp);
break :box try f.renderGlyph(alloc, atlas, cp);
},
}
.underline => try underline.renderGlyph(
alloc,
atlas,
@intToEnum(Sprite, cp),
self.width,
self.height,
self.underline_position,
self.thickness,
),
};
}
/// Kind of sprites we have. Drawing is implemented separately for each kind.
const Kind = enum {
box,
underline,
pub fn init(cp: u32) ?Kind {
return switch (cp) {
Sprite.start...Sprite.end => switch (@intToEnum(Sprite, cp)) {
.underline,
.underline_double,
.underline_dotted,
.underline_dashed,
.underline_curly,
=> .underline,
},
// Box fonts
0x2500...0x257F, // "Box Drawing" block
0x2580...0x259F, // "Block Elements" block

View File

@ -0,0 +1,205 @@
//! This file renders underline sprites. To draw underlines, we render the
//! full cell-width as a sprite and then draw it as a separate pass to the
//! text.
//!
//! We used to render the underlines directly in the GPU shaders but its
//! annoying to support multiple types of underlines and its also annoying
//! to maintain and debug another set of shaders for each renderer instead of
//! just relying on the glyph system we already need to support for text
//! anyways.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const Sprite = font.sprite.Sprite;
const Atlas = @import("../../Atlas.zig");
/// Draw an underline.
pub fn renderGlyph(
alloc: Allocator,
atlas: *Atlas,
sprite: Sprite,
width: u32,
height: u32,
line_pos: u32,
line_thickness: u32,
) !font.Glyph {
// Create the canvas we'll use to draw. We draw the underline in
// a full cell size and position it according to "pos".
var canvas = try font.sprite.Canvas.init(alloc, width, height);
defer canvas.deinit(alloc);
// Perform the actual drawing
(Draw{
.width = width,
.height = height,
.pos = line_pos,
.thickness = line_thickness,
}).draw(&canvas, sprite);
// Write the drawing to the atlas
const region = try canvas.writeAtlas(alloc, atlas);
// Our coordinates start at the BOTTOM for our renderers so we have to
// specify an offset of the full height because we rendered a full size
// cell.
const offset_y = @intCast(i32, height);
return font.Glyph{
.width = width,
.height = height,
.offset_x = 0,
.offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = @intToFloat(f32, width),
};
}
/// Stores drawing state.
const Draw = struct {
width: u32,
height: u32,
pos: u32,
thickness: u32,
/// Draw a specific underline sprite to the canvas.
fn draw(self: Draw, canvas: *font.sprite.Canvas, sprite: Sprite) void {
switch (sprite) {
.underline => self.drawSingle(canvas),
.underline_double => self.drawDouble(canvas),
.underline_dotted => self.drawDotted(canvas),
.underline_dashed => self.drawDashed(canvas),
.underline_curly => self.drawCurly(canvas),
}
}
/// Draw a single underline.
fn drawSingle(self: Draw, canvas: *font.sprite.Canvas) void {
canvas.rect(.{
.x = 0,
.y = self.pos,
.width = self.width,
.height = self.thickness,
}, .on);
}
/// Draw a double underline.
fn drawDouble(self: Draw, canvas: *font.sprite.Canvas) void {
canvas.rect(.{
.x = 0,
.y = self.pos,
.width = self.width,
.height = self.thickness,
}, .on);
canvas.rect(.{
.x = 0,
.y = self.pos + (self.thickness * 2),
.width = self.width,
.height = self.thickness,
}, .on);
}
/// Draw a dotted underline.
fn drawDotted(self: Draw, canvas: *font.sprite.Canvas) void {
const dot_width = @max(self.thickness, 3);
const dot_count = self.width / dot_width;
var i: u32 = 0;
while (i < dot_count) : (i += 2) {
canvas.rect(.{
.x = i * dot_width,
.y = self.pos,
.width = dot_width,
.height = self.thickness,
}, .on);
}
}
/// Draw a dashed underline.
fn drawDashed(self: Draw, canvas: *font.sprite.Canvas) void {
const dash_width = self.width / 3 + 1;
const dash_count = (self.width / dash_width) + 1;
var i: u32 = 0;
while (i < dash_count) : (i += 2) {
canvas.rect(.{
.x = i * dash_width,
.y = self.pos,
.width = dash_width,
.height = self.thickness,
}, .on);
}
}
/// Draw a curly underline. Thanks to Wez Furlong for providing
/// the basic math structure for this since I was lazy with the
/// geometry.
fn drawCurly(self: Draw, canvas: *font.sprite.Canvas) void {
// This is the lowest that the curl can go.
const y_max = self.height - 1;
// The full heightof the wave can be from the bottom to the
// underline position. We also calculate our starting y which is
// slightly below our descender since our wave will move about that.
const wave_height = @intToFloat(f64, y_max - self.pos);
const half_height = wave_height / 4;
const y = self.pos + @floatToInt(u32, half_height);
const x_factor = (2 * std.math.pi) / @intToFloat(f64, self.width);
var x: u32 = 0;
while (x < self.width) : (x += 1) {
const vertical = @floatToInt(
u32,
(-1 * half_height) * @sin(@intToFloat(f64, x) * x_factor) + half_height,
);
var row: u32 = 0;
while (row < self.thickness) : (row += 1) {
const y1 = @min(row + y + vertical, y_max);
canvas.rect(.{
.x = x,
.y = y1,
.width = 1,
.height = 1,
}, .on);
}
}
}
};
test "single" {
const testing = std.testing;
const alloc = testing.allocator;
var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
_ = try renderGlyph(
alloc,
&atlas_greyscale,
.underline,
36,
18,
9,
2,
);
}
test "curly" {
const testing = std.testing;
const alloc = testing.allocator;
var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
_ = try renderGlyph(
alloc,
&atlas_greyscale,
.underline_curly,
36,
18,
9,
2,
);
}

View File

@ -102,8 +102,6 @@ const GPUUniforms = extern struct {
cell_size: [2]f32,
/// Metrics for underline/strikethrough
underline_position: f32,
underline_thickness: f32,
strikethrough_position: f32,
strikethrough_thickness: f32,
};
@ -115,7 +113,6 @@ const GPUCellMode = enum(u8) {
cursor_rect = 3,
cursor_rect_hollow = 4,
cursor_bar = 5,
underline = 6,
strikethrough = 8,
pub fn fromCursor(cursor: renderer.CursorStyle) GPUCellMode {
@ -171,6 +168,14 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
};
log.debug("cell dimensions={}", .{metrics});
// Set the sprite font up
options.font_group.group.sprite = font.sprite.Face{
.width = @floatToInt(u32, metrics.cell_width),
.height = @floatToInt(u32, metrics.cell_height),
.thickness = 2,
.underline_position = @floatToInt(u32, metrics.underline_position),
};
// Create the font shaper. We initially create a shaper that can support
// a width of 160 which is a common width for modern screens to help
// avoid allocations later.
@ -259,8 +264,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.uniforms = .{
.projection_matrix = undefined,
.cell_size = undefined,
.underline_position = metrics.underline_position,
.underline_thickness = metrics.underline_thickness,
.strikethrough_position = metrics.strikethrough_position,
.strikethrough_thickness = metrics.strikethrough_thickness,
},
@ -397,8 +400,6 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
self.uniforms = .{
.projection_matrix = self.uniforms.projection_matrix,
.cell_size = .{ new_cell_size.width, new_cell_size.height },
.underline_position = metrics.underline_position,
.underline_thickness = metrics.underline_thickness,
.strikethrough_position = metrics.strikethrough_position,
.strikethrough_thickness = metrics.strikethrough_thickness,
};
@ -408,11 +409,13 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
if (std.meta.eql(self.cell_size, new_cell_size)) return;
self.cell_size = new_cell_size;
// Set the cell size of the box font
if (self.font_group.group.sprite) |*sprite| {
sprite.width = @floatToInt(u32, self.cell_size.width);
sprite.height = @floatToInt(u32, self.cell_size.height);
}
// Set the sprite font up
self.font_group.group.sprite = font.sprite.Face{
.width = @floatToInt(u32, self.cell_size.width),
.height = @floatToInt(u32, self.cell_size.height),
.thickness = 2,
.underline_position = @floatToInt(u32, metrics.underline_position),
};
// Notify the window that the cell size changed.
_ = self.window_mailbox.push(.{
@ -707,8 +710,6 @@ pub fn setScreenSize(self: *Metal, _: renderer.ScreenSize) !void {
-1 * padding.top,
),
.cell_size = .{ self.cell_size.width, self.cell_size.height },
.underline_position = old.underline_position,
.underline_thickness = old.underline_thickness,
.strikethrough_position = old.strikethrough_position,
.strikethrough_thickness = old.strikethrough_thickness,
};
@ -911,12 +912,31 @@ pub fn updateCell(
});
}
if (cell.attrs.underline) {
if (cell.attrs.underline != .none) {
const sprite: font.Sprite = switch (cell.attrs.underline) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
.dotted => .underline_dotted,
.dashed => .underline_dashed,
.curly => .underline_curly,
};
const glyph = try self.font_group.renderGlyph(
self.alloc,
font.sprite_index,
@enumToInt(sprite),
null,
);
self.cells.appendAssumeCapacity(.{
.mode = .underline,
.mode = .fg,
.grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) },
.cell_width = cell.widthLegacy(),
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
.glyph_size = .{ glyph.width, glyph.height },
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
});
}

View File

@ -137,7 +137,6 @@ const GPUCellMode = enum(u8) {
cursor_rect = 3,
cursor_rect_hollow = 4,
cursor_bar = 5,
underline = 6,
strikethrough = 8,
// Non-exhaustive because masks change it
@ -180,8 +179,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
const pbind = try program.use();
defer pbind.unbind();
try program.setUniform("cell_size", @Vector(2, f32){ metrics.cell_width, metrics.cell_height });
try program.setUniform("underline_position", metrics.underline_position);
try program.setUniform("underline_thickness", metrics.underline_thickness);
try program.setUniform("strikethrough_position", metrics.strikethrough_position);
try program.setUniform("strikethrough_thickness", metrics.strikethrough_thickness);
@ -501,12 +498,6 @@ pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void {
if (std.meta.eql(self.cell_size, new_cell_size)) return;
self.cell_size = new_cell_size;
// Set the cell size of the box font
if (self.font_group.group.sprite) |*sprite| {
sprite.width = @floatToInt(u32, self.cell_size.width);
sprite.height = @floatToInt(u32, self.cell_size.height);
}
// Notify the window that the cell size changed.
_ = self.window_mailbox.push(.{
.cell_size = new_cell_size,
@ -530,12 +521,18 @@ fn resetFontMetrics(
};
log.debug("cell dimensions={}", .{metrics});
// Set details for our sprite font
font_group.group.sprite = font.sprite.Face{
.width = @floatToInt(u32, metrics.cell_width),
.height = @floatToInt(u32, metrics.cell_height),
.thickness = 2,
.underline_position = @floatToInt(u32, metrics.underline_position),
};
// Set our uniforms that rely on metrics
const pbind = try program.use();
defer pbind.unbind();
try program.setUniform("cell_size", @Vector(2, f32){ metrics.cell_width, metrics.cell_height });
try program.setUniform("underline_position", metrics.underline_position);
try program.setUniform("underline_thickness", metrics.underline_thickness);
try program.setUniform("strikethrough_position", metrics.strikethrough_position);
try program.setUniform("strikethrough_thickness", metrics.strikethrough_thickness);
@ -928,7 +925,7 @@ pub fn updateCell(
var i: usize = 0;
if (colors.bg != null) i += 1;
if (!cell.empty()) i += 1;
if (cell.attrs.underline) i += 1;
if (cell.attrs.underline != .none) i += 1;
if (cell.attrs.strikethrough) i += 1;
break :needed i;
};
@ -1002,18 +999,33 @@ pub fn updateCell(
});
}
if (cell.attrs.underline) {
if (cell.attrs.underline != .none) {
const sprite: font.Sprite = switch (cell.attrs.underline) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
.dotted => .underline_dotted,
.dashed => .underline_dashed,
.curly => .underline_curly,
};
const underline_glyph = try self.font_group.renderGlyph(
self.alloc,
font.sprite_index,
@enumToInt(sprite),
null,
);
self.cells.appendAssumeCapacity(.{
.mode = .underline,
.mode = .fg,
.grid_col = @intCast(u16, x),
.grid_row = @intCast(u16, y),
.grid_width = cell.widthLegacy(),
.glyph_x = 0,
.glyph_y = 0,
.glyph_width = 0,
.glyph_height = 0,
.glyph_offset_x = 0,
.glyph_offset_y = 0,
.glyph_x = underline_glyph.atlas_x,
.glyph_y = underline_glyph.atlas_y,
.glyph_width = underline_glyph.width,
.glyph_height = underline_glyph.height,
.glyph_offset_x = underline_glyph.offset_x,
.glyph_offset_y = underline_glyph.offset_y,
.fg_r = colors.fg.r,
.fg_g = colors.fg.g,
.fg_b = colors.fg.b,

View File

@ -30,7 +30,6 @@ const uint MODE_FG_COLOR = 7u;
const uint MODE_CURSOR_RECT = 3u;
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
const uint MODE_CURSOR_BAR = 5u;
const uint MODE_UNDERLINE = 6u;
const uint MODE_STRIKETHROUGH = 8u;
void main() {
@ -94,10 +93,6 @@ void main() {
out_FragColor = color;
break;
case MODE_UNDERLINE:
out_FragColor = color;
break;
case MODE_STRIKETHROUGH:
out_FragColor = color;
break;

View File

@ -8,15 +8,12 @@ enum Mode : uint8_t {
MODE_CURSOR_RECT = 3u,
MODE_CURSOR_RECT_HOLLOW = 4u,
MODE_CURSOR_BAR = 5u,
MODE_UNDERLINE = 6u,
MODE_STRIKETHROUGH = 8u,
};
struct Uniforms {
float4x4 projection_matrix;
float2 cell_size;
float underline_position;
float underline_thickness;
float strikethrough_position;
float strikethrough_thickness;
};
@ -154,22 +151,6 @@ vertex VertexOut uber_vertex(
break;
}
case MODE_UNDERLINE: {
// Underline Y value is just our thickness
float2 underline_size = float2(cell_size_scaled.x, uniforms.underline_thickness);
// Position the underline where we are told to
float2 underline_offset = float2(cell_size_scaled.x, uniforms.underline_position);
// Go to the bottom of the cell, take away the size of the
// underline, and that is our position. We also float it slightly
// above the bottom.
cell_pos = cell_pos + underline_offset - (underline_size * position);
out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0);
break;
}
case MODE_STRIKETHROUGH: {
// Strikethrough Y value is just our thickness
float2 strikethrough_size = float2(cell_size_scaled.x, uniforms.strikethrough_thickness);
@ -259,9 +240,6 @@ fragment float4 uber_fragment(
case MODE_CURSOR_BAR:
return in.color;
case MODE_UNDERLINE:
return in.color;
case MODE_STRIKETHROUGH:
return in.color;
}

View File

@ -10,7 +10,6 @@ const uint MODE_FG_COLOR = 7u;
const uint MODE_CURSOR_RECT = 3u;
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
const uint MODE_CURSOR_BAR = 5u;
const uint MODE_UNDERLINE = 6u;
const uint MODE_STRIKETHROUGH = 8u;
// The grid coordinates (x, y) where x < columns and y < rows
@ -58,8 +57,6 @@ uniform sampler2D text;
uniform sampler2D text_color;
uniform vec2 cell_size;
uniform mat4 projection;
uniform float underline_position;
uniform float underline_thickness;
uniform float strikethrough_position;
uniform float strikethrough_thickness;
@ -200,22 +197,6 @@ void main() {
color = bg_color_in / 255.0;
break;
case MODE_UNDERLINE:
// Underline Y value is just our thickness
vec2 underline_size = vec2(cell_size_scaled.x, underline_thickness);
// Position the underline where we are told to
vec2 underline_offset = vec2(cell_size_scaled.x, underline_position) ;
// Go to the bottom of the cell, take away the size of the
// underline, and that is our position. We also float it slightly
// above the bottom.
cell_pos = cell_pos + underline_offset - (underline_size * position);
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
color = fg_color_in / 255.0;
break;
case MODE_STRIKETHROUGH:
// Strikethrough Y value is just our thickness
vec2 strikethrough_size = vec2(cell_size_scaled.x, strikethrough_thickness);

View File

@ -79,6 +79,10 @@ pub const Action = union(enum) {
intermediates: []u8,
params: []u16,
final: u8,
sep: Sep,
/// The separator used for CSI params.
pub const Sep = enum { semicolon, colon };
// Implement formatter for logging
pub fn format(
@ -392,6 +396,11 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
.intermediates = self.intermediates[0..self.intermediates_idx],
.params = self.params[0..self.params_idx],
.final = c,
.sep = switch (self.params_sep) {
.none, .semicolon => .semicolon,
.colon => .colon,
.mixed => unreachable,
},
},
};
},

View File

@ -56,6 +56,7 @@ const Allocator = std.mem.Allocator;
const utf8proc = @import("utf8proc");
const trace = @import("tracy").trace;
const sgr = @import("sgr.zig");
const color = @import("color.zig");
const point = @import("point.zig");
const CircBuf = @import("circ_buf.zig").CircBuf;
@ -167,10 +168,10 @@ pub const Cell = struct {
bold: bool = false,
italic: bool = false,
faint: bool = false,
underline: bool = false,
blink: bool = false,
inverse: bool = false,
strikethrough: bool = false,
underline: sgr.Attribute.Underline = .none,
/// True if this is a wide character. This char takes up
/// two cells. The following cell ALWAYS is a space.
@ -241,7 +242,7 @@ pub const Cell = struct {
}
test {
//log.warn("CELL={} {}", .{ @sizeOf(Cell), @alignOf(Cell) });
//log.warn("CELL={} bits={} {}", .{ @sizeOf(Cell), @bitSizeOf(Cell), @alignOf(Cell) });
try std.testing.expectEqual(12, @sizeOf(Cell));
}
};

View File

@ -379,12 +379,12 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
self.screen.cursor.pen.attrs.faint = true;
},
.underline => {
self.screen.cursor.pen.attrs.underline = true;
.underline => |v| {
self.screen.cursor.pen.attrs.underline = v;
},
.reset_underline => {
self.screen.cursor.pen.attrs.underline = false;
self.screen.cursor.pen.attrs.underline = .none;
},
.blink => {

View File

@ -31,7 +31,7 @@ pub const Attribute = union(enum) {
faint: void,
/// Underline the text
underline: void,
underline: Underline,
reset_underline: void,
/// Blink the text
@ -75,6 +75,15 @@ pub const Attribute = union(enum) {
g: u8,
b: u8,
};
pub const Underline = enum(u3) {
none = 0,
single = 1,
double = 2,
curly = 3,
dotted = 4,
dashed = 5,
};
};
/// Parser parses the attributes from a list of SGR parameters.
@ -82,6 +91,9 @@ pub const Parser = struct {
params: []const u16,
idx: usize = 0,
/// True if the separator is a colon
colon: bool = false,
/// Next returns the next attribute or null if there are no more attributes.
pub fn next(self: *Parser) ?Attribute {
if (self.idx > self.params.len) return null;
@ -107,7 +119,41 @@ pub const Parser = struct {
3 => return Attribute{ .italic = {} },
4 => return Attribute{ .underline = {} },
4 => blk: {
if (self.colon) {
switch (slice.len) {
// 0 is unreachable because we're here and we read
// an element to get here.
0 => unreachable,
// 1 is unreachable because we can't have a colon
// separator if there are no separators.
1 => unreachable,
// 2 means we have a specific underline style.
2 => {
self.idx += 1;
switch (slice[1]) {
0 => return Attribute{ .reset_underline = {} },
1 => return Attribute{ .underline = .single },
2 => return Attribute{ .underline = .double },
3 => return Attribute{ .underline = .curly },
4 => return Attribute{ .underline = .dotted },
5 => return Attribute{ .underline = .dashed },
// For unknown underline styles, just render
// a single underline.
else => return Attribute{ .underline = .single },
}
},
// Colon-separated must only be 2.
else => break :blk,
}
}
return Attribute{ .underline = .single };
},
5 => return Attribute{ .blink = {} },
@ -206,6 +252,11 @@ fn testParse(params: []const u16) Attribute {
return p.next().?;
}
fn testParseColon(params: []const u16) Attribute {
var p: Parser = .{ .params = params, .colon = true };
return p.next().?;
}
test "sgr: Parser" {
try testing.expect(testParse(&[_]u16{}) == .unset);
try testing.expect(testParse(&[_]u16{0}) == .unset);
@ -275,6 +326,37 @@ test "sgr: underline" {
}
}
test "sgr: underline styles" {
{
const v = testParseColon(&[_]u16{ 4, 2 });
try testing.expect(v == .underline);
try testing.expect(v.underline == .double);
}
{
const v = testParseColon(&[_]u16{ 4, 0 });
try testing.expect(v == .reset_underline);
}
{
const v = testParseColon(&[_]u16{ 4, 1 });
try testing.expect(v == .underline);
try testing.expect(v.underline == .single);
}
{
const v = testParseColon(&[_]u16{ 4, 4 });
try testing.expect(v == .underline);
try testing.expect(v.underline == .dotted);
}
{
const v = testParseColon(&[_]u16{ 4, 5 });
try testing.expect(v == .underline);
try testing.expect(v.underline == .dashed);
}
}
test "sgr: blink" {
{
const v = testParse(&[_]u16{5});

View File

@ -383,7 +383,7 @@ pub fn Stream(comptime Handler: type) type {
// SGR - Select Graphic Rendition
'm' => if (@hasDecl(T, "setAttribute")) {
var p: sgr.Parser = .{ .params = action.params };
var p: sgr.Parser = .{ .params = action.params, .colon = action.sep == .colon };
while (p.next()) |attr| try self.handler.setAttribute(attr);
} else log.warn("unimplemented CSI callback: {}", .{action}),