mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Fix emoji size and atlas padding (#3016)
Fixes a bug where emojis got smaller when font thicken was enabled because they were constrained for some reason, here demonstrated by artificially narrowing the cells with `adjust-cell-width=-45%`: |`font-thicken`|main|this PR| |-|-|-| |`false`|<img width="328" alt="image" src="https://github.com/user-attachments/assets/c5c25d94-ea73-47e1-80e9-ed3baff76511" />|<img width="328" alt="image" src="https://github.com/user-attachments/assets/f74a35e1-ab7e-470d-b873-f33e63c1c9bb" />| |`true`|<img width="328" alt="image" src="https://github.com/user-attachments/assets/e7c1e096-4a54-456c-afb0-5adcb94eaf00" />|<img width="328" alt="image" src="https://github.com/user-attachments/assets/80d3d7dd-f572-489a-ab56-d2d5ca9f3508" />| And reworks the CoreText atlas margin/padding calculations to be more efficient, you can see comparisons with and without thicken both before and after this change here: <details> <summary>Atlas Textures</summary> ### main |`font-thicken`|gray|color| |-|-|-| |`false`||| |`true`||| ### this PR |`font-thicken`|gray|color| |-|-|-| |`false`||| |`true`||| </details> The atlas padding calculations were also affecting the issue with the emojis getting smaller when thicken was enabled, because they were given extra space to account for the thickening but they're bitmap so they don't get affected by thickening.
This commit is contained in:
@ -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,
|
||||||
|
@ -256,9 +256,7 @@ vertex CellTextVertexOut cell_text_vertex(
|
|||||||
offset.y = uniforms.cell_size.y - offset.y;
|
offset.y = uniforms.cell_size.y - offset.y;
|
||||||
|
|
||||||
// If we're constrained then we need to scale the glyph.
|
// If we're constrained then we need to scale the glyph.
|
||||||
// We also always constrain colored glyphs since we should have
|
if (in.mode == MODE_TEXT_CONSTRAINED) {
|
||||||
// their scaled cell size exactly correct.
|
|
||||||
if (in.mode == MODE_TEXT_CONSTRAINED || in.mode == MODE_TEXT_COLOR) {
|
|
||||||
float max_width = uniforms.cell_size.x * in.constraint_width;
|
float max_width = uniforms.cell_size.x * in.constraint_width;
|
||||||
if (size.x > max_width) {
|
if (size.x > max_width) {
|
||||||
float new_y = size.y * (max_width / size.x);
|
float new_y = size.y * (max_width / size.x);
|
||||||
|
@ -208,10 +208,8 @@ void main() {
|
|||||||
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
|
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
|
||||||
|
|
||||||
// If this is a constrained mode, we need to constrain it!
|
// If this is a constrained mode, we need to constrain it!
|
||||||
// We also always constrain colored glyphs since we should have
|
|
||||||
// their scaled cell size exactly correct.
|
|
||||||
vec2 glyph_size_calc = glyph_size;
|
vec2 glyph_size_calc = glyph_size;
|
||||||
if (mode == MODE_FG_CONSTRAINED || mode == MODE_FG_COLOR) {
|
if (mode == MODE_FG_CONSTRAINED) {
|
||||||
if (glyph_size.x > cell_size_scaled.x) {
|
if (glyph_size.x > cell_size_scaled.x) {
|
||||||
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
|
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
|
||||||
glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2);
|
glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2);
|
||||||
|
Reference in New Issue
Block a user