mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-24 20:56:08 +03:00
412 lines
16 KiB
Zig
412 lines
16 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
const macos = @import("macos");
|
|
const harfbuzz = @import("harfbuzz");
|
|
const font = @import("../main.zig");
|
|
const Atlas = @import("../../Atlas.zig");
|
|
|
|
pub const Face = struct {
|
|
/// Our font face
|
|
font: *macos.text.Font,
|
|
|
|
/// Harfbuzz font corresponding to this face.
|
|
hb_font: harfbuzz.Font,
|
|
|
|
/// The presentation for this font.
|
|
presentation: font.Presentation,
|
|
|
|
/// Metrics for this font face. These are useful for renderers.
|
|
metrics: font.face.Metrics,
|
|
|
|
/// Initialize a CoreText-based font from a TTF/TTC in memory.
|
|
pub fn init(lib: font.Library, source: [:0]const u8, size: font.face.DesiredSize) !Face {
|
|
_ = lib;
|
|
|
|
const data = try macos.foundation.Data.createWithBytesNoCopy(source);
|
|
defer data.release();
|
|
|
|
const arr = macos.text.createFontDescriptorsFromData(data) orelse
|
|
return error.FontInitFailure;
|
|
defer arr.release();
|
|
|
|
const desc = arr.getValueAtIndex(macos.text.FontDescriptor, 0);
|
|
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
|
defer ct_font.release();
|
|
|
|
return try initFontCopy(ct_font, size);
|
|
}
|
|
|
|
/// Initialize a CoreText-based face from another initialized font face
|
|
/// but with a new size. This is often how CoreText fonts are initialized
|
|
/// 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(@intToFloat(f32, size.points), null);
|
|
errdefer ct_font.release();
|
|
|
|
var hb_font = try harfbuzz.coretext.createFont(ct_font);
|
|
errdefer hb_font.destroy();
|
|
|
|
const traits = ct_font.getSymbolicTraits();
|
|
|
|
return Face{
|
|
.font = ct_font,
|
|
.hb_font = hb_font,
|
|
.presentation = if (traits.color_glyphs) .emoji else .text,
|
|
.metrics = try calcMetrics(ct_font),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Face) void {
|
|
self.font.release();
|
|
self.hb_font.destroy();
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Resize the font in-place. If this succeeds, the caller is responsible
|
|
/// for clearing any glyph caches, font atlas data, etc.
|
|
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {
|
|
// We just create a copy and replace ourself
|
|
const face = try initFontCopy(self.font, size);
|
|
self.deinit();
|
|
self.* = face;
|
|
}
|
|
|
|
/// Returns the glyph index for the given Unicode code point. If this
|
|
/// face doesn't support this glyph, null is returned.
|
|
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
|
// Turn UTF-32 into UTF-16 for CT API
|
|
var unichars: [2]u16 = undefined;
|
|
const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(cp, &unichars);
|
|
const len: usize = if (pair) 2 else 1;
|
|
|
|
// Get our glyphs
|
|
var glyphs = [2]macos.graphics.Glyph{ 0, 0 };
|
|
if (!self.font.getGlyphsForCharacters(unichars[0..len], glyphs[0..len]))
|
|
return null;
|
|
|
|
// We can have pairs due to chars like emoji but we expect all of them
|
|
// to decode down into exactly one glyph ID.
|
|
if (pair) assert(glyphs[1] == 0);
|
|
|
|
return @intCast(u32, 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: *Atlas,
|
|
glyph_index: u32,
|
|
max_height: ?u16,
|
|
) !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(macos.graphics.Glyph, 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 = @floatToInt(u32, @ceil(bounding[0].size.width));
|
|
const glyph_height = @floatToInt(u32, @ceil(bounding[0].size.height));
|
|
|
|
// 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);
|
|
|
|
// 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{
|
|
.width = 0,
|
|
.height = 0,
|
|
.offset_x = 0,
|
|
.offset_y = 0,
|
|
.atlas_x = 0,
|
|
.atlas_y = 0,
|
|
.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);
|
|
|
|
// 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);
|
|
defer alloc.free(buf);
|
|
std.mem.set(u8, buf, 0);
|
|
|
|
const space = try macos.graphics.ColorSpace.createDeviceGray();
|
|
defer space.release();
|
|
|
|
const ctx = try macos.graphics.BitmapContext.create(
|
|
buf,
|
|
width,
|
|
height,
|
|
8,
|
|
width,
|
|
space,
|
|
@enumToInt(macos.graphics.BitmapInfo.alpha_mask) &
|
|
@enumToInt(macos.graphics.ImageAlphaInfo.none),
|
|
);
|
|
defer ctx.release();
|
|
|
|
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);
|
|
|
|
// 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);
|
|
|
|
const region = try atlas.reserve(alloc, width, height);
|
|
atlas.set(region, buf);
|
|
|
|
const offset_y = 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;
|
|
|
|
// 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 @intCast(i32, height) + @floatToInt(i32, @ceil(baseline_with_offset));
|
|
};
|
|
|
|
return font.Glyph{
|
|
.width = glyph_width,
|
|
.height = glyph_height,
|
|
.offset_x = @floatToInt(i32, @ceil(bounding[0].origin.x)),
|
|
.offset_y = offset_y,
|
|
.atlas_x = region.x + padding,
|
|
.atlas_y = region.y + padding,
|
|
.advance_x = @floatCast(f32, advances[0].width),
|
|
};
|
|
}
|
|
|
|
fn calcMetrics(ct_font: *macos.text.Font) !font.face.Metrics {
|
|
// Cell width is calculated by calculating the widest width of the
|
|
// visible ASCII characters. Usually 'M' is widest but we just take
|
|
// whatever is widest.
|
|
const cell_width: f32 = cell_width: {
|
|
// Build a comptime array of all the ASCII chars
|
|
const unichars = comptime unichars: {
|
|
const len = 127 - 32;
|
|
var result: [len]u16 = undefined;
|
|
var i: u16 = 32;
|
|
while (i < 127) : (i += 1) {
|
|
result[i - 32] = i;
|
|
}
|
|
|
|
break :unichars result;
|
|
};
|
|
|
|
// Get our glyph IDs for the ASCII chars
|
|
var glyphs: [unichars.len]macos.graphics.Glyph = undefined;
|
|
_ = ct_font.getGlyphsForCharacters(&unichars, &glyphs);
|
|
|
|
// Get all our advances
|
|
var advances: [unichars.len]macos.graphics.Size = undefined;
|
|
_ = ct_font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
|
|
|
|
// Find the maximum advance
|
|
var max: f64 = 0;
|
|
var i: usize = 0;
|
|
while (i < advances.len) : (i += 1) {
|
|
max = @max(advances[i].width, max);
|
|
}
|
|
|
|
break :cell_width @floatCast(f32, max);
|
|
};
|
|
|
|
// Calculate the cell height by using CoreText's layout engine
|
|
// to tell us after laying out some text. This is inspired by Kitty's
|
|
// approach. Previously we were using descent/ascent math and it wasn't
|
|
// quite the same with CoreText and I never figured out why.
|
|
const layout_metrics: struct {
|
|
height: f32,
|
|
ascent: f32,
|
|
} = metrics: {
|
|
const unit = "AQWMH_gyl " ** 100;
|
|
|
|
// Setup our string we'll layout. We just stylize a string of
|
|
// ASCII characters to setup the letters.
|
|
const string = try macos.foundation.MutableAttributedString.create(unit.len);
|
|
defer string.release();
|
|
const rep = try macos.foundation.String.createWithBytes(unit, .utf8, false);
|
|
defer rep.release();
|
|
string.replaceString(macos.foundation.Range.init(0, 0), rep);
|
|
string.setAttribute(
|
|
macos.foundation.Range.init(0, unit.len),
|
|
macos.text.StringAttribute.font,
|
|
ct_font,
|
|
);
|
|
|
|
// Create our framesetter with our string. This is used to
|
|
// emit "frames" for the layout.
|
|
const fs = try macos.text.Framesetter.createWithAttributedString(
|
|
@ptrCast(*macos.foundation.AttributedString, string),
|
|
);
|
|
defer fs.release();
|
|
|
|
// Create a rectangle to fit all of this and create a frame of it.
|
|
const path = try macos.graphics.MutablePath.create();
|
|
path.addRect(null, macos.graphics.Rect.init(10, 10, 200, 200));
|
|
defer path.release();
|
|
const frame = try fs.createFrame(
|
|
macos.foundation.Range.init(0, 0),
|
|
@ptrCast(*macos.graphics.Path, path),
|
|
null,
|
|
);
|
|
defer frame.release();
|
|
|
|
// Use our text layout from earlier to measure the difference
|
|
// between the lines.
|
|
var points: [2]macos.graphics.Point = undefined;
|
|
frame.getLineOrigins(macos.foundation.Range.init(0, 1), points[0..]);
|
|
frame.getLineOrigins(macos.foundation.Range.init(1, 1), points[1..]);
|
|
|
|
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.
|
|
|
|
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(f32, points[0].y - points[1].y),
|
|
.ascent = @floatCast(f32, ascent),
|
|
};
|
|
};
|
|
|
|
// 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 -
|
|
@floatCast(f32, ct_font.getUnderlinePosition()));
|
|
const underline_thickness = @ceil(@floatCast(f32, 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}", .{
|
|
// cell_width,
|
|
// cell_height,
|
|
// cell_baseline,
|
|
// underline_position,
|
|
// underline_thickness,
|
|
// });
|
|
return font.face.Metrics{
|
|
.cell_width = cell_width,
|
|
.cell_height = cell_height,
|
|
.cell_baseline = cell_baseline,
|
|
.underline_position = underline_position,
|
|
.underline_thickness = underline_thickness,
|
|
.strikethrough_position = strikethrough_position,
|
|
.strikethrough_thickness = strikethrough_thickness,
|
|
};
|
|
}
|
|
};
|
|
|
|
test {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var atlas = try Atlas.init(alloc, 512, .greyscale);
|
|
defer atlas.deinit(alloc);
|
|
|
|
const name = try macos.foundation.String.createWithBytes("Monaco", .utf8, false);
|
|
defer name.release();
|
|
const desc = try macos.text.FontDescriptor.createWithNameAndSize(name, 12);
|
|
defer desc.release();
|
|
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
|
defer ct_font.release();
|
|
|
|
var face = try Face.initFontCopy(ct_font, .{ .points = 12 });
|
|
defer face.deinit();
|
|
|
|
try testing.expectEqual(font.Presentation.text, face.presentation);
|
|
|
|
// Generate all visible ASCII
|
|
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);
|
|
}
|
|
}
|
|
|
|
test "emoji" {
|
|
const testing = std.testing;
|
|
|
|
const name = try macos.foundation.String.createWithBytes("Apple Color Emoji", .utf8, false);
|
|
defer name.release();
|
|
const desc = try macos.text.FontDescriptor.createWithNameAndSize(name, 12);
|
|
defer desc.release();
|
|
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
|
defer ct_font.release();
|
|
|
|
var face = try Face.initFontCopy(ct_font, .{ .points = 18 });
|
|
defer face.deinit();
|
|
|
|
// Presentation
|
|
try testing.expectEqual(font.Presentation.emoji, face.presentation);
|
|
|
|
// Glyph index check
|
|
try testing.expect(face.glyphIndex('🥸') != null);
|
|
}
|
|
|
|
test "in-memory" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
const testFont = @import("../test.zig").fontRegular;
|
|
|
|
var atlas = try Atlas.init(alloc, 512, .greyscale);
|
|
defer atlas.deinit(alloc);
|
|
|
|
var lib = try font.Library.init();
|
|
defer lib.deinit();
|
|
|
|
var face = try Face.init(lib, testFont, .{ .points = 12 });
|
|
defer face.deinit();
|
|
|
|
try testing.expectEqual(font.Presentation.text, face.presentation);
|
|
|
|
// Generate all visible ASCII
|
|
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);
|
|
}
|
|
}
|