diff --git a/src/config/Config.zig b/src/config/Config.zig index 10b2e9524..e2f5999ed 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -62,7 +62,9 @@ const c = @cImport({ /// Finally, some styles may be synthesized if they are not supported. /// For example, if a font does not have an italic style and no alternative /// italic font is specified, Ghostty will synthesize an italic style by -/// applying a slant to the regular style. +/// applying a slant to the regular style. If you want to disable these +/// synthesized styles then you can use the `font-style` configurations +/// as documented below. /// /// You can disable styles completely by using the `font-style` set of /// configurations. See the documentation for `font-style` for more information. diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 256c6b73e..01237a107 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -273,9 +273,15 @@ pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void { // If we don't have bold, use the regular font. const bold_list = self.faces.getPtr(.bold); - if (bold_list.count() == 0) { - log.warn("bold style not available, using regular font", .{}); - try bold_list.append(alloc, .{ .alias = regular_entry }); + if (bold_list.count() == 0) bold: { + const synthetic = self.syntheticBold(regular_entry) catch |err| { + log.warn("failed to create synthetic bold, bold style will not be available err={}", .{err}); + try bold_list.append(alloc, .{ .alias = regular_entry }); + break :bold; + }; + + log.info("synthetic bold face created", .{}); + try bold_list.append(alloc, .{ .loaded = synthetic }); } // If we don't have bold italic, use the regular italic font. @@ -304,6 +310,26 @@ pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void { } } +// Create an synthetic italic font face from the given entry and return it. +fn syntheticBold(self: *Collection, entry: *Entry) !Face { + // Not all font backends support synthetic bold. + if (comptime !@hasDecl(Face, "syntheticBold")) return error.SyntheticBoldUnavailable; + + // We require loading options to create a synthetic bold face. + const opts = self.load_options orelse return error.DeferredLoadingUnavailable; + + // Try to bold it. + const regular = try self.getFaceFromEntry(entry); + const face = try regular.syntheticBold(opts.faceOptions()); + + var buf: [256]u8 = undefined; + if (face.name(&buf)) |name| { + log.info("font synthetic bold created family={s}", .{name}); + } else |_| {} + + return face; +} + // Create an synthetic italic font face from the given entry and return it. fn syntheticItalic(self: *Collection, entry: *Entry) !Face { // Not all font backends support synthetic italicization. diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 55d6dffdf..835994542 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -24,6 +24,10 @@ pub const Face = struct { /// Set quirks.disableDefaultFontFeatures quirks_disable_default_font_features: bool = false, + /// True if this font face should be rasterized with a synthetic bold + /// effect. This is used for fonts that don't have a bold variant. + synthetic_bold: ?f64 = null, + /// If the face can possibly be colored, then this is the state /// used to check for color information. This is null if the font /// can't possibly be colored (i.e. doesn't have SVG, sbix, etc @@ -175,6 +179,25 @@ pub const Face = struct { return try initFont(ct_font, opts); } + /// Return a new face that is the same as this but applies a synthetic + /// bold effect to it. This is useful for fonts that don't have a bold + /// variant. + pub fn syntheticBold(self: *const Face, opts: font.face.Options) !Face { + const ct_font = try self.font.copyWithAttributes(0.0, null, null); + errdefer ct_font.release(); + var face = try initFont(ct_font, opts); + + // TO determine our synthetic bold line width we get a multiplier + // from the font size in points. This is a heuristic that is based + // on the fact that a line width of 1 looks good to me at 12 points + // and we want to scale that up roughly linearly with the font size. + const points_f64: f64 = @floatCast(opts.size.points); + const line_width = @max(points_f64 / 12.0, 1); + face.synthetic_bold = line_width; + + return face; + } + /// Returns the font name. If allocation is required, buf will be used, /// but sometimes allocation isn't required and a static string is /// returned. @@ -300,11 +323,23 @@ pub const Face = struct { .advance_x = 0, }; - // If we're doing thicken, then getBoundsForGlyphs does not take - // into account the anti-aliasing that will be added to the glyph. - // We need to add some padding to allow that to happen. A padding of - // 2 is usually enough for anti-aliasing. - const padding_ctx: u32 = if (opts.thicken) 2 else 0; + // Additional padding we need to add to the bitmap context itself + // due to the glyph being larger than standard. + const padding_ctx: u32 = padding_ctx: { + // If we're doing thicken, then getBoundsForGlyphs does not take + // into account the anti-aliasing that will be added to the glyph. + // We need to add some padding to allow that to happen. A padding of + // 2 is usually enough for anti-aliasing. + var result: u32 = if (opts.thicken) 2 else 0; + + // If we have a synthetic bold, add padding for the stroke width + if (self.synthetic_bold) |line_width| { + // x2 for top and bottom padding + result += @intFromFloat(@ceil(line_width) * 2); + } + + break :padding_ctx result; + }; const padded_width: u32 = width + (padding_ctx * 2); const padded_height: u32 = height + (padding_ctx * 2); @@ -390,6 +425,13 @@ pub const Face = struct { context.setGrayStrokeColor(ctx, 1, 1); } + // If we are drawing with synthetic bold then use a fill stroke + // which strokes the outlines of the glyph making a more bold look. + if (self.synthetic_bold) |line_width| { + context.setTextDrawingMode(ctx, .fill_stroke); + context.setLineWidth(ctx, line_width); + } + // 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. We also add the padding so that they render