From a3401d96908cd5bfc43c77cf247364306d37f8b3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Mar 2023 11:08:20 -0800 Subject: [PATCH 01/24] log if a configured font is not found --- src/Surface.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 1943ffe58..a54d09db2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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}); } } From 2a1cbb4f21f133764cbeefef7b4e521717ca71b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Mar 2023 11:29:40 -0800 Subject: [PATCH 02/24] coretext: calculate units per em/point --- pkg/macos/text/font.zig | 8 ++++++++ src/font/face/coretext.zig | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index 6db9fa5c9..5ff031183 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -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(c.CTFontRef, self)); + } + + pub fn getSize(self: *Font) f64 { + return c.CTFontGetSize(@ptrCast(c.CTFontRef, self)); + } }; pub const FontOrientation = enum(c_uint) { diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index a95fcb70a..c551e3bd3 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -317,6 +317,10 @@ pub const Face = struct { const strikethrough_position = cell_baseline * 0.6; const strikethrough_thickness = underline_thickness; + // 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("width={d}, height={d} baseline={d} underline_pos={d} underline_thickness={d}", .{ // cell_width, // cell_height, From ef5d86ffb09ab1688233f6e226ba2ea578615063 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Mar 2023 14:02:45 -0800 Subject: [PATCH 03/24] coretext: initially fill grey --- src/font/face/coretext.zig | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index c551e3bd3..86bbca747 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -161,11 +161,23 @@ pub const Face = struct { ); defer ctx.release(); + // Perform an initial fill so that we're sure it starts as we want. + ctx.setGrayFillColor(0, 0); + ctx.fillRect(.{ + .origin = .{ .x = 0, .y = 0 }, + .size = .{ + .width = @intToFloat(f64, width), + .height = @intToFloat(f64, height), + }, + }); + ctx.setAllowsAntialiasing(true); ctx.setShouldAntialias(true); ctx.setShouldSmoothFonts(true); ctx.setGrayFillColor(1, 1); - ctx.setGrayStrokeColor(1, 1); + // With this set the text gets chunky. With it unset the text doesn't + // look right at small font sizes. Something isn't right. + // ctx.setGrayStrokeColor(1, 1); ctx.setTextDrawingMode(.fill_stroke); ctx.setTextMatrix(macos.graphics.AffineTransform.identity()); ctx.setTextPosition(0, 0); From 45da58188cab8d71ce956b3c567bce7d8984d78a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 13:53:05 -0700 Subject: [PATCH 04/24] fix up for new zig --- pkg/macos/text/font.zig | 4 ++-- src/font/face/coretext.zig | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index 5ff031183..cf24bc5d0 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -132,11 +132,11 @@ pub const Font = opaque { } pub fn getUnitsPerEm(self: *Font) u32 { - return c.CTFontGetUnitsPerEm(@ptrCast(c.CTFontRef, self)); + return c.CTFontGetUnitsPerEm(@ptrCast(self)); } pub fn getSize(self: *Font) f64 { - return c.CTFontGetSize(@ptrCast(c.CTFontRef, self)); + return c.CTFontGetSize(@ptrCast(self)); } }; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 86bbca747..58bbddac9 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -144,7 +144,7 @@ pub const Face = struct { // TODO(mitchellh): color is going to require a depth here var buf = try alloc.alloc(u8, width * height); defer alloc.free(buf); - std.mem.set(u8, buf, 0); + @memset(buf, 0); const space = try macos.graphics.ColorSpace.createDeviceGray(); defer space.release(); @@ -166,8 +166,8 @@ pub const Face = struct { ctx.fillRect(.{ .origin = .{ .x = 0, .y = 0 }, .size = .{ - .width = @intToFloat(f64, width), - .height = @intToFloat(f64, height), + .width = @floatFromInt(width), + .height = @floatFromInt(height), }, }); From 4d7a2c9f05a9a823652d24b73329e482389a5025 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 13:54:13 -0700 Subject: [PATCH 05/24] font: remove the old comment about not doing the grey stroke --- src/font/face/coretext.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 58bbddac9..3de6f6964 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -175,9 +175,7 @@ pub const Face = struct { ctx.setShouldAntialias(true); ctx.setShouldSmoothFonts(true); ctx.setGrayFillColor(1, 1); - // With this set the text gets chunky. With it unset the text doesn't - // look right at small font sizes. Something isn't right. - // ctx.setGrayStrokeColor(1, 1); + ctx.setGrayStrokeColor(1, 1); ctx.setTextDrawingMode(.fill_stroke); ctx.setTextMatrix(macos.graphics.AffineTransform.identity()); ctx.setTextPosition(0, 0); From 079fe7bc949b59646699c1053a589fc4f1fa2e63 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 14:13:40 -0700 Subject: [PATCH 06/24] coretext: the size needs to be in pixels! (see comment) --- src/font/face/coretext.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 3de6f6964..39d5987be 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -41,8 +41,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); From 552a1b51d06a4e00a172082e1fdca0a834c762f7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 14:47:59 -0700 Subject: [PATCH 07/24] coretext: rasterization looking cleaner --- pkg/macos/graphics/context.zig | 35 +++++++++ src/font/face/coretext.zig | 133 +++++++++++++++++++++++++++++++-- src/font/face/freetype.zig | 18 ++--- 3 files changed, 170 insertions(+), 16 deletions(-) diff --git a/pkg/macos/graphics/context.zig b/pkg/macos/graphics/context.zig index 7ba03b31c..1bacec1e3 100644 --- a/pkg/macos/graphics/context.zig +++ b/pkg/macos/graphics/context.zig @@ -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), diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 39d5987be..b8d93c4ed 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -95,6 +95,122 @@ pub const Face = struct { return @intCast(glyphs[0]); } + pub fn renderGlyph2( + self: Face, + alloc: Allocator, + atlas: *font.Atlas, + glyph_index: u32, + max_height: ?u16, + ) !font.Glyph { + _ = 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; + const rect = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, &bounding); + const rasterized_left: i32 = @intFromFloat(@floor(rect.origin.x)); + const rasterized_width: u32 = @intFromFloat(@ceil( + rect.origin.x - @floor(rect.origin.x) + rect.size.width, + )); + const rasterized_descent: i32 = @intFromFloat(@ceil(-rect.origin.y)); + const rasterized_ascent: i32 = @intFromFloat(@ceil(rect.size.height + rect.origin.y)); + const rasterized_height: u32 = @intCast(rasterized_descent + rasterized_ascent); + + // 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 (rasterized_width == 0 or rasterized_height == 0) return font.Glyph{ + .width = 0, + .height = 0, + .offset_x = 0, + .offset_y = 0, + .atlas_x = 0, + .atlas_y = 0, + .advance_x = 0, + }; + + // 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, rasterized_width * rasterized_height); + defer alloc.free(buf); + @memset(buf, 0); + + const space = try macos.graphics.ColorSpace.createDeviceGray(); + defer space.release(); + + const ctx = try macos.graphics.BitmapContext.create( + buf, + rasterized_width, + rasterized_height, + 8, + rasterized_width, + space, + @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) & + @intFromEnum(macos.graphics.ImageAlphaInfo.none), + ); + defer ctx.release(); + + // Perform an initial fill. This ensures that we don't have any + // uninitialized pixels in the bitmap. + ctx.setGrayFillColor(0, 0); + ctx.fillRect(.{ + .origin = .{ .x = 0, .y = 0 }, + .size = .{ + .width = @floatFromInt(rasterized_width), + .height = @floatFromInt(rasterized_height), + }, + }); + + ctx.setAllowsFontSmoothing(true); + ctx.setShouldSmoothFonts(true); + ctx.setAllowsFontSubpixelQuantization(true); + ctx.setShouldSubpixelQuantizeFonts(true); + ctx.setAllowsFontSubpixelPositioning(true); + ctx.setShouldSubpixelPositionFonts(true); + ctx.setAllowsAntialiasing(true); + ctx.setShouldAntialias(true); + + // Set our color for drawing + ctx.setGrayFillColor(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 + // are offset by bearings, so we have to undo those bearings in order + // to get them to 0,0. + self.font.drawGlyphs(&glyphs, &.{ + .{ + .x = -1 * @as(f64, @floatFromInt(rasterized_left)), + .y = @as(f64, @floatFromInt(rasterized_descent)), + }, + }, ctx); + + const region = try atlas.reserve(alloc, rasterized_width, rasterized_height); + atlas.set(region, buf); + + std.log.warn("FONT FONT FONT rasterized_left={} rasterized_width={} rasterized_descent={} rasterized_ascent={} rasterized_height={}", .{ + rasterized_left, + rasterized_width, + rasterized_descent, + rasterized_ascent, + rasterized_height, + }); + + return .{ + .width = @intCast(rasterized_width), + .height = @intCast(rasterized_height), + .advance_x = 0, + .offset_x = @intCast(rasterized_left), + .offset_y = @intCast(rasterized_ascent), + .atlas_x = @intCast(region.x), + .atlas_y = @intCast(region.y), + }; + } + /// Render a glyph using the glyph index. The rendered glyph is stored in the /// given texture atlas. pub fn renderGlyph( @@ -125,6 +241,8 @@ pub const Face = struct { const width = glyph_width + (padding * 2); const height = glyph_height + (padding * 2); + if (true) return try self.renderGlyph2(alloc, atlas, glyph_index, 0); + // 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{ @@ -333,13 +451,14 @@ pub const Face = struct { // const units_per_em = ct_font.getUnitsPerEm(); // const units_per_point = @intToFloat(f64, units_per_em) / ct_font.getSize(); - // std.log.warn("width={d}, height={d} baseline={d} underline_pos={d} underline_thickness={d}", .{ - // cell_width, - // cell_height, - // cell_baseline, - // underline_position, - // underline_thickness, - // }); + 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, diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index fdeb27e82..4e21ad968 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -467,15 +467,15 @@ pub const Face = struct { .thickness = underline_thickness, }; - // log.warn("METRICS={} width={d} height={d} baseline={d} underline_pos={d} underline_thickness={d} strikethrough={}", .{ - // size_metrics, - // cell_width, - // cell_height, - // cell_height - cell_baseline, - // underline_position, - // underline_thickness, - // strikethrough, - // }); + log.warn("METRICS={} width={d} height={d} baseline={d} underline_pos={d} underline_thickness={d} strikethrough={}", .{ + size_metrics, + cell_width, + cell_height, + cell_height - cell_baseline, + underline_position, + underline_thickness, + strikethrough, + }); return .{ .cell_width = cell_width, From 286944cd43e8d0bddd2431c0fa2f2781fbd3c2c3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 15:24:14 -0700 Subject: [PATCH 08/24] cleaning up rasterization, comments --- src/font/face/coretext.zig | 102 +++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index b8d93c4ed..7108d30b0 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -102,25 +102,41 @@ pub const Face = struct { glyph_index: u32, max_height: ?u16, ) !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; - const rect = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, &bounding); - const rasterized_left: i32 = @intFromFloat(@floor(rect.origin.x)); - const rasterized_width: u32 = @intFromFloat(@ceil( - rect.origin.x - @floor(rect.origin.x) + rect.size.width, - )); - const rasterized_descent: i32 = @intFromFloat(@ceil(-rect.origin.y)); - const rasterized_ascent: i32 = @intFromFloat(@ceil(rect.size.height + rect.origin.y)); - const rasterized_height: u32 = @intCast(rasterized_descent + rasterized_ascent); + // Get the bounding rect for rendering this glyph. + const rect = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, null); + + // 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 glyph_height: u32 = @intFromFloat(glyph_ascent + render_y); + + // The glyph width is our advertised bounding with plus the rounding + // difference from our rendering X. + const glyph_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 (rasterized_width == 0 or rasterized_height == 0) return font.Glyph{ + if (glyph_width == 0 or glyph_height == 0) return font.Glyph{ .width = 0, .height = 0, .offset_x = 0, @@ -130,10 +146,15 @@ pub const Face = struct { .advance_x = 0, }; + // 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); + // 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, rasterized_width * rasterized_height); + var buf = try alloc.alloc(u8, width * height); defer alloc.free(buf); @memset(buf, 0); @@ -142,10 +163,10 @@ pub const Face = struct { const ctx = try macos.graphics.BitmapContext.create( buf, - rasterized_width, - rasterized_height, + width, + height, 8, - rasterized_width, + width, space, @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) & @intFromEnum(macos.graphics.ImageAlphaInfo.none), @@ -158,8 +179,8 @@ pub const Face = struct { ctx.fillRect(.{ .origin = .{ .x = 0, .y = 0 }, .size = .{ - .width = @floatFromInt(rasterized_width), - .height = @floatFromInt(rasterized_height), + .width = @floatFromInt(width), + .height = @floatFromInt(height), }, }); @@ -175,39 +196,44 @@ pub const Face = struct { // Set our color for drawing ctx.setGrayFillColor(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 // 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 + // slightly off the edge of the bitmap. self.font.drawGlyphs(&glyphs, &.{ .{ - .x = -1 * @as(f64, @floatFromInt(rasterized_left)), - .y = @as(f64, @floatFromInt(rasterized_descent)), + .x = padding + (-1 * render_x), + .y = padding + render_y, }, }, ctx); - const region = try atlas.reserve(alloc, rasterized_width, rasterized_height); + const region = try atlas.reserve(alloc, width, height); atlas.set(region, buf); - std.log.warn("FONT FONT FONT rasterized_left={} rasterized_width={} rasterized_descent={} rasterized_ascent={} rasterized_height={}", .{ - rasterized_left, - rasterized_width, - rasterized_descent, - rasterized_ascent, - rasterized_height, - }); + 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 + glyph_ascent; + + break :offset_y @intFromFloat(@ceil(baseline_with_offset)); + }; return .{ - .width = @intCast(rasterized_width), - .height = @intCast(rasterized_height), + .width = glyph_width, + .height = glyph_height, + .offset_x = @intFromFloat(render_x), + .offset_y = offset_y, + .atlas_x = region.x + padding, + .atlas_y = region.y + padding, + + // 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, - .offset_x = @intCast(rasterized_left), - .offset_y = @intCast(rasterized_ascent), - .atlas_x = @intCast(region.x), - .atlas_y = @intCast(region.y), }; } From a74e49833b3fbbe82919302c7d9ab56dfaab8f3e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 15:57:59 -0700 Subject: [PATCH 09/24] coretext: colored glyph rendering --- pkg/macos/graphics/context.zig | 10 +++++++ src/font/face/coretext.zig | 50 +++++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/pkg/macos/graphics/context.zig b/pkg/macos/graphics/context.zig index 1bacec1e3..15d0fd906 100644 --- a/pkg/macos/graphics/context.zig +++ b/pkg/macos/graphics/context.zig @@ -101,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), diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 7108d30b0..87ccb4678 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -151,31 +151,50 @@ pub const Face = struct { const width = glyph_width + (padding * 2); const height = glyph_height + (padding * 2); + // 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); + var buf = try alloc.alloc(u8, width * height * color.depth); defer alloc.free(buf); @memset(buf, 0); - const space = try macos.graphics.ColorSpace.createDeviceGray(); - defer space.release(); - 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. - ctx.setGrayFillColor(0, 0); + if (color.color) + ctx.setRGBFillColor(1, 1, 1, 0) + else + ctx.setGrayFillColor(0, 0); ctx.fillRect(.{ .origin = .{ .x = 0, .y = 0 }, .size = .{ @@ -185,7 +204,7 @@ pub const Face = struct { }); ctx.setAllowsFontSmoothing(true); - ctx.setShouldSmoothFonts(true); + ctx.setShouldSmoothFonts(true); // The amadeus "enthicken" ctx.setAllowsFontSubpixelQuantization(true); ctx.setShouldSubpixelQuantizeFonts(true); ctx.setAllowsFontSubpixelPositioning(true); @@ -194,8 +213,13 @@ pub const Face = struct { ctx.setShouldAntialias(true); // Set our color for drawing - ctx.setGrayFillColor(1, 1); - ctx.setGrayStrokeColor(1, 1); + 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 From c52dc229f3f4bf4352bb6527b502267b5fd16523 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 16:59:17 -0700 Subject: [PATCH 10/24] coretext: validate atlas depth matches color depth --- src/font/face/coretext.zig | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 87ccb4678..36e585533 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -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, @@ -172,6 +174,15 @@ pub const Face = struct { }; defer color.space.release(); + // 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 // TODO(perf): cache this buffer var buf = try alloc.alloc(u8, width * height * color.depth); From 55254acaad0c85660f1faf469151fa421fc671cc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 17:12:24 -0700 Subject: [PATCH 11/24] coretext: fix emoji placement --- src/font/face/coretext.zig | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 36e585533..029bfef12 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -215,7 +215,7 @@ pub const Face = struct { }); ctx.setAllowsFontSmoothing(true); - ctx.setShouldSmoothFonts(true); // The amadeus "enthicken" + ctx.setShouldSmoothFonts(false); // The amadeus "enthicken" ctx.setAllowsFontSubpixelQuantization(true); ctx.setShouldSubpixelQuantizeFonts(true); ctx.setAllowsFontSubpixelPositioning(true); @@ -247,6 +247,18 @@ pub const Face = struct { atlas.set(region, buf); const offset_y: i32 = offset_y: { + // For non-scalable colorized fonts, we assume they are pictographic + // and just center the glyph. So far this has only applied to emoji + // fonts. Emoji fonts don't always report a correct ascender/descender + // (mainly Apple Emoji) so we just center them. Also, since emoji font + // aren't scalable, cell_baseline is incorrect anyways. + // + // NOTE(mitchellh): I don't know if this is right, this doesn't + // _feel_ right, but it makes all my limited test cases work. + if (color.color) { + break :offset_y @intFromFloat(self.metrics.cell_height); + } + // 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; @@ -258,6 +270,17 @@ pub const Face = struct { break :offset_y @intFromFloat(@ceil(baseline_with_offset)); }; + log.warn("FONT FONT FONT width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{ + glyph_width, + glyph_height, + render_x, + render_y, + offset_y, + glyph_ascent, + self.metrics.cell_height, + self.metrics.cell_baseline, + }); + return .{ .width = glyph_width, .height = glyph_height, From 5706770c38e715e9180e2366563c15354409a35d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 21:02:10 -0700 Subject: [PATCH 12/24] coretext: handle glyph padding in region reservation --- src/font/face/coretext.zig | 58 +++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 029bfef12..0ff2eb6ac 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -104,11 +104,6 @@ pub const Face = struct { glyph_index: u32, max_height: ?u16, ) !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)}; @@ -130,15 +125,15 @@ pub const Face = struct { // 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 glyph_height: u32 = @intFromFloat(glyph_ascent + render_y); + 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 glyph_width: u32 = @intFromFloat(@ceil(rect.size.width + (rect.origin.x - render_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 or glyph_height == 0) return font.Glyph{ + if (width == 0 or height == 0) return font.Glyph{ .width = 0, .height = 0, .offset_x = 0, @@ -148,11 +143,6 @@ pub const Face = struct { .advance_x = 0, }; - // 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); - // Settings that are specific to if we are rendering text or emoji. const color: struct { color: bool, @@ -238,12 +228,33 @@ pub const Face = struct { // slightly off the edge of the bitmap. self.font.drawGlyphs(&glyphs, &.{ .{ - .x = padding + (-1 * render_x), - .y = padding + render_y, + .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: i32 = offset_y: { @@ -270,9 +281,10 @@ pub const Face = struct { break :offset_y @intFromFloat(@ceil(baseline_with_offset)); }; - log.warn("FONT FONT FONT width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{ - glyph_width, - glyph_height, + log.warn("FONT FONT FONT rect={} width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{ + rect, + width, + height, render_x, render_y, offset_y, @@ -282,12 +294,12 @@ pub const Face = struct { }); return .{ - .width = glyph_width, - .height = glyph_height, + .width = width, + .height = height, .offset_x = @intFromFloat(render_x), .offset_y = offset_y, - .atlas_x = region.x + padding, - .atlas_y = region.y + padding, + .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. From 69396b0853f11a0deac4f330a8034286685b52f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 21:48:55 -0700 Subject: [PATCH 13/24] coretext: layout rect must fit all chars --- src/font/face/coretext.zig | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 0ff2eb6ac..308ac3344 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -499,8 +499,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), From d39e3f542883c41104815b16f4fc55497d5b6843 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 21:59:52 -0700 Subject: [PATCH 14/24] coretext: improved baseline calculation --- pkg/macos/text/line.zig | 22 ++-------------------- src/font/Group.zig | 4 +++- src/font/face/coretext.zig | 16 +++++++++++----- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/pkg/macos/text/line.zig b/pkg/macos/text/line.zig index e6a506de9..d392643e4 100644 --- a/pkg/macos/text/line.zig +++ b/pkg/macos/text/line.zig @@ -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, diff --git a/src/font/Group.zig b/src/font/Group.zig index 804438c44..f7f9e271f 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -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, max_height); + // log.warn("GLYPH={}", .{glyph}); + return glyph; } /// The wasm-compatible API. diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 308ac3344..b04e35d07 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -545,16 +545,22 @@ pub const Face = struct { // NOTE(mitchellh): For some reason, CTLineGetBoundsWithOptions // returns garbage and I can't figure out why... so we use the // raw ascender. + 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), }; }; From 362eeac74b12c00c7def4e73b0b60ea70d65473f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 22:28:09 -0700 Subject: [PATCH 15/24] coretext: do not treat color diffs special for offset --- src/font/face/coretext.zig | 34 +++++++++++----------------------- src/font/face/freetype.zig | 8 ++++++++ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index b04e35d07..a61c74d64 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -258,18 +258,6 @@ pub const Face = struct { atlas.set(region, buf); const offset_y: i32 = offset_y: { - // For non-scalable colorized fonts, we assume they are pictographic - // and just center the glyph. So far this has only applied to emoji - // fonts. Emoji fonts don't always report a correct ascender/descender - // (mainly Apple Emoji) so we just center them. Also, since emoji font - // aren't scalable, cell_baseline is incorrect anyways. - // - // NOTE(mitchellh): I don't know if this is right, this doesn't - // _feel_ right, but it makes all my limited test cases work. - if (color.color) { - break :offset_y @intFromFloat(self.metrics.cell_height); - } - // 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; @@ -281,17 +269,17 @@ pub const Face = struct { break :offset_y @intFromFloat(@ceil(baseline_with_offset)); }; - log.warn("FONT FONT FONT 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, - }); + // 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, diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 4e21ad968..41c21bd37 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -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, From 3c5be0726f4e2bb45caa405edef6d995597bde8a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 22:43:09 -0700 Subject: [PATCH 16/24] sprite: all rasterized sprites should have a 1px padding --- src/font/sprite/canvas.zig | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index 8603a7eec..08e80210b 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -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(); From 42cc11e32c3bdfeae3a29e70cdca91a8cfcbe45d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 22:44:44 -0700 Subject: [PATCH 17/24] coretext: remove the old renderGlyph impl --- src/font/face/coretext.zig | 130 +------------------------------------ 1 file changed, 1 insertion(+), 129 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index a61c74d64..b44d9905a 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -97,7 +97,7 @@ pub const Face = struct { return @intCast(glyphs[0]); } - pub fn renderGlyph2( + pub fn renderGlyph( self: Face, alloc: Allocator, atlas: *font.Atlas, @@ -295,134 +295,6 @@ pub const Face = struct { }; } - /// 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, - ) !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))); - - // 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); - - if (true) return try self.renderGlyph2(alloc, atlas, glyph_index, 0); - - // 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{ - .width = 0, - .height = 0, - .offset_x = 0, - .offset_y = 0, - .atlas_x = 0, - .atlas_y = 0, - .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); - - // 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); - defer alloc.free(buf); - @memset(buf, 0); - - const space = try macos.graphics.ColorSpace.createDeviceGray(); - defer space.release(); - - const ctx = try macos.graphics.BitmapContext.create( - buf, - width, - height, - 8, - width, - space, - @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) & - @intFromEnum(macos.graphics.ImageAlphaInfo.none), - ); - defer ctx.release(); - - // Perform an initial fill so that we're sure it starts as we want. - ctx.setGrayFillColor(0, 0); - ctx.fillRect(.{ - .origin = .{ .x = 0, .y = 0 }, - .size = .{ - .width = @floatFromInt(width), - .height = @floatFromInt(height), - }, - }); - - 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); - - // 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); - - const region = try atlas.reserve(alloc, width, height); - atlas.set(region, buf); - - const offset_y = 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; - - // 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))); - }; - - return font.Glyph{ - .width = glyph_width, - .height = glyph_height, - .offset_x = @intFromFloat(@ceil(bounding[0].origin.x)), - .offset_y = offset_y, - .atlas_x = region.x + padding, - .atlas_y = region.y + padding, - .advance_x = @floatCast(advances[0].width), - }; - } - fn calcMetrics(ct_font: *macos.text.Font) !font.face.Metrics { // Cell width is calculated by calculating the widest width of the // visible ASCII characters. Usually 'M' is widest but we just take From b5cc37e20c59b6617c41768db11a7b5febd8aa9c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 22:54:08 -0700 Subject: [PATCH 18/24] font: comment out debug logs --- src/font/face/coretext.zig | 17 +++++++++-------- src/font/face/freetype.zig | 18 +++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index b44d9905a..9fb0774da 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -437,14 +437,15 @@ pub const Face = struct { // 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, - }); + // 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, diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 41c21bd37..eab180d62 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -475,15 +475,15 @@ pub const Face = struct { .thickness = underline_thickness, }; - log.warn("METRICS={} width={d} height={d} baseline={d} underline_pos={d} underline_thickness={d} strikethrough={}", .{ - size_metrics, - cell_width, - cell_height, - cell_height - cell_baseline, - underline_position, - underline_thickness, - strikethrough, - }); + // log.warn("METRICS={} width={d} height={d} baseline={d} underline_pos={d} underline_thickness={d} strikethrough={}", .{ + // size_metrics, + // cell_width, + // cell_height, + // cell_height - cell_baseline, + // underline_position, + // underline_thickness, + // strikethrough, + // }); return .{ .cell_width = cell_width, From e99376cac14dc59b33c3272ca71aed960e3165b7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Jun 2023 22:55:41 -0700 Subject: [PATCH 19/24] font: update comment --- src/font/face/coretext.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 9fb0774da..2380cc555 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -402,9 +402,7 @@ 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); From 3795cd6c2d8864e846c24a63f2f720244c79d783 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 1 Jul 2023 09:55:19 -0700 Subject: [PATCH 20/24] font: turn rasterization options into a struct, add thicken --- src/config.zig | 4 ++++ src/font/Group.zig | 18 ++++++++++-------- src/font/GroupCache.zig | 18 ++++++++++-------- src/font/face.zig | 14 ++++++++++++++ src/font/face/coretext.zig | 10 ++++------ src/font/face/freetype.zig | 18 ++++++++++-------- src/font/face/web_canvas.zig | 6 +++--- src/renderer/Metal.zig | 8 +++++--- src/renderer/OpenGL.zig | 6 +++--- 9 files changed, 63 insertions(+), 39 deletions(-) diff --git a/src/config.zig b/src/config.zig index a6c7c3b59..a5de9afca 100644 --- a/src/config.zig +++ b/src/config.zig @@ -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 }, diff --git a/src/font/Group.zig b/src/font/Group.zig index f7f9e271f..f32395608 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -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,7 @@ pub fn renderGlyph( const face = &self.faces.get(index.style).items[@intCast(index.idx)]; try face.load(self.lib, self.size); - const glyph = 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; } @@ -383,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); @@ -427,7 +429,7 @@ test { &atlas_greyscale, idx, glyph_index, - null, + .{}, ); } @@ -483,7 +485,7 @@ test "box glyph" { &atlas_greyscale, idx, 0x2500, - null, + .{}, ); try testing.expectEqual(@as(u32, 36), glyph.height); } @@ -514,7 +516,7 @@ test "resize" { &atlas_greyscale, idx, glyph_index, - null, + .{}, ); try testing.expectEqual(@as(u32, 11), glyph.height); @@ -531,7 +533,7 @@ test "resize" { &atlas_greyscale, idx, glyph_index, - null, + .{}, ); try testing.expectEqual(@as(u32, 21), glyph.height); @@ -574,7 +576,7 @@ test "discover monospace with fontconfig and freetype" { &atlas_greyscale, idx, glyph_index, - null, + .{}, ); } } diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 57a4f1acb..af73aac58 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -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); diff --git a/src/font/face.zig b/src/font/face.zig index b6d7465cf..24f0f5368 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -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 { diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 2380cc555..1b4ec2142 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -102,10 +102,8 @@ pub const Face = struct { alloc: Allocator, atlas: *font.Atlas, glyph_index: u32, - max_height: ?u16, + opts: font.face.RenderOptions, ) !font.Glyph { - _ = max_height; - var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)}; // Get the bounding rect for rendering this glyph. @@ -205,7 +203,7 @@ pub const Face = struct { }); ctx.setAllowsFontSmoothing(true); - ctx.setShouldSmoothFonts(false); // The amadeus "enthicken" + ctx.setShouldSmoothFonts(opts.thicken); // The amadeus "enthicken" ctx.setAllowsFontSubpixelQuantization(true); ctx.setShouldSubpixelQuantizeFonts(true); ctx.setAllowsFontSubpixelPositioning(true); @@ -479,7 +477,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).?, .{}); } } @@ -523,6 +521,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).?, .{}); } } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index eab180d62..a09a523ef 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -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; @@ -522,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); } } @@ -551,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); } } @@ -610,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, .{}); } diff --git a/src/font/face/web_canvas.zig b/src/font/face/web_canvas.zig index 821b5f422..458e018c1 100644 --- a/src/font/face/web_canvas.zig +++ b/src/font/face/web_canvas.zig @@ -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); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 7d41f2bc2..61e12a55b 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -996,7 +996,9 @@ 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)), + }, ); // If we're rendering a color font, we use the color atlas @@ -1031,7 +1033,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 +1085,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; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 26dfc015f..f130fc6ab 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -991,7 +991,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 +1144,7 @@ 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)) }, ); // If we're rendering a color font, we use the color atlas @@ -1190,7 +1190,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; From 0bb2d22052cb9f796880570935b028a6f9095ca4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 1 Jul 2023 10:01:40 -0700 Subject: [PATCH 21/24] renderer: hook up font thickening setting --- src/renderer/Metal.zig | 12 ++++++++++++ src/renderer/OpenGL.zig | 16 +++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 61e12a55b..760a2813b 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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.*; } @@ -998,6 +1009,7 @@ pub fn updateCell( shaper_cell.glyph_index, .{ .max_height = @intFromFloat(@ceil(self.cell_size.height)), + .thicken = self.config.font_thicken, }, ); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index f130fc6ab..e48c9d9bf 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -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 @@ -1144,7 +1147,10 @@ pub fn updateCell( self.alloc, shaper_run.font_index, shaper_cell.glyph_index, - .{ .max_height = @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 @@ -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.*; } From 51e42a62eda0ae9162c50cf582becadf86b45f93 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 1 Jul 2023 10:04:49 -0700 Subject: [PATCH 22/24] font: default rasterizer on macOS is now coretext --- src/font/main.zig | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/font/main.zig b/src/font/main.zig index f1e6b298d..b3ad82b73 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -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 From 126817cac282b76210f4276a26a0ed4f76833124 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 1 Jul 2023 10:12:29 -0700 Subject: [PATCH 23/24] coretext: tweak underline position --- src/font/face/coretext.zig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 1b4ec2142..dc33cabcd 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -423,12 +423,17 @@ pub const Face = struct { // 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; + // 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(); From 06f63288c8d7e74e09946d7f92a51b5ec271ba31 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 1 Jul 2023 10:15:50 -0700 Subject: [PATCH 24/24] coretext: address TODO --- src/font/face/coretext.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index dc33cabcd..cdc4bd17d 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -171,8 +171,10 @@ pub const Face = struct { return error.InvalidAtlasFormat; } - // Our buffer for rendering - // TODO(perf): cache this buffer + // 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); @memset(buf, 0);