mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 17:26:09 +03:00
font(freetype): constrain emoji to cell width (#6602)
When scaling emoji (with freetype), we would unilaterally scale the bitmap to fit within the `cell_height`. For narrow fonts, this would result in a horizontal overflow:  Modify the glyph rendering such that we scale to fit within the cell width. After doing so, the above image looks like:  The emoji glyph is noticeably smaller because we have constrained the height further than before, but fits perfectly within two cells. I am using Victor Mono as my font, which is pretty narrow. The effect would be even more pronounced on something like Iosevka.
This commit is contained in:
@ -390,13 +390,28 @@ 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 = metrics.cell_height;
|
const original_width = bitmap_original.width;
|
||||||
const bm = bitmap_original;
|
const original_height = bitmap_original.rows;
|
||||||
if (bm.rows <= max) break :resized null;
|
var result = bitmap_original;
|
||||||
|
// TODO: We are limiting this to only emoji. We can rework this after a future
|
||||||
|
// improvement (promised by Qwerasd) which implements more flexible resizing rules. For
|
||||||
|
// now, this will suffice
|
||||||
|
if (self.isColorGlyph(glyph_index) and opts.cell_width != null) {
|
||||||
|
const cell_width = opts.cell_width orelse unreachable;
|
||||||
|
// If we have a cell_width, we constrain the glyph to fit within the cell
|
||||||
|
result.width = metrics.cell_width * @as(u32, cell_width);
|
||||||
|
result.rows = (result.width * original_height) / original_width;
|
||||||
|
} else {
|
||||||
|
// If we don't have a cell_width, we scale to fill vertically
|
||||||
|
result.rows = metrics.cell_height;
|
||||||
|
result.width = (metrics.cell_height * original_width) / original_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we already fit, we don't need to resize
|
||||||
|
if (original_height <= result.rows and original_width <= result.width) {
|
||||||
|
break :resized null;
|
||||||
|
}
|
||||||
|
|
||||||
var result = bm;
|
|
||||||
result.rows = max;
|
|
||||||
result.width = (result.rows * bm.width) / bm.rows;
|
|
||||||
result.pitch = @as(c_int, @intCast(result.width)) * atlas.format.depth();
|
result.pitch = @as(c_int, @intCast(result.width)) * atlas.format.depth();
|
||||||
|
|
||||||
const buf = try alloc.alloc(
|
const buf = try alloc.alloc(
|
||||||
@ -407,10 +422,10 @@ pub const Face = struct {
|
|||||||
errdefer alloc.free(buf);
|
errdefer alloc.free(buf);
|
||||||
|
|
||||||
if (stb.stbir_resize_uint8(
|
if (stb.stbir_resize_uint8(
|
||||||
bm.buffer,
|
bitmap_original.buffer,
|
||||||
@intCast(bm.width),
|
@intCast(original_width),
|
||||||
@intCast(bm.rows),
|
@intCast(original_height),
|
||||||
bm.pitch,
|
bitmap_original.pitch,
|
||||||
result.buffer,
|
result.buffer,
|
||||||
@intCast(result.width),
|
@intCast(result.width),
|
||||||
@intCast(result.rows),
|
@intCast(result.rows),
|
||||||
@ -520,7 +535,7 @@ pub const Face = struct {
|
|||||||
// NOTE(mitchellh): I don't know if this is right, this doesn't
|
// NOTE(mitchellh): I don't know if this is right, this doesn't
|
||||||
// _feel_ right, but it makes all my limited test cases work.
|
// _feel_ right, but it makes all my limited test cases work.
|
||||||
if (self.face.hasColor() and !self.face.isScalable()) {
|
if (self.face.hasColor() and !self.face.isScalable()) {
|
||||||
break :offset_y @intCast(tgt_h);
|
break :offset_y @intCast(tgt_h + (metrics.cell_height -| tgt_h) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Y offset is the offset of the top of our bitmap PLUS our
|
// The Y offset is the offset of the top of our bitmap PLUS our
|
||||||
|
@ -1392,29 +1392,29 @@ pub fn rebuildCells(
|
|||||||
// Try to read the cells from the shaping cache if we can.
|
// Try to read the cells from the shaping cache if we can.
|
||||||
self.font_shaper_cache.get(run) orelse
|
self.font_shaper_cache.get(run) orelse
|
||||||
cache: {
|
cache: {
|
||||||
// Otherwise we have to shape them.
|
// Otherwise we have to shape them.
|
||||||
const cells = try self.font_shaper.shape(run);
|
const cells = try self.font_shaper.shape(run);
|
||||||
|
|
||||||
// Try to cache them. If caching fails for any reason we
|
// Try to cache them. If caching fails for any reason we
|
||||||
// continue because it is just a performance optimization,
|
// continue because it is just a performance optimization,
|
||||||
// not a correctness issue.
|
// not a correctness issue.
|
||||||
self.font_shaper_cache.put(
|
self.font_shaper_cache.put(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
run,
|
run,
|
||||||
cells,
|
cells,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.warn(
|
log.warn(
|
||||||
"error caching font shaping results err={}",
|
"error caching font shaping results err={}",
|
||||||
.{err},
|
.{err},
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The cells we get from direct shaping are always owned
|
||||||
|
// by the shaper and valid until the next shaping call so
|
||||||
|
// we can safely use them.
|
||||||
|
break :cache cells;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The cells we get from direct shaping are always owned
|
|
||||||
// by the shaper and valid until the next shaping call so
|
|
||||||
// we can safely use them.
|
|
||||||
break :cache cells;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Advance our index until we reach or pass
|
// Advance our index until we reach or pass
|
||||||
// our current x position in the shaper cells.
|
// our current x position in the shaper cells.
|
||||||
while (shaper_cells.?[shaper_cells_i].x < x) {
|
while (shaper_cells.?[shaper_cells_i].x < x) {
|
||||||
@ -1637,29 +1637,29 @@ pub fn rebuildCells(
|
|||||||
// Try to read the cells from the shaping cache if we can.
|
// Try to read the cells from the shaping cache if we can.
|
||||||
self.font_shaper_cache.get(run) orelse
|
self.font_shaper_cache.get(run) orelse
|
||||||
cache: {
|
cache: {
|
||||||
// Otherwise we have to shape them.
|
// Otherwise we have to shape them.
|
||||||
const cells = try self.font_shaper.shape(run);
|
const cells = try self.font_shaper.shape(run);
|
||||||
|
|
||||||
// Try to cache them. If caching fails for any reason we
|
// Try to cache them. If caching fails for any reason we
|
||||||
// continue because it is just a performance optimization,
|
// continue because it is just a performance optimization,
|
||||||
// not a correctness issue.
|
// not a correctness issue.
|
||||||
self.font_shaper_cache.put(
|
self.font_shaper_cache.put(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
run,
|
run,
|
||||||
cells,
|
cells,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.warn(
|
log.warn(
|
||||||
"error caching font shaping results err={}",
|
"error caching font shaping results err={}",
|
||||||
.{err},
|
.{err},
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The cells we get from direct shaping are always owned
|
||||||
|
// by the shaper and valid until the next shaping call so
|
||||||
|
// we can safely use them.
|
||||||
|
break :cache cells;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The cells we get from direct shaping are always owned
|
|
||||||
// by the shaper and valid until the next shaping call so
|
|
||||||
// we can safely use them.
|
|
||||||
break :cache cells;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cells = shaper_cells orelse break :glyphs;
|
const cells = shaper_cells orelse break :glyphs;
|
||||||
|
|
||||||
// If there are no shaper cells for this run, ignore it.
|
// If there are no shaper cells for this run, ignore it.
|
||||||
@ -2105,6 +2105,7 @@ fn addGlyph(
|
|||||||
shaper_run.font_index,
|
shaper_run.font_index,
|
||||||
shaper_cell.glyph_index,
|
shaper_cell.glyph_index,
|
||||||
.{
|
.{
|
||||||
|
.cell_width = if (cell.wide == .wide) 2 else 1,
|
||||||
.grid_metrics = self.grid_metrics,
|
.grid_metrics = self.grid_metrics,
|
||||||
.thicken = self.config.font_thicken,
|
.thicken = self.config.font_thicken,
|
||||||
.thicken_strength = self.config.font_thicken_strength,
|
.thicken_strength = self.config.font_thicken_strength,
|
||||||
|
Reference in New Issue
Block a user