font/coretext: support synthetic bold

This commit is contained in:
Mitchell Hashimoto
2024-08-23 20:52:13 -07:00
parent 74291793db
commit d22551cd31
3 changed files with 79 additions and 9 deletions

View File

@ -62,7 +62,9 @@ const c = @cImport({
/// Finally, some styles may be synthesized if they are not supported. /// 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 /// 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 /// 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 /// You can disable styles completely by using the `font-style` set of
/// configurations. See the documentation for `font-style` for more information. /// configurations. See the documentation for `font-style` for more information.

View File

@ -273,9 +273,15 @@ pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void {
// If we don't have bold, use the regular font. // If we don't have bold, use the regular font.
const bold_list = self.faces.getPtr(.bold); const bold_list = self.faces.getPtr(.bold);
if (bold_list.count() == 0) { if (bold_list.count() == 0) bold: {
log.warn("bold style not available, using regular font", .{}); const synthetic = self.syntheticBold(regular_entry) catch |err| {
try bold_list.append(alloc, .{ .alias = regular_entry }); 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, 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. // Create an synthetic italic font face from the given entry and return it.
fn syntheticItalic(self: *Collection, entry: *Entry) !Face { fn syntheticItalic(self: *Collection, entry: *Entry) !Face {
// Not all font backends support synthetic italicization. // Not all font backends support synthetic italicization.

View File

@ -24,6 +24,10 @@ pub const Face = struct {
/// Set quirks.disableDefaultFontFeatures /// Set quirks.disableDefaultFontFeatures
quirks_disable_default_font_features: bool = false, 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 /// If the face can possibly be colored, then this is the state
/// used to check for color information. This is null if the font /// 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 /// 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 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, /// Returns the font name. If allocation is required, buf will be used,
/// but sometimes allocation isn't required and a static string is /// but sometimes allocation isn't required and a static string is
/// returned. /// returned.
@ -300,11 +323,23 @@ pub const Face = struct {
.advance_x = 0, .advance_x = 0,
}; };
// If we're doing thicken, then getBoundsForGlyphs does not take // Additional padding we need to add to the bitmap context itself
// into account the anti-aliasing that will be added to the glyph. // due to the glyph being larger than standard.
// We need to add some padding to allow that to happen. A padding of const padding_ctx: u32 = padding_ctx: {
// 2 is usually enough for anti-aliasing. // If we're doing thicken, then getBoundsForGlyphs does not take
const padding_ctx: u32 = if (opts.thicken) 2 else 0; // 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_width: u32 = width + (padding_ctx * 2);
const padded_height: u32 = height + (padding_ctx * 2); const padded_height: u32 = height + (padding_ctx * 2);
@ -390,6 +425,13 @@ pub const Face = struct {
context.setGrayStrokeColor(ctx, 1, 1); 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 // We want to render the glyphs at (0,0), but the glyphs themselves
// are offset by bearings, so we have to undo those bearings in order // are offset by bearings, so we have to undo those bearings in order
// to get them to 0,0. We also add the padding so that they render // to get them to 0,0. We also add the padding so that they render