mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
Merge pull request #2144 from ghostty-org/synth-bold
CoreText: Synthetic Bold
This commit is contained in:
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user