mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-05-31 20:48:41 +03:00
Merge pull request #2295 from qwerasd205/underlines
Various underline (and strikethrough) improvements
This commit is contained in:
@ -188,6 +188,14 @@ pub const Font = opaque {
|
||||
return c.CTFontGetUnderlineThickness(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub fn getCapHeight(self: *Font) f64 {
|
||||
return c.CTFontGetCapHeight(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub fn getXHeight(self: *Font) f64 {
|
||||
return c.CTFontGetXHeight(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub fn getUnitsPerEm(self: *Font) u32 {
|
||||
return c.CTFontGetUnitsPerEm(@ptrCast(self));
|
||||
}
|
||||
|
@ -594,30 +594,37 @@ pub const Face = struct {
|
||||
// All of these metrics are based on our layout above.
|
||||
const cell_height = @ceil(layout_metrics.height);
|
||||
const cell_baseline = @ceil(layout_metrics.height - layout_metrics.ascent);
|
||||
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
|
||||
const strikethrough_position = strikethrough_position: {
|
||||
// This is the height above baseline consumed by text. We must take
|
||||
// into account that our cell height splits the leading between two
|
||||
// rows so we subtract leading space (blank space).
|
||||
const above_baseline = layout_metrics.ascent - (layout_metrics.leading / 2);
|
||||
|
||||
// We want to position the strikethrough at 65% of the height.
|
||||
// This generally gives a nice visual appearance. The number 65%
|
||||
// is somewhat arbitrary but is a common value across terminals.
|
||||
const pos = above_baseline * 0.65;
|
||||
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
|
||||
const strikethrough_thickness = underline_thickness;
|
||||
|
||||
const strikethrough_position = strikethrough_position: {
|
||||
// This is the height of lower case letters in our font.
|
||||
const ex_height = ct_font.getXHeight();
|
||||
|
||||
// We want to position the strikethrough so that it's
|
||||
// vertically centered on any lower case text. This is
|
||||
// a fairly standard choice for strikethrough positioning.
|
||||
//
|
||||
// Because our `strikethrough_position` is relative to the
|
||||
// top of the cell we start with the ascent metric, which
|
||||
// is the distance from the top down to the baseline, then
|
||||
// we subtract half of the ex height to go back up to the
|
||||
// correct height that should evenly split lowercase text.
|
||||
const pos = layout_metrics.ascent -
|
||||
ex_height * 0.5 -
|
||||
strikethrough_thickness * 0.5;
|
||||
|
||||
break :strikethrough_position @ceil(pos);
|
||||
};
|
||||
const strikethrough_thickness = underline_thickness;
|
||||
|
||||
// Underline position reported is usually something like "-1" to
|
||||
// represent the amount under the baseline. We add this to our real
|
||||
// baseline to get the actual value from the bottom (+y is up).
|
||||
// The final underline position is +y from the TOP (confusing)
|
||||
// so we have to subtract from the cell height.
|
||||
const underline_position = cell_height -
|
||||
(cell_baseline + @ceil(@as(f32, @floatCast(ct_font.getUnderlinePosition())))) +
|
||||
1;
|
||||
const underline_position = @ceil(layout_metrics.ascent -
|
||||
@as(f32, @floatCast(ct_font.getUnderlinePosition())));
|
||||
|
||||
// Note: is this useful?
|
||||
// const units_per_em = ct_font.getUnitsPerEm();
|
||||
|
@ -607,6 +607,20 @@ pub const Face = struct {
|
||||
break :cell_width f26dot6ToFloat(size_metrics.max_advance);
|
||||
};
|
||||
|
||||
// Ex height is calculated by measuring the height of the `x` glyph.
|
||||
// If that fails then we just pretend it's 65% of the ascent height.
|
||||
const ex_height: f32 = ex_height: {
|
||||
if (face.getCharIndex('x')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
break :ex_height f26dot6ToFloat(face.handle.*.glyph.*.metrics.height);
|
||||
} else |_| {
|
||||
// Ignore the error since we just fall back to 65% of the ascent below
|
||||
}
|
||||
}
|
||||
|
||||
break :ex_height f26dot6ToFloat(size_metrics.ascender) * 0.65;
|
||||
};
|
||||
|
||||
// Cell height is calculated as the maximum of multiple things in order
|
||||
// to handle edge cases in fonts: (1) the height as reported in metadata
|
||||
// by the font designer (2) the maximum glyph height as measured in the
|
||||
@ -646,50 +660,55 @@ pub const Face = struct {
|
||||
// is reversed.
|
||||
const cell_baseline = -1 * f26dot6ToFloat(size_metrics.descender);
|
||||
|
||||
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
|
||||
face,
|
||||
face.handle.*.underline_thickness,
|
||||
));
|
||||
|
||||
// The underline position. This is a value from the top where the
|
||||
// underline should go.
|
||||
const underline_position: f32 = underline_pos: {
|
||||
// The ascender is already scaled for scalable fonts, but the
|
||||
// underline position is not.
|
||||
const ascender_px = @as(i32, @intCast(size_metrics.ascender)) >> 6;
|
||||
const declared_px = freetype.mulFix(
|
||||
// From the FreeType docs:
|
||||
// > `underline_position`
|
||||
// > The position, in font units, of the underline line for
|
||||
// > this face. It is the center of the underlining stem.
|
||||
|
||||
const declared_px = @as(f32, @floatFromInt(freetype.mulFix(
|
||||
face.handle.*.underline_position,
|
||||
@intCast(face.handle.*.size.*.metrics.y_scale),
|
||||
) >> 6;
|
||||
))) / 64;
|
||||
|
||||
// We use the declared underline position if its available
|
||||
const declared = ascender_px - declared_px;
|
||||
// We use the declared underline position if its available.
|
||||
const declared = @ceil(cell_height - cell_baseline - declared_px - underline_thickness * 0.5);
|
||||
if (declared > 0)
|
||||
break :underline_pos @floatFromInt(declared);
|
||||
break :underline_pos declared;
|
||||
|
||||
// If we have no declared underline position, we go slightly under the
|
||||
// cell height (mainly: non-scalable fonts, i.e. emoji)
|
||||
break :underline_pos cell_height - 1;
|
||||
};
|
||||
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
|
||||
face,
|
||||
face.handle.*.underline_thickness,
|
||||
));
|
||||
|
||||
// The strikethrough position. We use the position provided by the
|
||||
// font if it exists otherwise we calculate a best guess.
|
||||
const strikethrough: struct {
|
||||
pos: f32,
|
||||
thickness: f32,
|
||||
} = if (face.getSfntTable(.os2)) |os2| .{
|
||||
.pos = pos: {
|
||||
// Ascender is scaled, strikeout pos is not
|
||||
const ascender_px = @as(i32, @intCast(size_metrics.ascender)) >> 6;
|
||||
const declared_px = freetype.mulFix(
|
||||
os2.yStrikeoutPosition,
|
||||
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
|
||||
) >> 6;
|
||||
} = if (face.getSfntTable(.os2)) |os2| st: {
|
||||
const thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize));
|
||||
|
||||
break :pos @floatFromInt(ascender_px - declared_px);
|
||||
},
|
||||
.thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize)),
|
||||
const pos = @as(f32, @floatFromInt(freetype.mulFix(
|
||||
os2.yStrikeoutPosition,
|
||||
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
|
||||
))) / 64;
|
||||
|
||||
break :st .{
|
||||
.pos = @ceil(cell_height - cell_baseline - pos),
|
||||
.thickness = thickness,
|
||||
};
|
||||
} else .{
|
||||
.pos = cell_baseline * 0.6,
|
||||
// Exactly 50% of the ex height so that our strikethrough is
|
||||
// centered through lowercase text. This is a common choice.
|
||||
.pos = @ceil(cell_height - cell_baseline - ex_height * 0.5 - underline_thickness * 0.5),
|
||||
.thickness = underline_thickness,
|
||||
};
|
||||
|
||||
@ -832,7 +851,7 @@ test "metrics" {
|
||||
.cell_width = 16,
|
||||
.cell_height = 35,
|
||||
.cell_baseline = 7,
|
||||
.underline_position = 36,
|
||||
.underline_position = 35,
|
||||
.underline_thickness = 2,
|
||||
.strikethrough_position = 20,
|
||||
.strikethrough_thickness = 2,
|
||||
|
@ -27,189 +27,174 @@ pub fn renderGlyph(
|
||||
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);
|
||||
// Draw the appropriate sprite
|
||||
var canvas: font.sprite.Canvas, const offset_y: i32 = switch (sprite) {
|
||||
.underline => try drawSingle(alloc, width, line_thickness),
|
||||
.underline_double => try drawDouble(alloc, width, line_thickness),
|
||||
.underline_dotted => try drawDotted(alloc, width, line_thickness),
|
||||
.underline_dashed => try drawDashed(alloc, width, line_thickness),
|
||||
.underline_curly => try drawCurly(alloc, width, line_thickness),
|
||||
.strikethrough => try drawSingle(alloc, width, line_thickness),
|
||||
else => unreachable,
|
||||
};
|
||||
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 = @as(i32, @intCast(height));
|
||||
|
||||
return font.Glyph{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.height = @intCast(region.height),
|
||||
.offset_x = 0,
|
||||
.offset_y = offset_y,
|
||||
// Glyph.offset_y is the distance between the top of the glyph and the
|
||||
// bottom of the cell. We want the top of the glyph to be at line_pos
|
||||
// from the TOP of the cell, and then offset by the offset_y from the
|
||||
// draw function.
|
||||
.offset_y = @as(i32, @intCast(height - line_pos)) - offset_y,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = @floatFromInt(width),
|
||||
};
|
||||
}
|
||||
|
||||
/// Stores drawing state.
|
||||
const Draw = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
pos: u32,
|
||||
thickness: u32,
|
||||
/// A tuple with the canvas that the desired sprite was drawn on and
|
||||
/// a recommended offset (+Y = down) to shift its Y position by, to
|
||||
/// correct for underline styles with additional thickness.
|
||||
const CanvasAndOffset = struct { font.sprite.Canvas, i32 };
|
||||
|
||||
/// 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),
|
||||
.strikethrough => self.drawSingle(canvas),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
/// Draw a single underline.
|
||||
fn drawSingle(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
/// Draw a single underline.
|
||||
fn drawSingle(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
// Ensure we never overflow out of bounds on the canvas
|
||||
const y_max = self.height -| 1;
|
||||
const bottom = @min(self.pos + self.thickness, y_max);
|
||||
const y = bottom -| self.thickness;
|
||||
const max_height = self.height - y;
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a double underline.
|
||||
fn drawDouble(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness * 3;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(thickness * 2),
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
const offset_y: i32 = -@as(i32, @intCast(thickness));
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a dotted underline.
|
||||
fn drawDotted(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
const dot_width = @max(thickness, 3);
|
||||
const dot_count = @max((width / dot_width) / 2, 1);
|
||||
const gap_width = try std.math.divCeil(u32, width -| (dot_count * dot_width), dot_count);
|
||||
var i: u32 = 0;
|
||||
while (i < dot_count) : (i += 1) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * (dot_width + gap_width), width - 1);
|
||||
const rect_width = @min(width - x, dot_width);
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(y),
|
||||
.width = self.width,
|
||||
.height = @min(self.thickness, max_height),
|
||||
.x = @intCast(x),
|
||||
.y = 0,
|
||||
.width = rect_width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
}
|
||||
|
||||
/// Draw a double underline.
|
||||
fn drawDouble(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
// The maximum y value has to have space for the bottom underline.
|
||||
// If we underflow (saturated) to 0, then we don't draw. This should
|
||||
// never happen but we don't want to draw something undefined.
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
const space = self.thickness * 2;
|
||||
const bottom = @min(self.pos + space, y_max);
|
||||
const top = bottom - space;
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a dashed underline.
|
||||
fn drawDashed(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
const dash_width = width / 3 + 1;
|
||||
const dash_count = (width / dash_width) + 1;
|
||||
var i: u32 = 0;
|
||||
while (i < dash_count) : (i += 2) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dash_width, width - 1);
|
||||
const rect_width = @min(width - x, dash_width);
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(top),
|
||||
.width = self.width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(bottom),
|
||||
.width = self.width,
|
||||
.height = self.thickness,
|
||||
.x = @intCast(x),
|
||||
.y = 0,
|
||||
.width = rect_width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
}
|
||||
|
||||
/// Draw a dotted underline.
|
||||
fn drawDotted(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const y = @min(self.pos, y_max);
|
||||
const dot_width = @max(self.thickness, 3);
|
||||
const dot_count = self.width / dot_width;
|
||||
var i: u32 = 0;
|
||||
while (i < dot_count) : (i += 2) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dot_width, self.width - 1);
|
||||
const width = @min(self.width - 1 - x, dot_width);
|
||||
canvas.rect(.{
|
||||
.x = @intCast(i * dot_width),
|
||||
.y = @intCast(y),
|
||||
.width = width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// 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(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness * 4;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
// Calculate the wave period for a single character
|
||||
// `2 * pi...` = 1 peak per character
|
||||
// `4 * pi...` = 2 peaks per character
|
||||
const wave_period = 2 * std.math.pi / @as(f64, @floatFromInt(width - 1));
|
||||
|
||||
// The full amplitude of the wave can be from the bottom to the
|
||||
// underline position. We also calculate our mid y point of the wave
|
||||
const half_amplitude: f64 = @as(f64, @floatFromInt(thickness));
|
||||
const y_mid: f64 = half_amplitude + 1;
|
||||
|
||||
// follow Xiaolin Wu's antialias algorithm to draw the curve
|
||||
var x: u32 = 0;
|
||||
while (x < width) : (x += 1) {
|
||||
const cosx: f64 = @cos(@as(f64, @floatFromInt(x)) * wave_period);
|
||||
const y: f64 = y_mid + half_amplitude * cosx;
|
||||
const y_upper: u32 = @intFromFloat(@floor(y));
|
||||
const y_lower: u32 = y_upper + thickness + (thickness >> 1);
|
||||
const alpha: u8 = @intFromFloat(255 * @abs(y - @floor(y)));
|
||||
|
||||
// upper and lower bounds
|
||||
canvas.pixel(x, @min(y_upper, height), @enumFromInt(255 - alpha));
|
||||
canvas.pixel(x, @min(y_lower, height), @enumFromInt(alpha));
|
||||
|
||||
// fill between upper and lower bound
|
||||
var y_fill: u32 = y_upper + 1;
|
||||
while (y_fill < y_lower) : (y_fill += 1) {
|
||||
canvas.pixel(x, @min(y_fill, height), .on);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a dashed underline.
|
||||
fn drawDashed(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const y = @min(self.pos, y_max);
|
||||
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) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dash_width, self.width - 1);
|
||||
const width = @min(self.width - 1 - x, dash_width);
|
||||
canvas.rect(.{
|
||||
.x = @intCast(x),
|
||||
.y = @intCast(y),
|
||||
.width = width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
}
|
||||
}
|
||||
const offset_y: i32 = -@as(i32, @intCast(thickness * 2));
|
||||
|
||||
/// 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;
|
||||
|
||||
// Calculate the wave period for a single character
|
||||
// `2 * pi...` = 1 peak per character
|
||||
// `4 * pi...` = 2 peaks per character
|
||||
const wave_period = 2 * std.math.pi / @as(f64, @floatFromInt(self.width - 1));
|
||||
|
||||
// Some fonts put the underline too close to the bottom of the
|
||||
// cell height and this doesn't allow us to make a high enough
|
||||
// wave. This constant is arbitrary, change it for aesthetics.
|
||||
const pos: u32 = pos: {
|
||||
const MIN_AMPLITUDE: u32 = @max(self.height / 9, 2);
|
||||
break :pos y_max - (MIN_AMPLITUDE * 2);
|
||||
};
|
||||
|
||||
// The full amplitude of the wave can be from the bottom to the
|
||||
// underline position. We also calculate our mid y point of the wave
|
||||
const double_amplitude: f64 = @floatFromInt(y_max - pos);
|
||||
const half_amplitude: f64 = @max(1, double_amplitude / 4);
|
||||
const y_mid: u32 = pos + @as(u32, @intFromFloat(2 * half_amplitude));
|
||||
|
||||
// follow Xiaolin Wu's antialias algorithm to draw the curve
|
||||
var x: u32 = 0;
|
||||
while (x < self.width) : (x += 1) {
|
||||
const y: f64 = @as(f64, @floatFromInt(y_mid)) + (half_amplitude * @cos(@as(f64, @floatFromInt(x)) * wave_period));
|
||||
const y_upper: u32 = @intFromFloat(@floor(y));
|
||||
const y_lower: u32 = y_upper + self.thickness;
|
||||
const alpha: u8 = @intFromFloat(255 * @abs(y - @floor(y)));
|
||||
|
||||
// upper and lower bounds
|
||||
canvas.pixel(x, @min(y_upper, y_max), @enumFromInt(255 - alpha));
|
||||
canvas.pixel(x, @min(y_lower, y_max), @enumFromInt(alpha));
|
||||
|
||||
// fill between upper and lower bound
|
||||
var y_fill: u32 = y_upper + 1;
|
||||
while (y_fill < y_lower) : (y_fill += 1) {
|
||||
canvas.pixel(x, @min(y_fill, y_max), .on);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
test "single" {
|
||||
const testing = std.testing;
|
||||
|
@ -2523,6 +2523,45 @@ fn updateCell(
|
||||
}
|
||||
}
|
||||
|
||||
// If the cell has an underline, draw it before the character glyph,
|
||||
// so that it layers underneath instead of overtop, since that can
|
||||
// make text difficult to read.
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
.single => .underline,
|
||||
.double => .underline_double,
|
||||
.dotted => .underline_dotted,
|
||||
.dashed => .underline_dashed,
|
||||
.curly => .underline_curly,
|
||||
};
|
||||
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
.{
|
||||
.cell_width = if (cell.wide == .wide) 2 else 1,
|
||||
.grid_metrics = self.grid_metrics,
|
||||
},
|
||||
);
|
||||
|
||||
const color = style.underlineColor(palette) orelse colors.fg;
|
||||
|
||||
try self.cells.add(self.alloc, .underline, .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||
.constraint_width = cell.gridWidth(),
|
||||
.color = .{ color.r, color.g, color.b, alpha },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.bearings = .{
|
||||
@intCast(render.glyph.offset_x),
|
||||
@intCast(render.glyph.offset_y),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If the shaper cell has a glyph, draw it.
|
||||
if (shaper_cell.glyph_index) |glyph_index| glyph: {
|
||||
// Render
|
||||
@ -2566,42 +2605,6 @@ fn updateCell(
|
||||
});
|
||||
}
|
||||
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
.single => .underline,
|
||||
.double => .underline_double,
|
||||
.dotted => .underline_dotted,
|
||||
.dashed => .underline_dashed,
|
||||
.curly => .underline_curly,
|
||||
};
|
||||
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
.{
|
||||
.cell_width = if (cell.wide == .wide) 2 else 1,
|
||||
.grid_metrics = self.grid_metrics,
|
||||
},
|
||||
);
|
||||
|
||||
const color = style.underlineColor(palette) orelse colors.fg;
|
||||
|
||||
try self.cells.add(self.alloc, .underline, .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||
.constraint_width = cell.gridWidth(),
|
||||
.color = .{ color.r, color.g, color.b, alpha },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.bearings = .{
|
||||
@intCast(render.glyph.offset_x),
|
||||
@intCast(render.glyph.offset_y),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (style.flags.strikethrough) {
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
|
@ -1761,52 +1761,9 @@ fn updateCell(
|
||||
@intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
|
||||
};
|
||||
|
||||
// If the cell has a character, draw it
|
||||
if (cell.hasText()) fg: {
|
||||
// Render
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
shaper_cell.glyph_index orelse break :fg,
|
||||
.{
|
||||
.grid_metrics = self.grid_metrics,
|
||||
.thicken = self.config.font_thicken,
|
||||
},
|
||||
);
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
const mode: CellProgram.CellMode = switch (try fgMode(
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
)) {
|
||||
.normal => .fg,
|
||||
.color => .fg_color,
|
||||
.constrained => .fg_constrained,
|
||||
.powerline => .fg_powerline,
|
||||
};
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
.glyph_x = render.glyph.atlas_x,
|
||||
.glyph_y = render.glyph.atlas_y,
|
||||
.glyph_width = render.glyph.width,
|
||||
.glyph_height = render.glyph.height,
|
||||
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
|
||||
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
|
||||
.r = colors.fg.r,
|
||||
.g = colors.fg.g,
|
||||
.b = colors.fg.b,
|
||||
.a = alpha,
|
||||
.bg_r = bg[0],
|
||||
.bg_g = bg[1],
|
||||
.bg_b = bg[2],
|
||||
.bg_a = bg[3],
|
||||
});
|
||||
}
|
||||
|
||||
// If the cell has an underline, draw it before the character glyph,
|
||||
// so that it layers underneath instead of overtop, since that can
|
||||
// make text difficult to read.
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
@ -1851,6 +1808,58 @@ fn updateCell(
|
||||
});
|
||||
}
|
||||
|
||||
// If the shaper cell has a glyph, draw it.
|
||||
if (shaper_cell.glyph_index) |glyph_index| glyph: {
|
||||
// Render
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
glyph_index,
|
||||
.{
|
||||
.grid_metrics = self.grid_metrics,
|
||||
.thicken = self.config.font_thicken,
|
||||
},
|
||||
);
|
||||
|
||||
// If the glyph is 0 width or height, it will be invisible
|
||||
// when drawn, so don't bother adding it to the buffer.
|
||||
if (render.glyph.width == 0 or render.glyph.height == 0) {
|
||||
break :glyph;
|
||||
}
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
const mode: CellProgram.CellMode = switch (try fgMode(
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
)) {
|
||||
.normal => .fg,
|
||||
.color => .fg_color,
|
||||
.constrained => .fg_constrained,
|
||||
.powerline => .fg_powerline,
|
||||
};
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
.glyph_x = render.glyph.atlas_x,
|
||||
.glyph_y = render.glyph.atlas_y,
|
||||
.glyph_width = render.glyph.width,
|
||||
.glyph_height = render.glyph.height,
|
||||
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
|
||||
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
|
||||
.r = colors.fg.r,
|
||||
.g = colors.fg.g,
|
||||
.b = colors.fg.b,
|
||||
.a = alpha,
|
||||
.bg_r = bg[0],
|
||||
.bg_g = bg[1],
|
||||
.bg_b = bg[2],
|
||||
.bg_a = bg[3],
|
||||
});
|
||||
}
|
||||
|
||||
if (style.flags.strikethrough) {
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
|
@ -224,30 +224,30 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
out.color = float4(in.color) / 255.0f;
|
||||
|
||||
// === Grid Cell ===
|
||||
//
|
||||
// offset.x = bearings.x
|
||||
// .|.
|
||||
// | |
|
||||
// +-------+_.
|
||||
// ._| | |
|
||||
// | | .###. | |
|
||||
// | | #...# | +- bearings.y
|
||||
// glyph_size.y -+ | ##### | |
|
||||
// | | #.... | |
|
||||
// ^ |_| .#### |_| _.
|
||||
// | | | +- offset.y = cell_size.y - bearings.y
|
||||
// . cell_pos -> +-------+ -'
|
||||
// +Y. |_._|
|
||||
// . |
|
||||
// | glyph_size.x
|
||||
// 0,0--...->
|
||||
// +X
|
||||
// 0,0--...->
|
||||
// |
|
||||
// . offset.x = bearings.x
|
||||
// +Y. .|.
|
||||
// . | |
|
||||
// | cell_pos -> +-------+ _.
|
||||
// v ._| |_. _|- offset.y = cell_size.y - bearings.y
|
||||
// | | .###. | |
|
||||
// | | #...# | |
|
||||
// glyph_size.y -+ | ##### | |
|
||||
// | | #.... | +- bearings.y
|
||||
// |_| .#### | |
|
||||
// | |_|
|
||||
// +-------+
|
||||
// |_._|
|
||||
// |
|
||||
// glyph_size.x
|
||||
//
|
||||
// In order to get the bottom left of the glyph, we compute an offset based
|
||||
// on the bearings. The Y bearing is the distance from the top of the cell
|
||||
// to the bottom of the glyph, so we subtract it from the cell height to get
|
||||
// the y offset. The X bearing is the distance from the left of the cell to
|
||||
// the left of the glyph, so it works as the x offset directly.
|
||||
// In order to get the top left of the glyph, we compute an offset based on
|
||||
// the bearings. The Y bearing is the distance from the bottom of the cell
|
||||
// to the top of the glyph, so we subtract it from the cell height to get
|
||||
// the y offset. The X bearing is the distance from the left of the cell
|
||||
// to the left of the glyph, so it works as the x offset directly.
|
||||
|
||||
float2 size = float2(in.glyph_size);
|
||||
float2 offset = float2(in.bearings);
|
||||
|
Reference in New Issue
Block a user