font: faces use primary grid metrics to better line up glyphs

Fixes #895

Every loaded font face calculates metrics for itself. One of the
important metrics is the baseline to "sit" the glyph on top of. Prior to
this commit, each rasterized glyph would sit on its own calculated
baseline. However, this leads to off-center rendering when the font
being rasterized isn't the font that defines the terminal grid.

This commit passes in the font metrics for the font defining the
terminal grid to all font rasterization requests. This can then be used
by non-primary fonts to sit the glyph according to the primary grid.
This commit is contained in:
Mitchell Hashimoto
2023-12-02 09:39:45 -08:00
parent 399dec6efa
commit 7f40881747
5 changed files with 104 additions and 49 deletions

View File

@ -71,10 +71,11 @@ pub const Variation = struct {
/// Additional options for rendering glyphs. /// Additional options for rendering glyphs.
pub const RenderOptions = struct { pub const RenderOptions = struct {
/// The maximum height of the glyph. If this is set, then any glyph /// The metrics that are defining the grid layout. These are usually
/// larger than this height will be shrunk to this height. The scaling /// the metrics of the primary font face. The grid metrics are used
/// is typically naive, but ultimately up to the rasterizer. /// by the font face to better layout the glyph in situations where
max_height: ?u16 = null, /// the font is not exactly the same size as the grid.
grid_metrics: ?Metrics = null,
/// The number of grid cells this glyph will take up. This can be used /// The number of grid cells this glyph will take up. This can be used
/// optionally by the rasterizer to better layout the glyph. /// optionally by the rasterizer to better layout the glyph.

View File

@ -386,7 +386,9 @@ pub const Face = struct {
const offset_y: i32 = offset_y: { const offset_y: i32 = offset_y: {
// Our Y coordinate in 3D is (0, 0) bottom left, +y is UP. // Our Y coordinate in 3D is (0, 0) bottom left, +y is UP.
// We need to calculate our baseline from the bottom of a cell. // We need to calculate our baseline from the bottom of a cell.
const baseline_from_bottom: f64 = @floatFromInt(self.metrics.cell_baseline); //const baseline_from_bottom: f64 = @floatFromInt(self.metrics.cell_baseline);
const metrics = opts.grid_metrics orelse self.metrics;
const baseline_from_bottom: f64 = @floatFromInt(metrics.cell_baseline);
// Next we offset our baseline by the bearing in the font. We // Next we offset our baseline by the bearing in the font. We
// ADD here because CoreText y is UP. // ADD here because CoreText y is UP.

View File

@ -226,6 +226,8 @@ pub const Face = struct {
glyph_index: u32, glyph_index: u32,
opts: font.face.RenderOptions, opts: font.face.RenderOptions,
) !Glyph { ) !Glyph {
const metrics = opts.grid_metrics orelse self.metrics;
// If our glyph has color, we want to render the color // If our glyph has color, we want to render the color
try self.face.loadGlyph(glyph_index, .{ try self.face.loadGlyph(glyph_index, .{
.render = true, .render = true,
@ -288,7 +290,7 @@ pub const Face = struct {
// and copy the atlas. // and copy the atlas.
const bitmap_original = bitmap_converted orelse bitmap_ft; const bitmap_original = bitmap_converted orelse bitmap_ft;
const bitmap_resized: ?freetype.c.struct_FT_Bitmap_ = resized: { const bitmap_resized: ?freetype.c.struct_FT_Bitmap_ = resized: {
const max = opts.max_height orelse break :resized null; const max = metrics.cell_height;
const bm = bitmap_original; const bm = bitmap_original;
if (bm.rows <= max) break :resized null; if (bm.rows <= max) break :resized null;
@ -425,7 +427,7 @@ pub const Face = struct {
// baseline calculation. The baseline calculation is so that everything // baseline calculation. The baseline calculation is so that everything
// is properly centered when we render it out into a monospace grid. // is properly centered when we render it out into a monospace grid.
// Note: we add here because our X/Y is actually reversed, adding goes UP. // Note: we add here because our X/Y is actually reversed, adding goes UP.
break :offset_y glyph_metrics.bitmap_top + @as(c_int, @intCast(self.metrics.cell_baseline)); break :offset_y glyph_metrics.bitmap_top + @as(c_int, @intCast(metrics.cell_baseline));
}; };
// log.warn("renderGlyph width={} height={} offset_x={} offset_y={} glyph_metrics={}", .{ // log.warn("renderGlyph width={} height={} offset_x={} offset_y={} glyph_metrics={}", .{
@ -662,7 +664,15 @@ test "color emoji" {
// resize // resize
{ {
const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{ const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{
.max_height = 24, .grid_metrics = .{
.cell_width = 10,
.cell_height = 24,
.cell_baseline = 0,
.underline_position = 0,
.underline_thickness = 0,
.strikethrough_position = 0,
.strikethrough_thickness = 0,
},
}); });
try testing.expectEqual(@as(u32, 24), glyph.height); try testing.expectEqual(@as(u32, 24), glyph.height);
} }

View File

@ -56,8 +56,8 @@ config: DerivedConfig,
/// The mailbox for communicating with the window. /// The mailbox for communicating with the window.
surface_mailbox: apprt.surface.Mailbox, surface_mailbox: apprt.surface.Mailbox,
/// Current cell dimensions for this grid. /// Current font metrics defining our grid.
cell_size: renderer.CellSize, grid_metrics: font.face.Metrics,
/// Current screen size dimensions for this grid. This is set on the first /// Current screen size dimensions for this grid. This is set on the first
/// resize event, and is not immediately available. /// resize event, and is not immediately available.
@ -359,7 +359,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.alloc = alloc, .alloc = alloc,
.config = options.config, .config = options.config,
.surface_mailbox = options.surface_mailbox, .surface_mailbox = options.surface_mailbox,
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .grid_metrics = metrics,
.screen_size = null, .screen_size = null,
.padding = options.padding, .padding = options.padding,
.focused = true, .focused = true,
@ -497,7 +497,10 @@ fn gridSize(self: *Metal) ?renderer.GridSize {
const screen_size = self.screen_size orelse return null; const screen_size = self.screen_size orelse return null;
return renderer.GridSize.init( return renderer.GridSize.init(
screen_size.subPadding(self.padding.explicit), screen_size.subPadding(self.padding.explicit),
self.cell_size, .{
.width = self.grid_metrics.cell_width,
.height = self.grid_metrics.cell_height,
},
); );
} }
@ -523,14 +526,13 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
const face = try self.font_group.group.faceFromIndex(index); const face = try self.font_group.group.faceFromIndex(index);
break :metrics face.metrics; break :metrics face.metrics;
}; };
const new_cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height };
// Update our uniforms // Update our uniforms
self.uniforms = .{ self.uniforms = .{
.projection_matrix = self.uniforms.projection_matrix, .projection_matrix = self.uniforms.projection_matrix,
.cell_size = .{ .cell_size = .{
@floatFromInt(new_cell_size.width), @floatFromInt(metrics.cell_width),
@floatFromInt(new_cell_size.height), @floatFromInt(metrics.cell_height),
}, },
.strikethrough_position = @floatFromInt(metrics.strikethrough_position), .strikethrough_position = @floatFromInt(metrics.strikethrough_position),
.strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness), .strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness),
@ -539,20 +541,23 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
// Recalculate our cell size. If it is the same as before, then we do // Recalculate our cell size. If it is the same as before, then we do
// nothing since the grid size couldn't have possibly changed. // nothing since the grid size couldn't have possibly changed.
if (std.meta.eql(self.cell_size, new_cell_size)) return; if (std.meta.eql(self.grid_metrics, metrics)) return;
self.cell_size = new_cell_size; self.grid_metrics = metrics;
// Set the sprite font up // Set the sprite font up
self.font_group.group.sprite = font.sprite.Face{ self.font_group.group.sprite = font.sprite.Face{
.width = self.cell_size.width, .width = metrics.cell_width,
.height = self.cell_size.height, .height = metrics.cell_height,
.thickness = metrics.underline_thickness * @as(u32, if (self.config.font_thicken) 2 else 1), .thickness = metrics.underline_thickness * @as(u32, if (self.config.font_thicken) 2 else 1),
.underline_position = metrics.underline_position, .underline_position = metrics.underline_position,
}; };
// Notify the window that the cell size changed. // Notify the window that the cell size changed.
_ = self.surface_mailbox.push(.{ _ = self.surface_mailbox.push(.{
.cell_size = new_cell_size, .cell_size = .{
.width = metrics.cell_width,
.height = metrics.cell_height,
},
}, .{ .forever = {} }); }, .{ .forever = {} });
} }
@ -1140,7 +1145,7 @@ fn prepKittyGraphics(
// so that we render (0, 0) with some offset for the texture. // so that we render (0, 0) with some offset for the texture.
const offset_y: u32 = if (rect.top_left.y < t.screen.viewport) offset_y: { const offset_y: u32 = if (rect.top_left.y < t.screen.viewport) offset_y: {
const offset_cells = t.screen.viewport - rect.top_left.y; const offset_cells = t.screen.viewport - rect.top_left.y;
const offset_pixels = offset_cells * self.cell_size.height; const offset_pixels = offset_cells * self.grid_metrics.cell_height;
break :offset_y @intCast(offset_pixels); break :offset_y @intCast(offset_pixels);
} else 0; } else 0;
@ -1181,8 +1186,8 @@ fn prepKittyGraphics(
image.height -| offset_y; image.height -| offset_y;
// Calculate the width/height of our image. // Calculate the width/height of our image.
const dest_width = if (p.columns > 0) p.columns * self.cell_size.width else source_width; const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
const dest_height = if (p.rows > 0) p.rows * self.cell_size.height else source_height; const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
// Accumulate the placement // Accumulate the placement
if (image.width > 0 and image.height > 0) { if (image.width > 0 and image.height > 0) {
@ -1286,7 +1291,14 @@ pub fn setScreenSize(
// the leftover amounts on the right/bottom that don't fit a full grid cell // the leftover amounts on the right/bottom that don't fit a full grid cell
// and we split them equal across all boundaries. // and we split them equal across all boundaries.
const padding = if (self.padding.balance) const padding = if (self.padding.balance)
renderer.Padding.balanced(dim, grid_size, self.cell_size) renderer.Padding.balanced(
dim,
grid_size,
.{
.width = self.grid_metrics.cell_width,
.height = self.grid_metrics.cell_height,
},
)
else else
self.padding.explicit; self.padding.explicit;
const padded_dim = dim.subPadding(padding); const padded_dim = dim.subPadding(padding);
@ -1307,8 +1319,8 @@ pub fn setScreenSize(
-1 * @as(f32, @floatFromInt(padding.top)), -1 * @as(f32, @floatFromInt(padding.top)),
), ),
.cell_size = .{ .cell_size = .{
@floatFromInt(self.cell_size.width), @floatFromInt(self.grid_metrics.cell_width),
@floatFromInt(self.cell_size.height), @floatFromInt(self.grid_metrics.cell_height),
}, },
.strikethrough_position = old.strikethrough_position, .strikethrough_position = old.strikethrough_position,
.strikethrough_thickness = old.strikethrough_thickness, .strikethrough_thickness = old.strikethrough_thickness,
@ -1366,7 +1378,7 @@ pub fn setScreenSize(
}; };
} }
log.debug("screen size screen={} grid={}, cell={}", .{ dim, grid_size, self.cell_size }); log.debug("screen size screen={} grid={}, cell_width={} cell_height={}", .{ dim, grid_size, self.grid_metrics.cell_width, self.grid_metrics.cell_height });
} }
/// Sync all the CPU cells with the GPU state (but still on the CPU here). /// Sync all the CPU cells with the GPU state (but still on the CPU here).
@ -1728,7 +1740,7 @@ pub fn updateCell(
shaper_run.font_index, shaper_run.font_index,
shaper_cell.glyph_index, shaper_cell.glyph_index,
.{ .{
.max_height = @intCast(self.cell_size.height), .grid_metrics = self.grid_metrics,
.thicken = self.config.font_thicken, .thicken = self.config.font_thicken,
}, },
); );
@ -1766,7 +1778,10 @@ pub fn updateCell(
self.alloc, self.alloc,
font.sprite_index, font.sprite_index,
@intFromEnum(sprite), @intFromEnum(sprite),
.{ .cell_width = if (cell.attrs.wide) 2 else 1 }, .{
.cell_width = if (cell.attrs.wide) 2 else 1,
.grid_metrics = self.grid_metrics,
},
); );
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
@ -1839,7 +1854,10 @@ fn addCursor(
self.alloc, self.alloc,
font.sprite_index, font.sprite_index,
@intFromEnum(sprite), @intFromEnum(sprite),
.{ .cell_width = if (wide) 2 else 1 }, .{
.cell_width = if (wide) 2 else 1,
.grid_metrics = self.grid_metrics,
},
) catch |err| { ) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err}); log.warn("error rendering cursor glyph err={}", .{err});
return null; return null;
@ -1894,7 +1912,7 @@ fn addPreeditCell(
self.alloc, self.alloc,
font_index, font_index,
glyph_index, glyph_index,
.{}, .{ .grid_metrics = self.grid_metrics },
) catch |err| { ) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err}); log.warn("error rendering preedit glyph err={}", .{err});
return; return;

