diff --git a/pkg/macos/graphics/context.zig b/pkg/macos/graphics/context.zig index 7ba03b31c..15d0fd906 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), @@ -66,6 +101,16 @@ pub fn Context(comptime T: type) type { ); } + pub fn setRGBStrokeColor(self: *T, r: f64, g: f64, b: f64, alpha: f64) void { + c.CGContextSetRGBStrokeColor( + @ptrCast(self), + r, + g, + b, + alpha, + ); + } + pub fn setTextDrawingMode(self: *T, mode: TextDrawingMode) void { c.CGContextSetTextDrawingMode( @ptrCast(self), diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index 6db9fa5c9..cf24bc5d0 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(self)); + } + + pub fn getSize(self: *Font) f64 { + return c.CTFontGetSize(@ptrCast(self)); + } }; pub const FontOrientation = enum(c_uint) { 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/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}); } } 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 804438c44..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,9 @@ pub fn renderGlyph( const face = &self.faces.get(index.style).items[@intCast(index.idx)]; try face.load(self.lib, self.size); - return try face.face.?.renderGlyph(alloc, atlas, glyph_index, max_height); + const glyph = try face.face.?.renderGlyph(alloc, atlas, glyph_index, opts); + // log.warn("GLYPH={}", .{glyph}); + return glyph; } /// The wasm-compatible API. @@ -381,7 +383,9 @@ pub const Wasm = struct { ) !*Glyph { const idx = @as(FontIndex, @bitCast(@as(u8, @intCast(idx_)))); const max_height = if (max_height_ <= 0) null else max_height_; - const glyph = try self.renderGlyph(alloc, atlas, idx, cp, max_height); + const glyph = try self.renderGlyph(alloc, atlas, idx, cp, .{ + .max_height = max_height, + }); var result = try alloc.create(Glyph); errdefer alloc.destroy(result); @@ -425,7 +429,7 @@ test { &atlas_greyscale, idx, glyph_index, - null, + .{}, ); } @@ -481,7 +485,7 @@ test "box glyph" { &atlas_greyscale, idx, 0x2500, - null, + .{}, ); try testing.expectEqual(@as(u32, 36), glyph.height); } @@ -512,7 +516,7 @@ test "resize" { &atlas_greyscale, idx, glyph_index, - null, + .{}, ); try testing.expectEqual(@as(u32, 11), glyph.height); @@ -529,7 +533,7 @@ test "resize" { &atlas_greyscale, idx, glyph_index, - null, + .{}, ); try testing.expectEqual(@as(u32, 21), glyph.height); @@ -572,7 +576,7 @@ test "discover monospace with fontconfig and freetype" { &atlas_greyscale, idx, glyph_index, - null, + .{}, ); } } 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 a95fcb70a..cdc4bd17d 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, @@ -41,8 +43,10 @@ pub const Face = struct { /// because the font is loaded at a default size during discovery, and then /// adjusted to the final size for final load. pub fn initFontCopy(base: *macos.text.Font, size: font.face.DesiredSize) !Face { - // Create a copy - const ct_font = try base.copyWithAttributes(@floatFromInt(size.points), null); + // Create a copy. The copyWithAttributes docs say the size is in points, + // but we need to scale the points by the DPI and to do that we use our + // function called "pixels". + const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null); errdefer ct_font.release(); var hb_font = try harfbuzz.coretext.createFont(ct_font); @@ -93,39 +97,41 @@ pub const Face = struct { return @intCast(glyphs[0]); } - /// Render a glyph using the glyph index. The rendered glyph is stored in the - /// given texture atlas. pub fn renderGlyph( self: Face, alloc: Allocator, atlas: *font.Atlas, glyph_index: u32, - max_height: ?u16, + opts: font.face.RenderOptions, ) !font.Glyph { - // We add a small pixel padding around the edge of our glyph so that - // anti-aliasing and smoothing doesn't cause us to pick up the pixels - // of another glyph when packed into the atlas. - const padding = 1; - - _ = max_height; - var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)}; - // Get the bounding rect for this glyph to determine the width/height - // of the bitmap. We use the rounded up width/height of the bounding rect. - var bounding: [1]macos.graphics.Rect = undefined; - _ = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, &bounding); - const glyph_width = @as(u32, @intFromFloat(@ceil(bounding[0].size.width))); - const glyph_height = @as(u32, @intFromFloat(@ceil(bounding[0].size.height))); + // Get the bounding rect for rendering this glyph. + const rect = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, null); - // Width and height. Note the padding doubling is because we want - // the padding on both sides (top/bottom, left/right). - const width = glyph_width + (padding * 2); - const height = glyph_height + (padding * 2); + // The x/y that we render the glyph at. The Y value has to be flipped + // because our coordinates in 3D space are (0, 0) bottom left with + // +y being up. + const render_x = @floor(rect.origin.x); + const render_y = @ceil(-rect.origin.y); + + // The ascent is the amount of pixels above the baseline this glyph + // is rendered. The ascent can be calculated by adding the full + // glyph height to the origin. + const glyph_ascent = @ceil(rect.size.height + rect.origin.y); + + // The glyph height is basically rect.size.height but we do the + // ascent plus the descent because both are rounded elements that + // will make us more accurate. + const height: u32 = @intFromFloat(glyph_ascent + render_y); + + // The glyph width is our advertised bounding with plus the rounding + // difference from our rendering X. + const width: u32 = @intFromFloat(@ceil(rect.size.width + (rect.origin.x - render_x))); // This bitmap is blank. I've seen it happen in a font, I don't know why. // If it is empty, we just return a valid glyph struct that does nothing. - if (glyph_width == 0) return font.Glyph{ + if (width == 0 or height == 0) return font.Glyph{ .width = 0, .height = 0, .offset_x = 0, @@ -135,77 +141,157 @@ pub const Face = struct { .advance_x = 0, }; - // Get the advance that we need for the glyph - var advances: [1]macos.graphics.Size = undefined; - _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); + // Settings that are specific to if we are rendering text or emoji. + const color: struct { + color: bool, + depth: u32, + space: *macos.graphics.ColorSpace, + context_opts: c_uint, + } = if (self.presentation == .text) .{ + .color = false, + .depth = 1, + .space = try macos.graphics.ColorSpace.createDeviceGray(), + .context_opts = @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) & + @intFromEnum(macos.graphics.ImageAlphaInfo.none), + } else .{ + .color = true, + .depth = 4, + .space = try macos.graphics.ColorSpace.createDeviceRGB(), + .context_opts = @intFromEnum(macos.graphics.BitmapInfo.byte_order_32_little) | + @intFromEnum(macos.graphics.ImageAlphaInfo.premultiplied_first), + }; + defer color.space.release(); - // Our buffer for rendering - // TODO(perf): cache this buffer - // TODO(mitchellh): color is going to require a depth here - var buf = try alloc.alloc(u8, width * height); + // This is just a safety check. + if (atlas.format.depth() != color.depth) { + log.warn("font atlas color depth doesn't equal font color depth atlas={} font={}", .{ + atlas.format.depth(), + color.depth, + }); + return error.InvalidAtlasFormat; + } + + // Our buffer for rendering. We could cache this but glyph rasterization + // usually stabilizes pretty quickly and is very infrequent so I think + // the allocation overhead is acceptable compared to the cost of + // caching it forever or having to deal with a cache lifetime. + var buf = try alloc.alloc(u8, width * height * color.depth); defer alloc.free(buf); - std.mem.set(u8, buf, 0); - - const space = try macos.graphics.ColorSpace.createDeviceGray(); - defer space.release(); + @memset(buf, 0); const ctx = try macos.graphics.BitmapContext.create( buf, width, height, 8, - width, - space, - @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) & - @intFromEnum(macos.graphics.ImageAlphaInfo.none), + width * color.depth, + color.space, + color.context_opts, ); defer ctx.release(); + // Perform an initial fill. This ensures that we don't have any + // uninitialized pixels in the bitmap. + if (color.color) + ctx.setRGBFillColor(1, 1, 1, 0) + else + ctx.setGrayFillColor(0, 0); + ctx.fillRect(.{ + .origin = .{ .x = 0, .y = 0 }, + .size = .{ + .width = @floatFromInt(width), + .height = @floatFromInt(height), + }, + }); + + ctx.setAllowsFontSmoothing(true); + ctx.setShouldSmoothFonts(opts.thicken); // The amadeus "enthicken" + ctx.setAllowsFontSubpixelQuantization(true); + ctx.setShouldSubpixelQuantizeFonts(true); + ctx.setAllowsFontSubpixelPositioning(true); + ctx.setShouldSubpixelPositionFonts(true); ctx.setAllowsAntialiasing(true); ctx.setShouldAntialias(true); - ctx.setShouldSmoothFonts(true); - ctx.setGrayFillColor(1, 1); - ctx.setGrayStrokeColor(1, 1); - ctx.setTextDrawingMode(.fill_stroke); - ctx.setTextMatrix(macos.graphics.AffineTransform.identity()); - ctx.setTextPosition(0, 0); + + // Set our color for drawing + if (color.color) { + ctx.setRGBFillColor(1, 1, 1, 1); + ctx.setRGBStrokeColor(1, 1, 1, 1); + } else { + ctx.setGrayFillColor(1, 1); + ctx.setGrayStrokeColor(1, 1); + } // We want to render the glyphs at (0,0), but the glyphs themselves // are offset by bearings, so we have to undo those bearings in order - // to get them to 0,0. - var pos = [_]macos.graphics.Point{.{ - .x = padding + (-1 * bounding[0].origin.x), - .y = padding + (-1 * bounding[0].origin.y), - }}; - self.font.drawGlyphs(&glyphs, &pos, ctx); + // to get them to 0,0. We also add the padding so that they render + // slightly off the edge of the bitmap. + self.font.drawGlyphs(&glyphs, &.{ + .{ + .x = -1 * render_x, + .y = render_y, + }, + }, ctx); - const region = try atlas.reserve(alloc, width, height); + const region = region: { + // We need to add a 1px padding to the font so that we don't + // get fuzzy issues when blending textures. + const padding = 1; + + // Get the full padded region + var region = try atlas.reserve( + alloc, + width + (padding * 2), // * 2 because left+right + height + (padding * 2), // * 2 because top+bottom + ); + + // Modify the region so that we remove the padding so that + // we write to the non-zero location. The data in an Altlas + // is always initialized to zero (Atlas.clear) so we don't + // need to worry about zero-ing that. + region.x += padding; + region.y += padding; + region.width -= padding * 2; + region.height -= padding * 2; + break :region region; + }; atlas.set(region, buf); - const offset_y = offset_y: { + const offset_y: i32 = offset_y: { // Our Y coordinate in 3D is (0, 0) bottom left, +y is UP. // We need to calculate our baseline from the bottom of a cell. const baseline_from_bottom = self.metrics.cell_height - self.metrics.cell_baseline; // Next we offset our baseline by the bearing in the font. We // ADD here because CoreText y is UP. - const baseline_with_offset = baseline_from_bottom + bounding[0].origin.y; + const baseline_with_offset = baseline_from_bottom + glyph_ascent; - // Finally, since we're rendering at (0, 0), the glyph will render - // by default below the line. We have to add height (glyph height) - // so that we shift the glyph UP to be on the line, then we add our - // baseline offset to move the glyph further UP to match the baseline. - break :offset_y @as(i32, @intCast(height)) + @as(i32, @intFromFloat(@ceil(baseline_with_offset))); + break :offset_y @intFromFloat(@ceil(baseline_with_offset)); }; - return font.Glyph{ - .width = glyph_width, - .height = glyph_height, - .offset_x = @intFromFloat(@ceil(bounding[0].origin.x)), + // log.warn("renderGlyph rect={} width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{ + // rect, + // width, + // height, + // render_x, + // render_y, + // offset_y, + // glyph_ascent, + // self.metrics.cell_height, + // self.metrics.cell_baseline, + // }); + + return .{ + .width = width, + .height = height, + .offset_x = @intFromFloat(render_x), .offset_y = offset_y, - .atlas_x = region.x + padding, - .atlas_y = region.y + padding, - .advance_x = @floatCast(advances[0].width), + .atlas_x = region.x, + .atlas_y = region.y, + + // This is not used, so we don't bother calculating it. If we + // ever need it, we can calculate it using getAdvancesForGlyph. + .advance_x = 0, }; } @@ -273,8 +359,32 @@ pub const Face = struct { defer fs.release(); // Create a rectangle to fit all of this and create a frame of it. + // The rectangle needs to fit all of our text so we use some + // heuristics based on cell_width to calculate it. We are + // VERY generous with our rect here because the text must fit. + const path_rect = rect: { + // The cell width at this point is valid, so let's make it + // fit 50 characters wide. + const width = cell_width * 50; + + // We are trying to calculate height so we don't know how + // high to make our frame. Well-behaved fonts will probably + // not have a height greater than 4x the width, so let's just + // generously use that metric to ensure we fit the frame. + const big_cell_height = cell_width * 4; + + // If we are fitting about ~50 characters per row, we need + // unit.len / 50 rows to fit all of our text. + const rows = (unit.len / 50) * 2; + + // Our final height is the number of rows times our generous height. + const height = rows * big_cell_height; + + break :rect macos.graphics.Rect.init(10, 10, width, height); + }; + const path = try macos.graphics.MutablePath.create(); - path.addRect(null, macos.graphics.Rect.init(10, 10, 200, 200)); + path.addRect(null, path_rect); defer path.release(); const frame = try fs.createFrame( macos.foundation.Range.init(0, 0), @@ -292,38 +402,53 @@ pub const Face = struct { const lines = frame.getLines(); const line = lines.getValueAtIndex(macos.text.Line, 0); - // NOTE(mitchellh): For some reason, CTLineGetBoundsWithOptions - // returns garbage and I can't figure out why... so we use the - // raw ascender. + // Get the bounds of the line to determine the ascent. + const bounds = line.getBoundsWithOptions(.{ .exclude_leading = true }); + const bounds_ascent = bounds.size.height + bounds.origin.y; + const baseline = @floor(bounds_ascent + 0.5); - var ascent: f64 = 0; - var descent: f64 = 0; - var leading: f64 = 0; - _ = line.getTypographicBounds(&ascent, &descent, &leading); + // This is an alternate approach to the above to calculate the + // baseline by simply using the ascender. Using this approach led + // to less accurate results, but I'm leaving it here for reference. + // var ascent: f64 = 0; + // var descent: f64 = 0; + // var leading: f64 = 0; + // _ = line.getTypographicBounds(&ascent, &descent, &leading); //std.log.warn("ascent={} descent={} leading={}", .{ ascent, descent, leading }); break :metrics .{ .height = @floatCast(points[0].y - points[1].y), - .ascent = @floatCast(ascent), + .ascent = @floatCast(baseline), }; }; // All of these metrics are based on our layout above. const cell_height = layout_metrics.height; const cell_baseline = layout_metrics.ascent; - const underline_position = @ceil(layout_metrics.ascent - - @as(f32, @floatCast(ct_font.getUnderlinePosition()))); const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness()))); const strikethrough_position = cell_baseline * 0.6; const strikethrough_thickness = underline_thickness; - // std.log.warn("width={d}, height={d} baseline={d} underline_pos={d} underline_thickness={d}", .{ + // Underline position is based on our baseline because the font advertised + // underline position is based on a zero baseline. We add a small amount + // to the underline position to make it look better. + const underline_position = @ceil(cell_baseline - + @as(f32, @floatCast(ct_font.getUnderlinePosition())) + + 1); + + // Note: is this useful? + // const units_per_em = ct_font.getUnitsPerEm(); + // const units_per_point = @intToFloat(f64, units_per_em) / ct_font.getSize(); + + // std.log.warn("font size size={d}", .{ct_font.getSize()}); + // std.log.warn("font metrics width={d}, height={d} baseline={d} underline_pos={d} underline_thickness={d}", .{ // cell_width, // cell_height, // cell_baseline, // underline_position, // underline_thickness, // }); + return font.face.Metrics{ .cell_width = cell_width, .cell_height = cell_height, @@ -359,7 +484,7 @@ test { var i: u8 = 32; while (i < 127) : (i += 1) { try testing.expect(face.glyphIndex(i) != null); - _ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, null); + _ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{}); } } @@ -403,6 +528,6 @@ test "in-memory" { var i: u8 = 32; while (i < 127) : (i += 1) { try testing.expect(face.glyphIndex(i) != null); - _ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, null); + _ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{}); } } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index fdeb27e82..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; @@ -328,6 +328,14 @@ pub const Face = struct { break :offset_y glyph_metrics.bitmap_top + @as(c_int, @intFromFloat(self.metrics.cell_baseline)); }; + // log.warn("renderGlyph width={} height={} offset_x={} offset_y={} glyph_metrics={}", .{ + // tgt_w, + // tgt_h, + // glyph_metrics.bitmap_left, + // offset_y, + // glyph_metrics, + // }); + // Store glyph metadata return Glyph{ .width = tgt_w, @@ -514,16 +522,16 @@ test { // Generate all visible ASCII var i: u8 = 32; while (i < 127) : (i += 1) { - _ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?, null); + _ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?, .{}); } // Test resizing { - const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, null); + const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{}); try testing.expectEqual(@as(u32, 11), g1.height); try ft_font.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); - const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, null); + const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{}); try testing.expectEqual(@as(u32, 21), g2.height); } } @@ -543,11 +551,13 @@ test "color emoji" { try testing.expectEqual(Presentation.emoji, ft_font.presentation); - _ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, null); + _ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{}); // resize { - const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, 24); + const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{ + .max_height = 24, + }); try testing.expectEqual(@as(u32, 24), glyph.height); } } @@ -602,5 +612,5 @@ test "mono to rgba" { defer ft_font.deinit(); // glyph 3 is mono in Noto - _ = try ft_font.renderGlyph(alloc, &atlas, 3, null); + _ = try ft_font.renderGlyph(alloc, &atlas, 3, .{}); } 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/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 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(); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 7d41f2bc2..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.*; } @@ -996,7 +1007,10 @@ pub fn updateCell( self.alloc, shaper_run.font_index, shaper_cell.glyph_index, - @intFromFloat(@ceil(self.cell_size.height)), + .{ + .max_height = @intFromFloat(@ceil(self.cell_size.height)), + .thicken = self.config.font_thicken, + }, ); // If we're rendering a color font, we use the color atlas @@ -1031,7 +1045,7 @@ pub fn updateCell( self.alloc, font.sprite_index, @intFromEnum(sprite), - null, + .{}, ); const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; @@ -1083,7 +1097,7 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void { self.alloc, font.sprite_index, @intFromEnum(sprite), - null, + .{}, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); return; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 26dfc015f..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 @@ -991,7 +994,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void { self.alloc, font.sprite_index, @intFromEnum(sprite), - null, + .{}, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); return; @@ -1144,7 +1147,10 @@ pub fn updateCell( self.alloc, shaper_run.font_index, shaper_cell.glyph_index, - @intFromFloat(@ceil(self.cell_size.height)), + .{ + .max_height = @intFromFloat(@ceil(self.cell_size.height)), + .thicken = self.config.font_thicken, + }, ); // If we're rendering a color font, we use the color atlas @@ -1190,7 +1196,7 @@ pub fn updateCell( self.alloc, font.sprite_index, @intFromEnum(sprite), - null, + .{}, ); const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; @@ -1254,6 +1260,14 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid /// Update the configuration. pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { + // If font thickening settings change, we need to reset our + // font texture completely because we need to re-render the glyphs. + if (self.config.font_thicken != config.font_thicken) { + self.font_group.reset(); + self.font_group.atlas_greyscale.clear(); + self.font_group.atlas_color.clear(); + } + self.config = config.*; }