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 2a8368053..842867930 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -260,7 +260,8 @@ pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void { // If we can't create a synthetic italic face, we'll just use the regular // face for italic. const italic_list = self.faces.getPtr(.italic); - if (italic_list.count() == 0) italic: { + const have_italic = italic_list.count() > 0; + if (!have_italic) italic: { const synthetic = self.syntheticItalic(regular_entry) catch |err| { log.warn("failed to create synthetic italic, italic style will not be available err={}", .{err}); try italic_list.append(alloc, .{ .alias = regular_entry }); @@ -273,52 +274,95 @@ 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 }); + const have_bold = bold_list.count() > 0; + if (!have_bold) 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. + // If we don't have bold italic, we attempt to synthesize a bold variant + // of the italic font. If we can't do that, we'll use the italic font. const bold_italic_list = self.faces.getPtr(.bold_italic); - if (bold_italic_list.count() == 0) { - log.warn("bold italic style not available, using italic font", .{}); + if (bold_italic_list.count() == 0) bold_italic: { + // Prefer to synthesize on top of the face we already had. If we + // have bold then we try to synthesize italic on top of bold. + if (have_bold) { + if (self.syntheticItalic(bold_list.at(0))) |synthetic| { + log.info("synthetic bold italic face created from bold", .{}); + try bold_italic_list.append(alloc, .{ .loaded = synthetic }); + break :bold_italic; + } else |_| {} - // Nested alias isn't allowed so if the italic entry is an - // alias then we use the aliased entry. - const italic_entry = italic_list.at(0); - switch (italic_entry.*) { - .alias => |v| try bold_italic_list.append( - alloc, - .{ .alias = v }, - ), - - .loaded, - .fallback_loaded, - .deferred, - .fallback_deferred, - => try bold_italic_list.append( - alloc, - .{ .alias = italic_entry }, - ), + // If synthesizing italic failed, then we try to synthesize + // bold on whatever italic font we have. } + + // Nested alias isn't allowed so we need to unwrap the italic entry. + const base_entry = base: { + const italic_entry = italic_list.at(0); + break :base switch (italic_entry.*) { + .alias => |v| v, + + .loaded, + .fallback_loaded, + .deferred, + .fallback_deferred, + => italic_entry, + }; + }; + + if (self.syntheticBold(base_entry)) |synthetic| { + log.info("synthetic bold italic face created from italic", .{}); + try bold_italic_list.append(alloc, .{ .loaded = synthetic }); + break :bold_italic; + } else |_| {} + + log.warn("bold italic style not available, using italic font", .{}); + try bold_italic_list.append(alloc, .{ .alias = base_entry }); } } -// 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 auto-italicization. - if (comptime !@hasDecl(Face, "italicize")) return error.SyntheticItalicUnavailable; +// Create a synthetic bold 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 auto-italicize. + // 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 a 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. + if (comptime !@hasDecl(Face, "syntheticItalic")) return error.SyntheticItalicUnavailable; + + // We require loading options to create a synthetic italic face. const opts = self.load_options orelse return error.DeferredLoadingUnavailable; // Try to italicize it. const regular = try self.getFaceFromEntry(entry); - const face = try regular.italicize(opts.faceOptions()); + const face = try regular.syntheticItalic(opts.faceOptions()); var buf: [256]u8 = undefined; if (face.name(&buf)) |name| { - log.info("font auto-italicized: {s}", .{name}); + log.info("font synthetic italic created family={s}", .{name}); } else |_| {} return face; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index adafd93d0..202230d5d 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 @@ -169,12 +173,31 @@ pub const Face = struct { /// Return a new face that is the same as this but has a transformation /// matrix applied to italicize it. - pub fn italicize(self: *const Face, opts: font.face.Options) !Face { + pub fn syntheticItalic(self: *const Face, opts: font.face.Options) !Face { const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null); errdefer ct_font.release(); 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