Merge pull request #982 from mitchellh/font-metrics

font: faces use primary grid metrics to better line up glyphs
This commit is contained in:
Mitchell Hashimoto
2023-12-02 10:34:15 -08:00
committed by GitHub
5 changed files with 104 additions and 49 deletions

View File

@ -71,10 +71,11 @@ pub const Variation = struct {
/// Additional options for rendering glyphs.
pub const RenderOptions = struct {
/// The maximum height of the glyph. If this is set, then any glyph
/// larger than this height will be shrunk to this height. The scaling
/// is typically naive, but ultimately up to the rasterizer.
max_height: ?u16 = null,
/// The metrics that are defining the grid layout. These are usually
/// the metrics of the primary font face. The grid metrics are used
/// by the font face to better layout the glyph in situations where
/// 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
/// 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: {
// 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.
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
// ADD here because CoreText y is UP.

View File

@ -226,6 +226,8 @@ pub const Face = struct {
glyph_index: u32,
opts: font.face.RenderOptions,
) !Glyph {
const metrics = opts.grid_metrics orelse self.metrics;
// If our glyph has color, we want to render the color
try self.face.loadGlyph(glyph_index, .{
.render = true,
@ -288,7 +290,7 @@ pub const Face = struct {
// and copy the atlas.
const bitmap_original = bitmap_converted orelse bitmap_ft;
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;
if (bm.rows <= max) break :resized null;
@ -425,7 +427,7 @@ pub const Face = struct {
// baseline calculation. The baseline calculation is so that everything
// 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.
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={}", .{
@ -662,7 +664,15 @@ test "color emoji" {
// resize
{
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);
}

View File

@ -56,8 +56,8 @@ config: DerivedConfig,
/// The mailbox for communicating with the window.
surface_mailbox: apprt.surface.Mailbox,
/// Current cell dimensions for this grid.
cell_size: renderer.CellSize,
/// Current font metrics defining our grid.
grid_metrics: font.face.Metrics,
/// Current screen size dimensions for this grid. This is set on the first
/// resize event, and is not immediately available.
@ -359,7 +359,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.alloc = alloc,
.config = options.config,
.surface_mailbox = options.surface_mailbox,
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
.grid_metrics = metrics,
.screen_size = null,
.padding = options.padding,
.focused = true,
@ -497,7 +497,10 @@ fn gridSize(self: *Metal) ?renderer.GridSize {
const screen_size = self.screen_size orelse return null;
return renderer.GridSize.init(
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);
break :metrics face.metrics;
};
const new_cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height };
// Update our uniforms
self.uniforms = .{
.projection_matrix = self.uniforms.projection_matrix,
.cell_size = .{
@floatFromInt(new_cell_size.width),
@floatFromInt(new_cell_size.height),
@floatFromInt(metrics.cell_width),
@floatFromInt(metrics.cell_height),
},
.strikethrough_position = @floatFromInt(metrics.strikethrough_position),
.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
// nothing since the grid size couldn't have possibly changed.
if (std.meta.eql(self.cell_size, new_cell_size)) return;
self.cell_size = new_cell_size;
if (std.meta.eql(self.grid_metrics, metrics)) return;
self.grid_metrics = metrics;
// Set the sprite font up
self.font_group.group.sprite = font.sprite.Face{
.width = self.cell_size.width,
.height = self.cell_size.height,
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.underline_thickness * @as(u32, if (self.config.font_thicken) 2 else 1),
.underline_position = metrics.underline_position,
};
// Notify the window that the cell size changed.
_ = self.surface_mailbox.push(.{
.cell_size = new_cell_size,
.cell_size = .{
.width = metrics.cell_width,
.height = metrics.cell_height,
},
}, .{ .forever = {} });
}
@ -1140,7 +1145,7 @@ fn prepKittyGraphics(
// 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_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);
} else 0;
@ -1181,8 +1186,8 @@ fn prepKittyGraphics(
image.height -| offset_y;
// 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_height = if (p.rows > 0) p.rows * self.cell_size.height else source_height;
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.grid_metrics.cell_height else source_height;
// Accumulate the placement
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
// and we split them equal across all boundaries.
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
self.padding.explicit;
const padded_dim = dim.subPadding(padding);
@ -1307,8 +1319,8 @@ pub fn setScreenSize(
-1 * @as(f32, @floatFromInt(padding.top)),
),
.cell_size = .{
@floatFromInt(self.cell_size.width),
@floatFromInt(self.cell_size.height),
@floatFromInt(self.grid_metrics.cell_width),
@floatFromInt(self.grid_metrics.cell_height),
},
.strikethrough_position = old.strikethrough_position,
.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).
@ -1728,7 +1740,7 @@ pub fn updateCell(
shaper_run.font_index,
shaper_cell.glyph_index,
.{
.max_height = @intCast(self.cell_size.height),
.grid_metrics = self.grid_metrics,
.thicken = self.config.font_thicken,
},
);
@ -1766,7 +1778,10 @@ pub fn updateCell(
self.alloc,
font.sprite_index,
@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;
@ -1839,7 +1854,10 @@ fn addCursor(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{ .cell_width = if (wide) 2 else 1 },
.{
.cell_width = if (wide) 2 else 1,
.grid_metrics = self.grid_metrics,
},
) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err});
return null;
@ -1894,7 +1912,7 @@ fn addPreeditCell(
self.alloc,
font_index,
glyph_index,
.{},
.{ .grid_metrics = self.grid_metrics },
) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err});
return;

View File

@ -47,8 +47,8 @@ alloc: std.mem.Allocator,
/// The configuration we need derived from the main config.
config: DerivedConfig,
/// Current cell dimensions for this grid.
cell_size: renderer.CellSize,
/// Current font metrics defining our grid.
grid_metrics: font.face.Metrics,
/// Current screen size dimensions for this grid. This is set on the first
/// resize event, and is not immediately available.
@ -128,7 +128,14 @@ const SetScreenSize = struct {
// Apply our padding
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
r.padding.explicit;
const padded_size = self.size.subPadding(padding);
@ -137,7 +144,10 @@ const SetScreenSize = struct {
padded_size,
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,
});
@ -340,7 +350,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.config = options.config,
.cells_bg = .{},
.cells = .{},
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
.grid_metrics = metrics,
.screen_size = null,
.gl_state = gl_state,
.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
// 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.cell_size, new_cell_size)) return;
self.cell_size = new_cell_size;
if (std.meta.eql(self.grid_metrics, metrics)) return;
self.grid_metrics = metrics;
// Notify the window that the cell size changed.
_ = self.surface_mailbox.push(.{
.cell_size = new_cell_size,
.cell_size = .{
.width = metrics.cell_width,
.height = metrics.cell_height,
},
}, .{ .forever = {} });
}
@ -780,7 +792,7 @@ fn prepKittyGraphics(
// 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_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);
} else 0;
@ -821,8 +833,8 @@ fn prepKittyGraphics(
image.height -| offset_y;
// 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_height = if (p.rows > 0) p.rows * self.cell_size.height else source_height;
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.grid_metrics.cell_height else source_height;
// Accumulate the placement
if (image.width > 0 and image.height > 0) {
@ -1145,7 +1157,7 @@ fn addPreeditCell(
self.alloc,
font_index,
glyph_index,
.{},
.{ .grid_metrics = self.grid_metrics },
) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err});
return;
@ -1239,7 +1251,10 @@ fn addCursor(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{ .cell_width = if (wide) 2 else 1 },
.{
.grid_metrics = self.grid_metrics,
.cell_width = if (wide) 2 else 1,
},
) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err});
return null;
@ -1431,7 +1446,7 @@ pub fn updateCell(
shaper_run.font_index,
shaper_cell.glyph_index,
.{
.max_height = @intCast(self.cell_size.height),
.grid_metrics = self.grid_metrics,
.thicken = self.config.font_thicken,
},
);
@ -1479,7 +1494,10 @@ pub fn updateCell(
self.alloc,
font.sprite_index,
@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;
@ -1537,7 +1555,10 @@ pub fn updateCell(
fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize {
return renderer.GridSize.init(
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={}", .{
dim,
grid_size,
self.cell_size,
renderer.CellSize{
.width = self.grid_metrics.cell_width,
.height = self.grid_metrics.cell_height,
},
self.padding.explicit,
});