View File

@ -47,8 +47,8 @@ alloc: std.mem.Allocator,
/// The configuration we need derived from the main config. /// The configuration we need derived from the main config.
config: DerivedConfig, config: DerivedConfig,
/// Current cell dimensions for this grid. /// Current font metrics defining our grid.
cell_size: renderer.CellSize, grid_metrics: font.face.Metrics,
/// Current screen size dimensions for this grid. This is set on the first /// Current screen size dimensions for this grid. This is set on the first
/// resize event, and is not immediately available. /// resize event, and is not immediately available.
@ -128,7 +128,14 @@ const SetScreenSize = struct {
// Apply our padding // Apply our padding
const padding = if (r.padding.balance) const padding = if (r.padding.balance)
renderer.Padding.balanced(self.size, r.gridSize(self.size), r.cell_size) renderer.Padding.balanced(
self.size,
r.gridSize(self.size),
.{
.width = r.grid_metrics.cell_width,
.height = r.grid_metrics.cell_height,
},
)
else else
r.padding.explicit; r.padding.explicit;
const padded_size = self.size.subPadding(padding); const padded_size = self.size.subPadding(padding);
@ -137,7 +144,10 @@ const SetScreenSize = struct {
padded_size, padded_size,
self.size, self.size,
r.gridSize(self.size), r.gridSize(self.size),
r.cell_size, renderer.CellSize{
.width = r.grid_metrics.cell_width,
.height = r.grid_metrics.cell_height,
},
r.padding.explicit, r.padding.explicit,
}); });
@ -340,7 +350,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.config = options.config, .config = options.config,
.cells_bg = .{}, .cells_bg = .{},
.cells = .{}, .cells = .{},
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .grid_metrics = metrics,
.screen_size = null, .screen_size = null,
.gl_state = gl_state, .gl_state = gl_state,
.font_group = options.font_group, .font_group = options.font_group,
@ -576,13 +586,15 @@ pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void {
// Recalculate our cell size. If it is the same as before, then we do // Recalculate our cell size. If it is the same as before, then we do
// nothing since the grid size couldn't have possibly changed. // nothing since the grid size couldn't have possibly changed.
const new_cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }; if (std.meta.eql(self.grid_metrics, metrics)) return;
if (std.meta.eql(self.cell_size, new_cell_size)) return; self.grid_metrics = metrics;
self.cell_size = new_cell_size;
// Notify the window that the cell size changed. // Notify the window that the cell size changed.
_ = self.surface_mailbox.push(.{ _ = self.surface_mailbox.push(.{
.cell_size = new_cell_size, .cell_size = .{
.width = metrics.cell_width,
.height = metrics.cell_height,
},
}, .{ .forever = {} }); }, .{ .forever = {} });
} }
@ -780,7 +792,7 @@ fn prepKittyGraphics(
// so that we render (0, 0) with some offset for the texture. // so that we render (0, 0) with some offset for the texture.
const offset_y: u32 = if (rect.top_left.y < t.screen.viewport) offset_y: { const offset_y: u32 = if (rect.top_left.y < t.screen.viewport) offset_y: {
const offset_cells = t.screen.viewport - rect.top_left.y; const offset_cells = t.screen.viewport - rect.top_left.y;
const offset_pixels = offset_cells * self.cell_size.height; const offset_pixels = offset_cells * self.grid_metrics.cell_height;
break :offset_y @intCast(offset_pixels); break :offset_y @intCast(offset_pixels);
} else 0; } else 0;
@ -821,8 +833,8 @@ fn prepKittyGraphics(
image.height -| offset_y; image.height -| offset_y;
// Calculate the width/height of our image. // Calculate the width/height of our image.
const dest_width = if (p.columns > 0) p.columns * self.cell_size.width else source_width; const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
const dest_height = if (p.rows > 0) p.rows * self.cell_size.height else source_height; const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
// Accumulate the placement // Accumulate the placement
if (image.width > 0 and image.height > 0) { if (image.width > 0 and image.height > 0) {
@ -1145,7 +1157,7 @@ fn addPreeditCell(
self.alloc, self.alloc,
font_index, font_index,
glyph_index, glyph_index,
.{}, .{ .grid_metrics = self.grid_metrics },
) catch |err| { ) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err}); log.warn("error rendering preedit glyph err={}", .{err});
return; return;
@ -1239,7 +1251,10 @@ fn addCursor(
self.alloc, self.alloc,
font.sprite_index, font.sprite_index,
@intFromEnum(sprite), @intFromEnum(sprite),
.{ .cell_width = if (wide) 2 else 1 }, .{
.grid_metrics = self.grid_metrics,
.cell_width = if (wide) 2 else 1,
},
) catch |err| { ) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err}); log.warn("error rendering cursor glyph err={}", .{err});
return null; return null;
@ -1431,7 +1446,7 @@ pub fn updateCell(
shaper_run.font_index, shaper_run.font_index,
shaper_cell.glyph_index, shaper_cell.glyph_index,
.{ .{
.max_height = @intCast(self.cell_size.height), .grid_metrics = self.grid_metrics,
.thicken = self.config.font_thicken, .thicken = self.config.font_thicken,
}, },
); );
@ -1479,7 +1494,10 @@ pub fn updateCell(
self.alloc, self.alloc,
font.sprite_index, font.sprite_index,
@intFromEnum(sprite), @intFromEnum(sprite),
.{ .cell_width = if (cell.attrs.wide) 2 else 1 }, .{
.grid_metrics = self.grid_metrics,
.cell_width = if (cell.attrs.wide) 2 else 1,
},
); );
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
@ -1537,7 +1555,10 @@ pub fn updateCell(
fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize { fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize {
return renderer.GridSize.init( return renderer.GridSize.init(
screen_size.subPadding(self.padding.explicit), screen_size.subPadding(self.padding.explicit),
self.cell_size, .{
.width = self.grid_metrics.cell_width,
.height = self.grid_metrics.cell_height,
},
); );
} }
@ -1598,7 +1619,10 @@ pub fn setScreenSize(
log.debug("screen size screen={} grid={} cell={} padding={}", .{ log.debug("screen size screen={} grid={} cell={} padding={}", .{
dim, dim,
grid_size, grid_size,
self.cell_size, renderer.CellSize{
.width = self.grid_metrics.cell_width,
.height = self.grid_metrics.cell_height,
},
self.padding.explicit, self.padding.explicit,
}); });