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 { pub fn setShouldAntialias(self: *T, v: bool) void {
c.CGContextSetShouldAntialias( c.CGContextSetShouldAntialias(
@ptrCast(self), @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 { pub fn setGrayFillColor(self: *T, gray: f64, alpha: f64) void {
c.CGContextSetGrayFillColor( c.CGContextSetGrayFillColor(
@ptrCast(self), @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 { pub fn setTextDrawingMode(self: *T, mode: TextDrawingMode) void {
c.CGContextSetTextDrawingMode( c.CGContextSetTextDrawingMode(
@ptrCast(self), @ptrCast(self),

View File

@ -130,6 +130,14 @@ pub const Font = opaque {
pub fn getUnderlineThickness(self: *Font) f64 { pub fn getUnderlineThickness(self: *Font) f64 {
return c.CTFontGetUnderlineThickness(@ptrCast(self)); 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) { pub const FontOrientation = enum(c_uint) {

View File

@ -30,30 +30,12 @@ pub const Line = opaque {
self: *Line, self: *Line,
opts: LineBoundsOptions, opts: LineBoundsOptions,
) graphics.Rect { ) graphics.Rect {
// return @bitCast(c.CGRect, c.CTLineGetBoundsWithOptions( return @bitCast(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(
@ptrCast(self), @ptrCast(self),
opts.cval(), opts.cval(),
@ptrCast(&result), ));
);
return result;
} }
// See getBoundsWithOptions
extern "c" fn zig_cabi_CTLineGetBoundsWithOptions(
c.CTLineRef,
c.CTLineBoundsOptions,
*c.CGRect,
) void;
pub fn getTypographicBounds( pub fn getTypographicBounds(
self: *Line, self: *Line,
ascent: ?*f64, ascent: ?*f64,

View File

@ -231,7 +231,7 @@ pub fn init(
if (try disco_it.next()) |face| { if (try disco_it.next()) |face| {
log.info("font regular: {s}", .{try face.name()}); log.info("font regular: {s}", .{try face.name()});
try group.addFace(alloc, .regular, face); try group.addFace(alloc, .regular, face);
} } else std.log.warn("font-family not found: {s}", .{family});
} }
if (config.@"font-family-bold") |family| { if (config.@"font-family-bold") |family| {
var disco_it = try disco.discover(.{ var disco_it = try disco.discover(.{
@ -243,7 +243,7 @@ pub fn init(
if (try disco_it.next()) |face| { if (try disco_it.next()) |face| {
log.info("font bold: {s}", .{try face.name()}); log.info("font bold: {s}", .{try face.name()});
try group.addFace(alloc, .bold, face); try group.addFace(alloc, .bold, face);
} } else std.log.warn("font-family-bold not found: {s}", .{family});
} }
if (config.@"font-family-italic") |family| { if (config.@"font-family-italic") |family| {
var disco_it = try disco.discover(.{ var disco_it = try disco.discover(.{
@ -255,7 +255,7 @@ pub fn init(
if (try disco_it.next()) |face| { if (try disco_it.next()) |face| {
log.info("font italic: {s}", .{try face.name()}); log.info("font italic: {s}", .{try face.name()});
try group.addFace(alloc, .italic, face); try group.addFace(alloc, .italic, face);
} } else std.log.warn("font-family-italic not found: {s}", .{family});
} }
if (config.@"font-family-bold-italic") |family| { if (config.@"font-family-bold-italic") |family| {
var disco_it = try disco.discover(.{ var disco_it = try disco.discover(.{
@ -268,7 +268,7 @@ pub fn init(
if (try disco_it.next()) |face| { if (try disco_it.next()) |face| {
log.info("font bold+italic: {s}", .{try face.name()}); log.info("font bold+italic: {s}", .{try face.name()});
try group.addFace(alloc, .bold_italic, face); 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, 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 for the window.
background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },

View File

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

View File

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

View File

@ -58,6 +58,20 @@ pub const Metrics = struct {
strikethrough_thickness: f32, 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; pub const Foo = if (options.backend == .coretext) coretext.Face else void;
test { test {

View File

@ -5,6 +5,8 @@ const macos = @import("macos");
const harfbuzz = @import("harfbuzz"); const harfbuzz = @import("harfbuzz");
const font = @import("../main.zig"); const font = @import("../main.zig");
const log = std.log.scoped(.font_face);
pub const Face = struct { pub const Face = struct {
/// Our font face /// Our font face
font: *macos.text.Font, 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 /// because the font is loaded at a default size during discovery, and then
/// adjusted to the final size for final load. /// adjusted to the final size for final load.
pub fn initFontCopy(base: *macos.text.Font, size: font.face.DesiredSize) !Face { pub fn initFontCopy(base: *macos.text.Font, size: font.face.DesiredSize) !Face {
// Create a copy // Create a copy. The copyWithAttributes docs say the size is in points,
const ct_font = try base.copyWithAttributes(@floatFromInt(size.points), null); // 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(); errdefer ct_font.release();
var hb_font = try harfbuzz.coretext.createFont(ct_font); var hb_font = try harfbuzz.coretext.createFont(ct_font);
@ -93,39 +97,41 @@ pub const Face = struct {
return @intCast(glyphs[0]); return @intCast(glyphs[0]);
} }
/// Render a glyph using the glyph index. The rendered glyph is stored in the
/// given texture atlas.
pub fn renderGlyph( pub fn renderGlyph(
self: Face, self: Face,
alloc: Allocator, alloc: Allocator,
atlas: *font.Atlas, atlas: *font.Atlas,
glyph_index: u32, glyph_index: u32,
max_height: ?u16, opts: font.face.RenderOptions,
) !font.Glyph { ) !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)}; var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)};
// Get the bounding rect for this glyph to determine the width/height // Get the bounding rect for rendering this glyph.
// of the bitmap. We use the rounded up width/height of the bounding rect. const rect = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, null);
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)));
// Width and height. Note the padding doubling is because we want // The x/y that we render the glyph at. The Y value has to be flipped
// the padding on both sides (top/bottom, left/right). // because our coordinates in 3D space are (0, 0) bottom left with
const width = glyph_width + (padding * 2); // +y being up.
const height = glyph_height + (padding * 2); 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. // 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 (glyph_width == 0) return font.Glyph{ if (width == 0 or height == 0) return font.Glyph{
.width = 0, .width = 0,
.height = 0, .height = 0,
.offset_x = 0, .offset_x = 0,
@ -135,77 +141,157 @@ pub const Face = struct {
.advance_x = 0, .advance_x = 0,
}; };
// Get the advance that we need for the glyph // Settings that are specific to if we are rendering text or emoji.
var advances: [1]macos.graphics.Size = undefined; const color: struct {
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); 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 // This is just a safety check.
// TODO(perf): cache this buffer if (atlas.format.depth() != color.depth) {
// TODO(mitchellh): color is going to require a depth here log.warn("font atlas color depth doesn't equal font color depth atlas={} font={}", .{
var buf = try alloc.alloc(u8, width * height); 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); defer alloc.free(buf);
std.mem.set(u8, buf, 0); @memset(buf, 0);
const space = try macos.graphics.ColorSpace.createDeviceGray();
defer space.release();
const ctx = try macos.graphics.BitmapContext.create( const ctx = try macos.graphics.BitmapContext.create(
buf, buf,
width, width,
height, height,
8, 8,
width, width * color.depth,
space, color.space,
@intFromEnum(macos.graphics.BitmapInfo.alpha_mask) & color.context_opts,
@intFromEnum(macos.graphics.ImageAlphaInfo.none),
); );
defer ctx.release(); 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.setAllowsAntialiasing(true);
ctx.setShouldAntialias(true); ctx.setShouldAntialias(true);
ctx.setShouldSmoothFonts(true);
// 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.setGrayFillColor(1, 1);
ctx.setGrayStrokeColor(1, 1); ctx.setGrayStrokeColor(1, 1);
ctx.setTextDrawingMode(.fill_stroke); }
ctx.setTextMatrix(macos.graphics.AffineTransform.identity());
ctx.setTextPosition(0, 0);
// 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. // to get them to 0,0. We also add the padding so that they render
var pos = [_]macos.graphics.Point{.{ // slightly off the edge of the bitmap.
.x = padding + (-1 * bounding[0].origin.x), self.font.drawGlyphs(&glyphs, &.{
.y = padding + (-1 * bounding[0].origin.y), .{
}}; .x = -1 * render_x,
self.font.drawGlyphs(&glyphs, &pos, ctx); .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); 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. // 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. // We need to calculate our baseline from the bottom of a cell.
const baseline_from_bottom = self.metrics.cell_height - self.metrics.cell_baseline; const baseline_from_bottom = self.metrics.cell_height - self.metrics.cell_baseline;
// Next we offset our baseline by the bearing in the font. We // Next we offset our baseline by the bearing in the font. We
// ADD here because CoreText y is UP. // 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 break :offset_y @intFromFloat(@ceil(baseline_with_offset));
// 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)));
}; };
return font.Glyph{ // log.warn("renderGlyph rect={} width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{
.width = glyph_width, // rect,
.height = glyph_height, // width,
.offset_x = @intFromFloat(@ceil(bounding[0].origin.x)), // 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, .offset_y = offset_y,
.atlas_x = region.x + padding, .atlas_x = region.x,
.atlas_y = region.y + padding, .atlas_y = region.y,
.advance_x = @floatCast(advances[0].width),
// 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(); defer fs.release();
// Create a rectangle to fit all of this and create a frame of it. // 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(); 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(); defer path.release();
const frame = try fs.createFrame( const frame = try fs.createFrame(
macos.foundation.Range.init(0, 0), macos.foundation.Range.init(0, 0),
@ -292,38 +402,53 @@ pub const Face = struct {
const lines = frame.getLines(); const lines = frame.getLines();
const line = lines.getValueAtIndex(macos.text.Line, 0); const line = lines.getValueAtIndex(macos.text.Line, 0);
// NOTE(mitchellh): For some reason, CTLineGetBoundsWithOptions // Get the bounds of the line to determine the ascent.
// returns garbage and I can't figure out why... so we use the const bounds = line.getBoundsWithOptions(.{ .exclude_leading = true });
// raw ascender. const bounds_ascent = bounds.size.height + bounds.origin.y;
const baseline = @floor(bounds_ascent + 0.5);
var ascent: f64 = 0; // This is an alternate approach to the above to calculate the
var descent: f64 = 0; // baseline by simply using the ascender. Using this approach led
var leading: f64 = 0; // to less accurate results, but I'm leaving it here for reference.
_ = line.getTypographicBounds(&ascent, &descent, &leading); // 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 }); //std.log.warn("ascent={} descent={} leading={}", .{ ascent, descent, leading });
break :metrics .{ break :metrics .{
.height = @floatCast(points[0].y - points[1].y), .height = @floatCast(points[0].y - points[1].y),
.ascent = @floatCast(ascent), .ascent = @floatCast(baseline),
}; };
}; };
// All of these metrics are based on our layout above. // All of these metrics are based on our layout above.
const cell_height = layout_metrics.height; const cell_height = layout_metrics.height;
const cell_baseline = layout_metrics.ascent; 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 underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
const strikethrough_position = cell_baseline * 0.6; const strikethrough_position = cell_baseline * 0.6;
const strikethrough_thickness = underline_thickness; 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_width,
// cell_height, // cell_height,
// cell_baseline, // cell_baseline,
// underline_position, // underline_position,
// underline_thickness, // underline_thickness,
// }); // });
return font.face.Metrics{ return font.face.Metrics{
.cell_width = cell_width, .cell_width = cell_width,
.cell_height = cell_height, .cell_height = cell_height,
@ -359,7 +484,7 @@ test {
var i: u8 = 32; var i: u8 = 32;
while (i < 127) : (i += 1) { while (i < 127) : (i += 1) {
try testing.expect(face.glyphIndex(i) != null); 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; var i: u8 = 32;
while (i < 127) : (i += 1) { while (i < 127) : (i += 1) {
try testing.expect(face.glyphIndex(i) != null); 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, alloc: Allocator,
atlas: *font.Atlas, atlas: *font.Atlas,
glyph_index: u32, glyph_index: u32,
max_height: ?u16, opts: font.face.RenderOptions,
) !Glyph { ) !Glyph {
// If our glyph has color, we want to render the color // If our glyph has color, we want to render the color
try self.face.loadGlyph(glyph_index, .{ try self.face.loadGlyph(glyph_index, .{
@ -188,7 +188,7 @@ 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 = max_height orelse break :resized null; const max = opts.max_height orelse break :resized null;
const bm = bitmap_original; const bm = bitmap_original;
if (bm.rows <= max) break :resized null; 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)); 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 // Store glyph metadata
return Glyph{ return Glyph{
.width = tgt_w, .width = tgt_w,
@ -514,16 +522,16 @@ test {
// Generate all visible ASCII // Generate all visible ASCII
var i: u8 = 32; var i: u8 = 32;
while (i < 127) : (i += 1) { 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 // 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 testing.expectEqual(@as(u32, 11), g1.height);
try ft_font.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); 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); try testing.expectEqual(@as(u32, 21), g2.height);
} }
} }
@ -543,11 +551,13 @@ test "color emoji" {
try testing.expectEqual(Presentation.emoji, ft_font.presentation); 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 // 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); try testing.expectEqual(@as(u32, 24), glyph.height);
} }
} }
@ -602,5 +612,5 @@ test "mono to rgba" {
defer ft_font.deinit(); defer ft_font.deinit();
// glyph 3 is mono in Noto // 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, alloc: Allocator,
atlas: *font.Atlas, atlas: *font.Atlas,
glyph_index: u32, glyph_index: u32,
max_height: ?u16, opts: font.face.RenderOptions,
) !font.Glyph { ) !font.Glyph {
_ = max_height; _ = opts;
var render = try self.renderGlyphInternal(alloc, glyph_index); var render = try self.renderGlyphInternal(alloc, glyph_index);
defer render.deinit(); defer render.deinit();
@ -551,7 +551,7 @@ pub const Wasm = struct {
} }
fn face_render_glyph_(face: *Face, atlas: *font.Atlas, codepoint: u32) !*font.Glyph { 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); const result = try alloc.create(font.Glyph);
errdefer alloc.destroy(result); errdefer alloc.destroy(result);

View File

@ -71,12 +71,10 @@ pub const Backend = enum {
}; };
} }
return if (target.isDarwin()) darwin: { // macOS also supports "coretext_freetype" but there is no scenario
// On macOS right now, the coretext renderer is still pretty buggy // that is the default. It is only used by people who want to
// so we default to coretext for font discovery and freetype for // self-compile Ghostty and prefer the freetype aesthetic.
// rasterization. return if (target.isDarwin()) .coretext else .fontconfig_freetype;
break :darwin .coretext_freetype;
} else .fontconfig_freetype;
} }
// All the functions below can be called at comptime or runtime to // 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 width = @as(u32, @intCast(self.image.getWidth()));
const height = @as(u32, @intCast(self.image.getHeight())); 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) { if (region.width > 0 and region.height > 0) {
const depth = atlas.format.depth(); 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 /// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain. /// pass around Config pointers which makes memory management a pain.
pub const DerivedConfig = struct { pub const DerivedConfig = struct {
font_thicken: bool,
cursor_color: ?terminal.color.RGB, cursor_color: ?terminal.color.RGB,
background: terminal.color.RGB, background: terminal.color.RGB,
foreground: terminal.color.RGB, foreground: terminal.color.RGB,
@ -142,6 +143,8 @@ pub const DerivedConfig = struct {
_ = alloc_gpa; _ = alloc_gpa;
return .{ return .{
.font_thicken = config.@"font-thicken",
.cursor_color = if (config.@"cursor-color") |col| .cursor_color = if (config.@"cursor-color") |col|
col.toTerminalRGB() col.toTerminalRGB()
else else
@ -738,6 +741,14 @@ fn drawCells(
/// Update the configuration. /// Update the configuration.
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { 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.*; self.config = config.*;
} }
@ -996,7 +1007,10 @@ pub fn updateCell(
self.alloc, self.alloc,
shaper_run.font_index, shaper_run.font_index,
shaper_cell.glyph_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 // If we're rendering a color font, we use the color atlas
@ -1031,7 +1045,7 @@ pub fn updateCell(
self.alloc, self.alloc,
font.sprite_index, font.sprite_index,
@intFromEnum(sprite), @intFromEnum(sprite),
null, .{},
); );
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; 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, self.alloc,
font.sprite_index, font.sprite_index,
@intFromEnum(sprite), @intFromEnum(sprite),
null, .{},
) catch |err| { ) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err}); log.warn("error rendering cursor glyph err={}", .{err});
return; return;

View File

@ -226,6 +226,7 @@ const GPUCellMode = enum(u8) {
/// configuration. This must be exported so that we don't need to /// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain. /// pass around Config pointers which makes memory management a pain.
pub const DerivedConfig = struct { pub const DerivedConfig = struct {
font_thicken: bool,
cursor_color: ?terminal.color.RGB, cursor_color: ?terminal.color.RGB,
background: terminal.color.RGB, background: terminal.color.RGB,
foreground: terminal.color.RGB, foreground: terminal.color.RGB,
@ -239,6 +240,8 @@ pub const DerivedConfig = struct {
_ = alloc_gpa; _ = alloc_gpa;
return .{ return .{
.font_thicken = config.@"font-thicken",
.cursor_color = if (config.@"cursor-color") |col| .cursor_color = if (config.@"cursor-color") |col|
col.toTerminalRGB() col.toTerminalRGB()
else else
@ -991,7 +994,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void {
self.alloc, self.alloc,
font.sprite_index, font.sprite_index,
@intFromEnum(sprite), @intFromEnum(sprite),
null, .{},
) catch |err| { ) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err}); log.warn("error rendering cursor glyph err={}", .{err});
return; return;
@ -1144,7 +1147,10 @@ pub fn updateCell(
self.alloc, self.alloc,
shaper_run.font_index, shaper_run.font_index,
shaper_cell.glyph_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 // If we're rendering a color font, we use the color atlas
@ -1190,7 +1196,7 @@ pub fn updateCell(
self.alloc, self.alloc,
font.sprite_index, font.sprite_index,
@intFromEnum(sprite), @intFromEnum(sprite),
null, .{},
); );
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; 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. /// Update the configuration.
pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { 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.*; self.config = config.*;
} }