mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
font(coretext): improve atlas padding calculations
- Simplifies and clarifies the math for how the bounding box for rendered glyphs is computed - Reduces margin from 2px between glyphs to 1px by only padding the bottom and right side of each glyph - Avoids excessive padding to glyph box when font thicken is enabled or when using a synthetic bold (it was previously 4x as much padding as necessary in some cases)
This commit is contained in:

committed by
Mitchell Hashimoto

parent
4ca6413ec9
commit
0e21293d43
@ -292,31 +292,45 @@ pub const Face = struct {
|
|||||||
var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)};
|
var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)};
|
||||||
|
|
||||||
// Get the bounding rect for rendering this glyph.
|
// Get the bounding rect for rendering this glyph.
|
||||||
const rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null);
|
// This is in a coordinate space with (0.0, 0.0)
|
||||||
|
// in the bottom left and +Y pointing up.
|
||||||
|
var rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null);
|
||||||
|
|
||||||
// The x/y that we render the glyph at. The Y value has to be flipped
|
// If we're rendering a synthetic bold then we will gain 50% of
|
||||||
// because our coordinates in 3D space are (0, 0) bottom left with
|
// the line width on every edge, which means we should increase
|
||||||
// +y being up.
|
// our width and height by the line width and subtract half from
|
||||||
const render_x = @floor(rect.origin.x);
|
// our origin points.
|
||||||
const render_y = @ceil(-rect.origin.y);
|
if (self.synthetic_bold) |line_width| {
|
||||||
|
rect.size.width += line_width;
|
||||||
|
rect.size.height += line_width;
|
||||||
|
rect.origin.x -= line_width / 2;
|
||||||
|
rect.origin.y -= line_width / 2;
|
||||||
|
}
|
||||||
|
|
||||||
// The ascent is the amount of pixels above the baseline this glyph
|
// We make an assumption that font smoothing ("thicken")
|
||||||
// is rendered. The ascent can be calculated by adding the full
|
// adds no more than 1 extra pixel to any edge. We don't
|
||||||
// glyph height to the origin.
|
// add extra size if it's a sbix color font though, since
|
||||||
const glyph_ascent = @ceil(rect.size.height + rect.origin.y);
|
// bitmaps aren't affected by smoothing.
|
||||||
|
const sbix = self.color != null and self.color.?.sbix;
|
||||||
|
if (opts.thicken and !sbix) {
|
||||||
|
rect.size.width += 2.0;
|
||||||
|
rect.size.height += 2.0;
|
||||||
|
rect.origin.x -= 1.0;
|
||||||
|
rect.origin.y -= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
// The glyph height is basically rect.size.height but we do the
|
// We compute the minimum and maximum x and y values.
|
||||||
// ascent plus the descent because both are rounded elements that
|
// We round our min points down and max points up.
|
||||||
// will make us more accurate.
|
const x0: i32, const x1: i32, const y0: i32, const y1: i32 = .{
|
||||||
const height: u32 = @intFromFloat(glyph_ascent + render_y);
|
@intFromFloat(@floor(rect.origin.x)),
|
||||||
|
@intFromFloat(@ceil(rect.origin.x) + @ceil(rect.size.width)),
|
||||||
// The glyph width is our advertised bounding with plus the rounding
|
@intFromFloat(@floor(rect.origin.y)),
|
||||||
// difference from our rendering X.
|
@intFromFloat(@ceil(rect.origin.y) + @ceil(rect.size.height)),
|
||||||
const width: u32 = @intFromFloat(@ceil(rect.size.width + (rect.origin.x - render_x)));
|
};
|
||||||
|
|
||||||
// This bitmap is blank. I've seen it happen in a font, I don't know why.
|
// This bitmap is blank. I've seen it happen in a font, I don't know why.
|
||||||
// If it is empty, we just return a valid glyph struct that does nothing.
|
// If it is empty, we just return a valid glyph struct that does nothing.
|
||||||
if (width == 0 or height == 0) return font.Glyph{
|
if (x1 <= x0 or y1 <= y0) return font.Glyph{
|
||||||
.width = 0,
|
.width = 0,
|
||||||
.height = 0,
|
.height = 0,
|
||||||
.offset_x = 0,
|
.offset_x = 0,
|
||||||
@ -326,25 +340,8 @@ pub const Face = struct {
|
|||||||
.advance_x = 0,
|
.advance_x = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Additional padding we need to add to the bitmap context itself
|
const width: u32 = @intCast(x1 - x0);
|
||||||
// due to the glyph being larger than standard.
|
const height: u32 = @intCast(y1 - y0);
|
||||||
const padding_ctx: u32 = padding_ctx: {
|
|
||||||
// If we're doing thicken, then getBoundsForGlyphs does not take
|
|
||||||
// into account the anti-aliasing that will be added to the glyph.
|
|
||||||
// We need to add some padding to allow that to happen. A padding of
|
|
||||||
// 2 is usually enough for anti-aliasing.
|
|
||||||
var result: u32 = if (opts.thicken) 2 else 0;
|
|
||||||
|
|
||||||
// If we have a synthetic bold, add padding for the stroke width
|
|
||||||
if (self.synthetic_bold) |line_width| {
|
|
||||||
// x2 for top and bottom padding
|
|
||||||
result += @intFromFloat(@ceil(line_width) * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
break :padding_ctx result;
|
|
||||||
};
|
|
||||||
const padded_width: u32 = width + (padding_ctx * 2);
|
|
||||||
const padded_height: u32 = height + (padding_ctx * 2);
|
|
||||||
|
|
||||||
// Settings that are specific to if we are rendering text or emoji.
|
// Settings that are specific to if we are rendering text or emoji.
|
||||||
const color: struct {
|
const color: struct {
|
||||||
@ -380,17 +377,17 @@ pub const Face = struct {
|
|||||||
// usually stabilizes pretty quickly and is very infrequent so I think
|
// usually stabilizes pretty quickly and is very infrequent so I think
|
||||||
// the allocation overhead is acceptable compared to the cost of
|
// the allocation overhead is acceptable compared to the cost of
|
||||||
// caching it forever or having to deal with a cache lifetime.
|
// caching it forever or having to deal with a cache lifetime.
|
||||||
const buf = try alloc.alloc(u8, padded_width * padded_height * color.depth);
|
const buf = try alloc.alloc(u8, width * height * color.depth);
|
||||||
defer alloc.free(buf);
|
defer alloc.free(buf);
|
||||||
@memset(buf, 0);
|
@memset(buf, 0);
|
||||||
|
|
||||||
const context = macos.graphics.BitmapContext.context;
|
const context = macos.graphics.BitmapContext.context;
|
||||||
const ctx = try macos.graphics.BitmapContext.create(
|
const ctx = try macos.graphics.BitmapContext.create(
|
||||||
buf,
|
buf,
|
||||||
padded_width,
|
width,
|
||||||
padded_height,
|
height,
|
||||||
8,
|
8,
|
||||||
padded_width * color.depth,
|
width * color.depth,
|
||||||
color.space,
|
color.space,
|
||||||
color.context_opts,
|
color.context_opts,
|
||||||
);
|
);
|
||||||
@ -405,8 +402,8 @@ pub const Face = struct {
|
|||||||
context.fillRect(ctx, .{
|
context.fillRect(ctx, .{
|
||||||
.origin = .{ .x = 0, .y = 0 },
|
.origin = .{ .x = 0, .y = 0 },
|
||||||
.size = .{
|
.size = .{
|
||||||
.width = @floatFromInt(padded_width),
|
.width = @floatFromInt(width),
|
||||||
.height = @floatFromInt(padded_height),
|
.height = @floatFromInt(height),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -437,67 +434,57 @@ pub const Face = struct {
|
|||||||
|
|
||||||
// We want to render the glyphs at (0,0), but the glyphs themselves
|
// We want to render the glyphs at (0,0), but the glyphs themselves
|
||||||
// are offset by bearings, so we have to undo those bearings in order
|
// are offset by bearings, so we have to undo those bearings in order
|
||||||
// to get them to 0,0. We also add the padding so that they render
|
// to get them to 0,0.
|
||||||
// slightly off the edge of the bitmap.
|
|
||||||
const padding_ctx_f64: f64 = @floatFromInt(padding_ctx);
|
|
||||||
self.font.drawGlyphs(&glyphs, &.{
|
self.font.drawGlyphs(&glyphs, &.{
|
||||||
.{
|
.{
|
||||||
.x = -1 * (render_x - padding_ctx_f64),
|
.x = @floatFromInt(-x0),
|
||||||
.y = render_y + padding_ctx_f64,
|
.y = @floatFromInt(-y0),
|
||||||
},
|
},
|
||||||
}, ctx);
|
}, ctx);
|
||||||
|
|
||||||
const region = region: {
|
const region = region: {
|
||||||
// We need to add a 1px padding to the font so that we don't
|
// We reserve a region that's 1px wider and taller than we need
|
||||||
// get fuzzy issues when blending textures.
|
// in order to create a 1px separation between adjacent glyphs
|
||||||
const padding = 1;
|
// to prevent interpolation with adjacent glyphs while sampling
|
||||||
|
// from the atlas.
|
||||||
// Get the full padded region
|
|
||||||
var region = try atlas.reserve(
|
var region = try atlas.reserve(
|
||||||
alloc,
|
alloc,
|
||||||
padded_width + (padding * 2), // * 2 because left+right
|
width + 1,
|
||||||
padded_height + (padding * 2), // * 2 because top+bottom
|
height + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Modify the region so that we remove the padding so that
|
// We adjust the region width and height back down since we
|
||||||
// we write to the non-zero location. The data in an Altlas
|
// don't need the extra pixel, we just needed to reserve it
|
||||||
// is always initialized to zero (Atlas.clear) so we don't
|
// so that it isn't used for other glyphs in the future.
|
||||||
// need to worry about zero-ing that.
|
region.width -= 1;
|
||||||
region.x += padding;
|
region.height -= 1;
|
||||||
region.y += padding;
|
|
||||||
region.width -= padding * 2;
|
|
||||||
region.height -= padding * 2;
|
|
||||||
break :region region;
|
break :region region;
|
||||||
};
|
};
|
||||||
atlas.set(region, buf);
|
atlas.set(region, buf);
|
||||||
|
|
||||||
const metrics = opts.grid_metrics orelse self.metrics;
|
const metrics = opts.grid_metrics orelse self.metrics;
|
||||||
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(metrics.cell_baseline);
|
|
||||||
|
|
||||||
// Next we offset our baseline by the bearing in the font. We
|
// This should be the distance from the bottom of
|
||||||
// ADD here because CoreText y is UP.
|
// the cell to the top of the glyph's bounding box.
|
||||||
const baseline_with_offset = baseline_from_bottom + glyph_ascent;
|
//
|
||||||
|
// The calculation is distance from bottom of cell to
|
||||||
// Add our context padding we may have created.
|
// baseline plus distance from baseline to top of glyph.
|
||||||
const baseline_with_padding = baseline_with_offset + padding_ctx_f64;
|
const offset_y: i32 = @as(i32, @intCast(metrics.cell_baseline)) + y1;
|
||||||
|
|
||||||
break :offset_y @intFromFloat(@ceil(baseline_with_padding));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// This should be the distance from the left of
|
||||||
|
// the cell to the left of the glyph's bounding box.
|
||||||
const offset_x: i32 = offset_x: {
|
const offset_x: i32 = offset_x: {
|
||||||
// Don't forget to apply our context padding if we have one
|
var result: i32 = x0;
|
||||||
var result: i32 = @intFromFloat(render_x - padding_ctx_f64);
|
|
||||||
|
|
||||||
// If our cell was resized to be wider then we center our
|
// If our cell was resized then we adjust our glyph's
|
||||||
// glyph in the cell.
|
// position relative to the new center. This keeps glyphs
|
||||||
|
// centered in the cell whether it was made wider or narrower.
|
||||||
if (metrics.original_cell_width) |original_width| {
|
if (metrics.original_cell_width) |original_width| {
|
||||||
if (original_width < metrics.cell_width) {
|
const before: i32 = @intCast(original_width);
|
||||||
const diff = (metrics.cell_width - original_width) / 2;
|
const after: i32 = @intCast(metrics.cell_width);
|
||||||
result += @intCast(diff);
|
// Increase the offset by half of the difference
|
||||||
}
|
// between the widths to keep things centered.
|
||||||
|
result += @divTrunc(after - before, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
break :offset_x result;
|
break :offset_x result;
|
||||||
@ -507,21 +494,9 @@ pub const Face = struct {
|
|||||||
var advances: [glyphs.len]macos.graphics.Size = undefined;
|
var advances: [glyphs.len]macos.graphics.Size = undefined;
|
||||||
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
|
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
|
||||||
|
|
||||||
// std.log.warn("renderGlyph rect={} width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{
|
|
||||||
// rect,
|
|
||||||
// width,
|
|
||||||
// height,
|
|
||||||
// render_x,
|
|
||||||
// render_y,
|
|
||||||
// offset_y,
|
|
||||||
// glyph_ascent,
|
|
||||||
// self.metrics.cell_height,
|
|
||||||
// self.metrics.cell_baseline,
|
|
||||||
// });
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.width = padded_width,
|
.width = width,
|
||||||
.height = padded_height,
|
.height = height,
|
||||||
.offset_x = offset_x,
|
.offset_x = offset_x,
|
||||||
.offset_y = offset_y,
|
.offset_y = offset_y,
|
||||||
.atlas_x = region.x,
|
.atlas_x = region.x,
|
||||||
|
Reference in New Issue
Block a user