Merge pull request #165 from mitchellh/coretext

CoreText Rasterizer
This commit is contained in:
Mitchell Hashimoto
2023-07-01 10:17:12 -07:00
committed by GitHub
15 changed files with 387 additions and 143 deletions

View File

@ -26,6 +26,27 @@ pub fn Context(comptime T: type) type {
);
}
pub fn setAllowsFontSmoothing(self: *T, v: bool) void {
c.CGContextSetAllowsFontSmoothing(
@ptrCast(self),
v,
);
}
pub fn setAllowsFontSubpixelPositioning(self: *T, v: bool) void {
c.CGContextSetAllowsFontSubpixelPositioning(
@ptrCast(self),
v,
);
}
pub fn setAllowsFontSubpixelQuantization(self: *T, v: bool) void {
c.CGContextSetAllowsFontSubpixelQuantization(
@ptrCast(self),
v,
);
}
pub fn setShouldAntialias(self: *T, v: bool) void {
c.CGContextSetShouldAntialias(
@ptrCast(self),
@ -40,6 +61,20 @@ pub fn Context(comptime T: type) type {
);
}
pub fn setShouldSubpixelPositionFonts(self: *T, v: bool) void {
c.CGContextSetShouldSubpixelPositionFonts(
@ptrCast(self),
v,
);
}
pub fn setShouldSubpixelQuantizeFonts(self: *T, v: bool) void {
c.CGContextSetShouldSubpixelQuantizeFonts(
@ptrCast(self),
v,
);
}
pub fn setGrayFillColor(self: *T, gray: f64, alpha: f64) void {
c.CGContextSetGrayFillColor(
@ptrCast(self),
@ -66,6 +101,16 @@ pub fn Context(comptime T: type) type {
);
}
pub fn setRGBStrokeColor(self: *T, r: f64, g: f64, b: f64, alpha: f64) void {
c.CGContextSetRGBStrokeColor(
@ptrCast(self),
r,
g,
b,
alpha,
);
}
pub fn setTextDrawingMode(self: *T, mode: TextDrawingMode) void {
c.CGContextSetTextDrawingMode(
@ptrCast(self),

View File

@ -130,6 +130,14 @@ pub const Font = opaque {
pub fn getUnderlineThickness(self: *Font) f64 {
return c.CTFontGetUnderlineThickness(@ptrCast(self));
}
pub fn getUnitsPerEm(self: *Font) u32 {
return c.CTFontGetUnitsPerEm(@ptrCast(self));
}
pub fn getSize(self: *Font) f64 {
return c.CTFontGetSize(@ptrCast(self));
}
};
pub const FontOrientation = enum(c_uint) {

View File

@ -30,30 +30,12 @@ pub const Line = opaque {
self: *Line,
opts: LineBoundsOptions,
) graphics.Rect {
// return @bitCast(c.CGRect, c.CTLineGetBoundsWithOptions(
// @ptrCast(c.CTLineRef, self),
// opts.cval(),
// ));
// We have to use a custom C wrapper here because there is some
// C ABI issue happening.
var result: graphics.Rect = undefined;
zig_cabi_CTLineGetBoundsWithOptions(
return @bitCast(c.CTLineGetBoundsWithOptions(
@ptrCast(self),
opts.cval(),
@ptrCast(&result),
);
return result;
));
}
// See getBoundsWithOptions
extern "c" fn zig_cabi_CTLineGetBoundsWithOptions(
c.CTLineRef,
c.CTLineBoundsOptions,
*c.CGRect,
) void;
pub fn getTypographicBounds(
self: *Line,
ascent: ?*f64,

View File

@ -231,7 +231,7 @@ pub fn init(
if (try disco_it.next()) |face| {
log.info("font regular: {s}", .{try face.name()});
try group.addFace(alloc, .regular, face);
}
} else std.log.warn("font-family not found: {s}", .{family});
}
if (config.@"font-family-bold") |family| {
var disco_it = try disco.discover(.{
@ -243,7 +243,7 @@ pub fn init(
if (try disco_it.next()) |face| {
log.info("font bold: {s}", .{try face.name()});
try group.addFace(alloc, .bold, face);
}
} else std.log.warn("font-family-bold not found: {s}", .{family});
}
if (config.@"font-family-italic") |family| {
var disco_it = try disco.discover(.{
@ -255,7 +255,7 @@ pub fn init(
if (try disco_it.next()) |face| {
log.info("font italic: {s}", .{try face.name()});
try group.addFace(alloc, .italic, face);
}
} else std.log.warn("font-family-italic not found: {s}", .{family});
}
if (config.@"font-family-bold-italic") |family| {
var disco_it = try disco.discover(.{
@ -268,7 +268,7 @@ pub fn init(
if (try disco_it.next()) |face| {
log.info("font bold+italic: {s}", .{try face.name()});
try group.addFace(alloc, .bold_italic, face);
}
} else std.log.warn("font-family-bold-italic not found: {s}", .{family});
}
}

View File

@ -34,6 +34,10 @@ pub const Config = struct {
else => 12,
},
/// Draw fonts with a thicker stroke, if supported. This is only supported
/// currently on macOS.
@"font-thicken": bool = false,
/// Background color for the window.
background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },

View File

@ -269,7 +269,7 @@ pub fn renderGlyph(
atlas: *font.Atlas,
index: FontIndex,
glyph_index: u32,
max_height: ?u16,
opts: font.face.RenderOptions,
) !Glyph {
// Special-case fonts are rendered directly.
if (index.special()) |sp| switch (sp) {
@ -282,7 +282,9 @@ pub fn renderGlyph(
const face = &self.faces.get(index.style).items[@intCast(index.idx)];
try face.load(self.lib, self.size);
return try face.face.?.renderGlyph(alloc, atlas, glyph_index, max_height);
const glyph = try face.face.?.renderGlyph(alloc, atlas, glyph_index, opts);
// log.warn("GLYPH={}", .{glyph});
return glyph;
}
/// The wasm-compatible API.
@ -381,7 +383,9 @@ pub const Wasm = struct {
) !*Glyph {
const idx = @as(FontIndex, @bitCast(@as(u8, @intCast(idx_))));
const max_height = if (max_height_ <= 0) null else max_height_;
const glyph = try self.renderGlyph(alloc, atlas, idx, cp, max_height);
const glyph = try self.renderGlyph(alloc, atlas, idx, cp, .{
.max_height = max_height,
});
var result = try alloc.create(Glyph);
errdefer alloc.destroy(result);
@ -425,7 +429,7 @@ test {
&atlas_greyscale,
idx,
glyph_index,
null,
.{},
);
}
@ -481,7 +485,7 @@ test "box glyph" {
&atlas_greyscale,
idx,
0x2500,
null,
.{},
);
try testing.expectEqual(@as(u32, 36), glyph.height);
}
@ -512,7 +516,7 @@ test "resize" {
&atlas_greyscale,
idx,
glyph_index,
null,
.{},
);
try testing.expectEqual(@as(u32, 11), glyph.height);
@ -529,7 +533,7 @@ test "resize" {
&atlas_greyscale,
idx,
glyph_index,
null,
.{},
);
try testing.expectEqual(@as(u32, 21), glyph.height);
@ -572,7 +576,7 @@ test "discover monospace with fontconfig and freetype" {
&atlas_greyscale,
idx,
glyph_index,
null,
.{},
);
}
}

View File

@ -122,7 +122,7 @@ pub fn renderGlyph(
alloc: Allocator,
index: Group.FontIndex,
glyph_index: u32,
max_height: ?u16,
opts: font.face.RenderOptions,
) !Glyph {
const key: GlyphKey = .{ .index = index, .glyph = glyph_index };
const gop = try self.glyphs.getOrPut(alloc, key);
@ -140,7 +140,7 @@ pub fn renderGlyph(
atlas,
index,
glyph_index,
max_height,
opts,
) catch |err| switch (err) {
// If the atlas is full, we resize it
error.AtlasFull => blk: {
@ -150,7 +150,7 @@ pub fn renderGlyph(
atlas,
index,
glyph_index,
max_height,
opts,
);
},
@ -203,7 +203,7 @@ test {
alloc,
idx,
glyph_index,
null,
.{},
);
}
@ -225,7 +225,7 @@ test {
alloc,
idx,
glyph_index,
null,
.{},
);
}
}
@ -300,7 +300,9 @@ pub const Wasm = struct {
) !*Glyph {
const idx = @as(Group.FontIndex, @bitCast(@as(u8, @intCast(idx_))));
const max_height = if (max_height_ <= 0) null else max_height_;
const glyph = try self.renderGlyph(alloc, idx, cp, max_height);
const glyph = try self.renderGlyph(alloc, idx, cp, .{
.max_height = max_height,
});
var result = try alloc.create(Glyph);
errdefer alloc.destroy(result);
@ -352,7 +354,7 @@ test "resize" {
alloc,
idx,
glyph_index,
null,
.{},
);
try testing.expectEqual(@as(u32, 11), glyph.height);
@ -368,7 +370,7 @@ test "resize" {
alloc,
idx,
glyph_index,
null,
.{},
);
try testing.expectEqual(@as(u32, 21), glyph.height);

View File

@ -58,6 +58,20 @@ pub const Metrics = struct {
strikethrough_thickness: f32,
};
/// 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,
/// Thicken the glyph. This draws the glyph with a thicker stroke width.
/// This is purely an aesthetic setting.
///
/// This only works with CoreText currently.
thicken: bool = false,
};
pub const Foo = if (options.backend == .coretext) coretext.Face else void;
test {

View File

@ -5,6 +5,8 @@ const macos = @import("macos");
const harfbuzz = @import("harfbuzz");
const font = @import("../main.zig");
const log = std.log.scoped(.font_face);
pub const Face = struct {
/// Our font face
font: *macos.text.Font,
@ -41,8 +43,10 @@ pub const Face = struct {
/// because the font is loaded at a default size during discovery, and then
/// adjusted to the final size for final load.
pub fn initFontCopy(base: *macos.text.Font, size: font.face.DesiredSize) !Face {
// Create a copy
const ct_font = try base.copyWithAttributes(@floatFromInt(size.points), null);
// Create a copy. The copyWithAttributes docs say the size is in points,
// but we need to scale the points by the DPI and to do that we use our
// function called "pixels".
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null);
errdefer ct_font.release();
var hb_font = try harfbuzz.coretext.createFont(ct_font);
@ -93,39 +97,41 @@ pub const Face = struct {
return @intCast(glyphs[0]);
}
/// Render a glyph using the glyph index. The rendered glyph is stored in the
/// given texture atlas.
pub fn renderGlyph(
self: Face,
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
max_height: ?u16,
opts: font.face.RenderOptions,
) !font.Glyph {
// We add a small pixel padding around the edge of our glyph so that
// anti-aliasing and smoothing doesn't cause us to pick up the pixels
// of another glyph when packed into the atlas.
const padding = 1;
_ = max_height;
var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)};
// Get the bounding rect for this glyph to determine the width/height
// of the bitmap. We use the rounded up width/height of the bounding rect.
var bounding: [1]macos.graphics.Rect = undefined;
_ = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, &bounding);
const glyph_width = @as(u32, @intFromFloat(@ceil(bounding[0].size.width)));
const glyph_height = @as(u32, @intFromFloat(@ceil(bounding[0].size.height)));
// Get the bounding rect for rendering this glyph.
const rect = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, null);
// Width and height. Note the padding doubling is because we want
// the padding on both sides (top/bottom, left/right).
const width = glyph_width + (padding * 2);
const height = glyph_height + (padding * 2);
// The x/y that we render the glyph at. The Y value has to be flipped
// because our coordinates in 3D space are (0, 0) bottom left with
// +y being up.
const render_x = @floor(rect.origin.x);
const render_y = @ceil(-rect.origin.y);
// The ascent is the amount of pixels above the baseline this glyph
// is rendered. The ascent can be calculated by adding the full
// glyph height to the origin.
const glyph_ascent = @ceil(rect.size.height + rect.origin.y);
// The glyph height is basically rect.size.height but we do the
// ascent plus the descent because both are rounded elements that
// will make us more accurate.
const height: u32 = @intFromFloat(glyph_ascent + render_y);
// The glyph width is our advertised bounding with plus the rounding
// difference from our rendering X.
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.
// If it is empty, we just return a valid glyph struct that does nothing.
if (glyph_width == 0) return font.Glyph{
if (width == 0 or height == 0) return font.Glyph{
.width = 0,
.height = 0,
.offset_x = 0,
@ -135,77 +141,157 @@ pub const Face = struct {
.advance_x = 0,
};
// Get the advance that we need for the glyph
var advances: [1]macos.graphics.Size = undefined;
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
// Settings that are specific to if we are rendering text or emoji.
const color: struct {
color: bool,
depth: u32,
space: *macos.graphics.ColorSpace,
context_opts: c_uint,
} = if (self.presentation == .text) .{
.color = false,
.depth = 1,
.space = try macos.graphics.ColorSpace.createDeviceGray(),
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) &
@intFromEnum(macos.graphics.ImageAlphaInfo.none),
} else .{
.color = true,
.depth = 4,
.space = try macos.graphics.ColorSpace.createDeviceRGB(),
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.byte_order_32_little) |
@intFromEnum(macos.graphics.ImageAlphaInfo.premultiplied_first),
};
defer color.space.release();
// Our buffer for rendering
// TODO(perf): cache this buffer
// TODO(mitchellh): color is going to require a depth here
var buf = try alloc.alloc(u8, width * height);
// This is just a safety check.
if (atlas.format.depth() != color.depth) {
log.warn("font atlas color depth doesn't equal font color depth atlas={} font={}", .{
atlas.format.depth(),
color.depth,
});
return error.InvalidAtlasFormat;
}
// Our buffer for rendering. We could cache this but glyph rasterization
// usually stabilizes pretty quickly and is very infrequent so I think
// the allocation overhead is acceptable compared to the cost of
// caching it forever or having to deal with a cache lifetime.
var buf = try alloc.alloc(u8, width * height * color.depth);
defer alloc.free(buf);
std.mem.set(u8, buf, 0);
const space = try macos.graphics.ColorSpace.createDeviceGray();
defer space.release();
@memset(buf, 0);
const ctx = try macos.graphics.BitmapContext.create(
buf,
width,
height,
8,
width,
space,
@intFromEnum(macos.graphics.BitmapInfo.alpha_mask) &
@intFromEnum(macos.graphics.ImageAlphaInfo.none),
width * color.depth,
color.space,
color.context_opts,
);
defer ctx.release();
// Perform an initial fill. This ensures that we don't have any
// uninitialized pixels in the bitmap.
if (color.color)
ctx.setRGBFillColor(1, 1, 1, 0)
else
ctx.setGrayFillColor(0, 0);
ctx.fillRect(.{
.origin = .{ .x = 0, .y = 0 },
.size = .{
.width = @floatFromInt(width),
.height = @floatFromInt(height),
},
});
ctx.setAllowsFontSmoothing(true);
ctx.setShouldSmoothFonts(opts.thicken); // The amadeus "enthicken"
ctx.setAllowsFontSubpixelQuantization(true);
ctx.setShouldSubpixelQuantizeFonts(true);
ctx.setAllowsFontSubpixelPositioning(true);
ctx.setShouldSubpixelPositionFonts(true);
ctx.setAllowsAntialiasing(true);
ctx.setShouldAntialias(true);
ctx.setShouldSmoothFonts(true);
ctx.setGrayFillColor(1, 1);
ctx.setGrayStrokeColor(1, 1);
ctx.setTextDrawingMode(.fill_stroke);
ctx.setTextMatrix(macos.graphics.AffineTransform.identity());
ctx.setTextPosition(0, 0);
// Set our color for drawing
if (color.color) {
ctx.setRGBFillColor(1, 1, 1, 1);
ctx.setRGBStrokeColor(1, 1, 1, 1);
} else {
ctx.setGrayFillColor(1, 1);
ctx.setGrayStrokeColor(1, 1);
}
// 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
// to get them to 0,0.
var pos = [_]macos.graphics.Point{.{
.x = padding + (-1 * bounding[0].origin.x),
.y = padding + (-1 * bounding[0].origin.y),
}};
self.font.drawGlyphs(&glyphs, &pos, ctx);
// to get them to 0,0. We also add the padding so that they render
// slightly off the edge of the bitmap.
self.font.drawGlyphs(&glyphs, &.{
.{
.x = -1 * render_x,
.y = render_y,
},
}, ctx);
const region = try atlas.reserve(alloc, width, height);
const region = region: {
// We need to add a 1px padding to the font so that we don't
// get fuzzy issues when blending textures.
const padding = 1;
// Get the full padded region
var region = try atlas.reserve(
alloc,
width + (padding * 2), // * 2 because left+right
height + (padding * 2), // * 2 because top+bottom
);
// Modify the region so that we remove the padding so that
// we write to the non-zero location. The data in an Altlas
// is always initialized to zero (Atlas.clear) so we don't
// need to worry about zero-ing that.
region.x += padding;
region.y += padding;
region.width -= padding * 2;
region.height -= padding * 2;
break :region region;
};
atlas.set(region, buf);
const offset_y = offset_y: {
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 = self.metrics.cell_height - self.metrics.cell_baseline;
// Next we offset our baseline by the bearing in the font. We
// ADD here because CoreText y is UP.
const baseline_with_offset = baseline_from_bottom + bounding[0].origin.y;
const baseline_with_offset = baseline_from_bottom + glyph_ascent;
// Finally, since we're rendering at (0, 0), the glyph will render
// by default below the line. We have to add height (glyph height)
// so that we shift the glyph UP to be on the line, then we add our
// baseline offset to move the glyph further UP to match the baseline.
break :offset_y @as(i32, @intCast(height)) + @as(i32, @intFromFloat(@ceil(baseline_with_offset)));
break :offset_y @intFromFloat(@ceil(baseline_with_offset));
};
return font.Glyph{
.width = glyph_width,
.height = glyph_height,
.offset_x = @intFromFloat(@ceil(bounding[0].origin.x)),
// 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 .{
.width = width,
.height = height,
.offset_x = @intFromFloat(render_x),
.offset_y = offset_y,
.atlas_x = region.x + padding,
.atlas_y = region.y + padding,
.advance_x = @floatCast(advances[0].width),
.atlas_x = region.x,
.atlas_y = region.y,
// This is not used, so we don't bother calculating it. If we
// ever need it, we can calculate it using getAdvancesForGlyph.
.advance_x = 0,
};
}
@ -273,8 +359,32 @@ pub const Face = struct {
defer fs.release();
// Create a rectangle to fit all of this and create a frame of it.
// The rectangle needs to fit all of our text so we use some
// heuristics based on cell_width to calculate it. We are
// VERY generous with our rect here because the text must fit.
const path_rect = rect: {
// The cell width at this point is valid, so let's make it
// fit 50 characters wide.
const width = cell_width * 50;
// We are trying to calculate height so we don't know how
// high to make our frame. Well-behaved fonts will probably
// not have a height greater than 4x the width, so let's just
// generously use that metric to ensure we fit the frame.
const big_cell_height = cell_width * 4;
// If we are fitting about ~50 characters per row, we need
// unit.len / 50 rows to fit all of our text.
const rows = (unit.len / 50) * 2;
// Our final height is the number of rows times our generous height.
const height = rows * big_cell_height;
break :rect macos.graphics.Rect.init(10, 10, width, height);
};
const path = try macos.graphics.MutablePath.create();
path.addRect(null, macos.graphics.Rect.init(10, 10, 200, 200));
path.addRect(null, path_rect);
defer path.release();
const frame = try fs.createFrame(
macos.foundation.Range.init(0, 0),
@ -292,38 +402,53 @@ pub const Face = struct {
const lines = frame.getLines();
const line = lines.getValueAtIndex(macos.text.Line, 0);
// NOTE(mitchellh): For some reason, CTLineGetBoundsWithOptions
// returns garbage and I can't figure out why... so we use the
// raw ascender.
// Get the bounds of the line to determine the ascent.
const bounds = line.getBoundsWithOptions(.{ .exclude_leading = true });
const bounds_ascent = bounds.size.height + bounds.origin.y;
const baseline = @floor(bounds_ascent + 0.5);
var ascent: f64 = 0;
var descent: f64 = 0;
var leading: f64 = 0;
_ = line.getTypographicBounds(&ascent, &descent, &leading);
// This is an alternate approach to the above to calculate the
// baseline by simply using the ascender. Using this approach led
// to less accurate results, but I'm leaving it here for reference.
// var ascent: f64 = 0;
// var descent: f64 = 0;
// var leading: f64 = 0;
// _ = line.getTypographicBounds(&ascent, &descent, &leading);
//std.log.warn("ascent={} descent={} leading={}", .{ ascent, descent, leading });
break :metrics .{
.height = @floatCast(points[0].y - points[1].y),
.ascent = @floatCast(ascent),
.ascent = @floatCast(baseline),
};
};
// All of these metrics are based on our layout above.
const cell_height = layout_metrics.height;
const cell_baseline = layout_metrics.ascent;
const underline_position = @ceil(layout_metrics.ascent -
@as(f32, @floatCast(ct_font.getUnderlinePosition())));
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
const strikethrough_position = cell_baseline * 0.6;
const strikethrough_thickness = underline_thickness;
// std.log.warn("width={d}, height={d} baseline={d} underline_pos={d} underline_thickness={d}", .{
// Underline position is based on our baseline because the font advertised
// underline position is based on a zero baseline. We add a small amount
// to the underline position to make it look better.
const underline_position = @ceil(cell_baseline -
@as(f32, @floatCast(ct_font.getUnderlinePosition())) +
1);
// Note: is this useful?
// const units_per_em = ct_font.getUnitsPerEm();
// const units_per_point = @intToFloat(f64, units_per_em) / ct_font.getSize();
// std.log.warn("font size size={d}", .{ct_font.getSize()});
// std.log.warn("font metrics width={d}, height={d} baseline={d} underline_pos={d} underline_thickness={d}", .{
// cell_width,
// cell_height,
// cell_baseline,
// underline_position,
// underline_thickness,
// });
return font.face.Metrics{
.cell_width = cell_width,
.cell_height = cell_height,
@ -359,7 +484,7 @@ test {
var i: u8 = 32;
while (i < 127) : (i += 1) {
try testing.expect(face.glyphIndex(i) != null);
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, null);
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
}
}
@ -403,6 +528,6 @@ test "in-memory" {
var i: u8 = 32;
while (i < 127) : (i += 1) {
try testing.expect(face.glyphIndex(i) != null);
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, null);
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
}
}

View File

@ -124,7 +124,7 @@ pub const Face = struct {
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
max_height: ?u16,
opts: font.face.RenderOptions,
) !Glyph {
// If our glyph has color, we want to render the color
try self.face.loadGlyph(glyph_index, .{
@ -188,7 +188,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 = max_height orelse break :resized null;
const max = opts.max_height orelse break :resized null;
const bm = bitmap_original;
if (bm.rows <= max) break :resized null;
@ -328,6 +328,14 @@ pub const Face = struct {
break :offset_y glyph_metrics.bitmap_top + @as(c_int, @intFromFloat(self.metrics.cell_baseline));
};
// log.warn("renderGlyph width={} height={} offset_x={} offset_y={} glyph_metrics={}", .{
// tgt_w,
// tgt_h,
// glyph_metrics.bitmap_left,
// offset_y,
// glyph_metrics,
// });
// Store glyph metadata
return Glyph{
.width = tgt_w,
@ -514,16 +522,16 @@ test {
// Generate all visible ASCII
var i: u8 = 32;
while (i < 127) : (i += 1) {
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?, null);
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?, .{});
}
// Test resizing
{
const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, null);
const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{});
try testing.expectEqual(@as(u32, 11), g1.height);
try ft_font.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, null);
const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{});
try testing.expectEqual(@as(u32, 21), g2.height);
}
}
@ -543,11 +551,13 @@ test "color emoji" {
try testing.expectEqual(Presentation.emoji, ft_font.presentation);
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, null);
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{});
// resize
{
const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, 24);
const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{
.max_height = 24,
});
try testing.expectEqual(@as(u32, 24), glyph.height);
}
}
@ -602,5 +612,5 @@ test "mono to rgba" {
defer ft_font.deinit();
// glyph 3 is mono in Noto
_ = try ft_font.renderGlyph(alloc, &atlas, 3, null);
_ = try ft_font.renderGlyph(alloc, &atlas, 3, .{});
}

View File

@ -190,9 +190,9 @@ pub const Face = struct {
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
max_height: ?u16,
opts: font.face.RenderOptions,
) !font.Glyph {
_ = max_height;
_ = opts;
var render = try self.renderGlyphInternal(alloc, glyph_index);
defer render.deinit();
@ -551,7 +551,7 @@ pub const Wasm = struct {
}
fn face_render_glyph_(face: *Face, atlas: *font.Atlas, codepoint: u32) !*font.Glyph {
const glyph = try face.renderGlyph(alloc, atlas, codepoint, null);
const glyph = try face.renderGlyph(alloc, atlas, codepoint, .{});
const result = try alloc.create(font.Glyph);
errdefer alloc.destroy(result);

View File

@ -71,12 +71,10 @@ pub const Backend = enum {
};
}
return if (target.isDarwin()) darwin: {
// On macOS right now, the coretext renderer is still pretty buggy
// so we default to coretext for font discovery and freetype for
// rasterization.
break :darwin .coretext_freetype;
} else .fontconfig_freetype;
// macOS also supports "coretext_freetype" but there is no scenario
// that is the default. It is only used by people who want to
// self-compile Ghostty and prefer the freetype aesthetic.
return if (target.isDarwin()) .coretext else .fontconfig_freetype;
}
// All the functions below can be called at comptime or runtime to

View File

@ -340,7 +340,31 @@ const PixmanImpl = struct {
const width = @as(u32, @intCast(self.image.getWidth()));
const height = @as(u32, @intCast(self.image.getHeight()));
const region = try atlas.reserve(alloc, width, height);
// Allocate our texture atlas region
const region = region: {
// We need to add a 1px padding to the font so that we don't
// get fuzzy issues when blending textures.
const padding = 1;
// Get the full padded region
var region = try atlas.reserve(
alloc,
width + (padding * 2), // * 2 because left+right
height + (padding * 2), // * 2 because top+bottom
);
// Modify the region so that we remove the padding so that
// we write to the non-zero location. The data in an Altlas
// is always initialized to zero (Atlas.clear) so we don't
// need to worry about zero-ing that.
region.x += padding;
region.y += padding;
region.width -= padding * 2;
region.height -= padding * 2;
break :region region;
};
if (region.width > 0 and region.height > 0) {
const depth = atlas.format.depth();

View File

@ -129,6 +129,7 @@ const GPUCellMode = enum(u8) {
/// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain.
pub const DerivedConfig = struct {
font_thicken: bool,
cursor_color: ?terminal.color.RGB,
background: terminal.color.RGB,
foreground: terminal.color.RGB,
@ -142,6 +143,8 @@ pub const DerivedConfig = struct {
_ = alloc_gpa;
return .{
.font_thicken = config.@"font-thicken",
.cursor_color = if (config.@"cursor-color") |col|
col.toTerminalRGB()
else
@ -738,6 +741,14 @@ fn drawCells(
/// Update the configuration.
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
// If font thickening settings change, we need to reset our
// font texture completely because we need to re-render the glyphs.
if (self.config.font_thicken != config.font_thicken) {
self.font_group.reset();
self.font_group.atlas_greyscale.clear();
self.font_group.atlas_color.clear();
}
self.config = config.*;
}
@ -996,7 +1007,10 @@ pub fn updateCell(
self.alloc,
shaper_run.font_index,
shaper_cell.glyph_index,
@intFromFloat(@ceil(self.cell_size.height)),
.{
.max_height = @intFromFloat(@ceil(self.cell_size.height)),
.thicken = self.config.font_thicken,
},
);
// If we're rendering a color font, we use the color atlas
@ -1031,7 +1045,7 @@ pub fn updateCell(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
null,
.{},
);
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
@ -1083,7 +1097,7 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void {
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
null,
.{},
) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err});
return;

View File

@ -226,6 +226,7 @@ const GPUCellMode = enum(u8) {
/// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain.
pub const DerivedConfig = struct {
font_thicken: bool,
cursor_color: ?terminal.color.RGB,
background: terminal.color.RGB,
foreground: terminal.color.RGB,
@ -239,6 +240,8 @@ pub const DerivedConfig = struct {
_ = alloc_gpa;
return .{
.font_thicken = config.@"font-thicken",
.cursor_color = if (config.@"cursor-color") |col|
col.toTerminalRGB()
else
@ -991,7 +994,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void {
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
null,
.{},
) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err});
return;
@ -1144,7 +1147,10 @@ pub fn updateCell(
self.alloc,
shaper_run.font_index,
shaper_cell.glyph_index,
@intFromFloat(@ceil(self.cell_size.height)),
.{
.max_height = @intFromFloat(@ceil(self.cell_size.height)),
.thicken = self.config.font_thicken,
},
);
// If we're rendering a color font, we use the color atlas
@ -1190,7 +1196,7 @@ pub fn updateCell(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
null,
.{},
);
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
@ -1254,6 +1260,14 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid
/// Update the configuration.
pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
// If font thickening settings change, we need to reset our
// font texture completely because we need to re-render the glyphs.
if (self.config.font_thicken != config.font_thicken) {
self.font_group.reset();
self.font_group.atlas_greyscale.clear();
self.font_group.atlas_color.clear();
}
self.config = config.*;
}