mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-24 04:36:10 +03:00
@ -26,6 +26,27 @@ pub fn Context(comptime T: type) type {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setAllowsFontSmoothing(self: *T, v: bool) void {
|
||||
c.CGContextSetAllowsFontSmoothing(
|
||||
@ptrCast(self),
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setAllowsFontSubpixelPositioning(self: *T, v: bool) void {
|
||||
c.CGContextSetAllowsFontSubpixelPositioning(
|
||||
@ptrCast(self),
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setAllowsFontSubpixelQuantization(self: *T, v: bool) void {
|
||||
c.CGContextSetAllowsFontSubpixelQuantization(
|
||||
@ptrCast(self),
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setShouldAntialias(self: *T, v: bool) void {
|
||||
c.CGContextSetShouldAntialias(
|
||||
@ptrCast(self),
|
||||
@ -40,6 +61,20 @@ pub fn Context(comptime T: type) type {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setShouldSubpixelPositionFonts(self: *T, v: bool) void {
|
||||
c.CGContextSetShouldSubpixelPositionFonts(
|
||||
@ptrCast(self),
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setShouldSubpixelQuantizeFonts(self: *T, v: bool) void {
|
||||
c.CGContextSetShouldSubpixelQuantizeFonts(
|
||||
@ptrCast(self),
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setGrayFillColor(self: *T, gray: f64, alpha: f64) void {
|
||||
c.CGContextSetGrayFillColor(
|
||||
@ptrCast(self),
|
||||
@ -66,6 +101,16 @@ pub fn Context(comptime T: type) type {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setRGBStrokeColor(self: *T, r: f64, g: f64, b: f64, alpha: f64) void {
|
||||
c.CGContextSetRGBStrokeColor(
|
||||
@ptrCast(self),
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
alpha,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setTextDrawingMode(self: *T, mode: TextDrawingMode) void {
|
||||
c.CGContextSetTextDrawingMode(
|
||||
@ptrCast(self),
|
||||
|
@ -130,6 +130,14 @@ pub const Font = opaque {
|
||||
pub fn getUnderlineThickness(self: *Font) f64 {
|
||||
return c.CTFontGetUnderlineThickness(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub fn getUnitsPerEm(self: *Font) u32 {
|
||||
return c.CTFontGetUnitsPerEm(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub fn getSize(self: *Font) f64 {
|
||||
return c.CTFontGetSize(@ptrCast(self));
|
||||
}
|
||||
};
|
||||
|
||||
pub const FontOrientation = enum(c_uint) {
|
||||
|
@ -30,30 +30,12 @@ pub const Line = opaque {
|
||||
self: *Line,
|
||||
opts: LineBoundsOptions,
|
||||
) graphics.Rect {
|
||||
// return @bitCast(c.CGRect, c.CTLineGetBoundsWithOptions(
|
||||
// @ptrCast(c.CTLineRef, self),
|
||||
// opts.cval(),
|
||||
// ));
|
||||
|
||||
// We have to use a custom C wrapper here because there is some
|
||||
// C ABI issue happening.
|
||||
var result: graphics.Rect = undefined;
|
||||
zig_cabi_CTLineGetBoundsWithOptions(
|
||||
return @bitCast(c.CTLineGetBoundsWithOptions(
|
||||
@ptrCast(self),
|
||||
opts.cval(),
|
||||
@ptrCast(&result),
|
||||
);
|
||||
|
||||
return result;
|
||||
));
|
||||
}
|
||||
|
||||
// See getBoundsWithOptions
|
||||
extern "c" fn zig_cabi_CTLineGetBoundsWithOptions(
|
||||
c.CTLineRef,
|
||||
c.CTLineBoundsOptions,
|
||||
*c.CGRect,
|
||||
) void;
|
||||
|
||||
pub fn getTypographicBounds(
|
||||
self: *Line,
|
||||
ascent: ?*f64,
|
||||
|
@ -231,7 +231,7 @@ pub fn init(
|
||||
if (try disco_it.next()) |face| {
|
||||
log.info("font regular: {s}", .{try face.name()});
|
||||
try group.addFace(alloc, .regular, face);
|
||||
}
|
||||
} else std.log.warn("font-family not found: {s}", .{family});
|
||||
}
|
||||
if (config.@"font-family-bold") |family| {
|
||||
var disco_it = try disco.discover(.{
|
||||
@ -243,7 +243,7 @@ pub fn init(
|
||||
if (try disco_it.next()) |face| {
|
||||
log.info("font bold: {s}", .{try face.name()});
|
||||
try group.addFace(alloc, .bold, face);
|
||||
}
|
||||
} else std.log.warn("font-family-bold not found: {s}", .{family});
|
||||
}
|
||||
if (config.@"font-family-italic") |family| {
|
||||
var disco_it = try disco.discover(.{
|
||||
@ -255,7 +255,7 @@ pub fn init(
|
||||
if (try disco_it.next()) |face| {
|
||||
log.info("font italic: {s}", .{try face.name()});
|
||||
try group.addFace(alloc, .italic, face);
|
||||
}
|
||||
} else std.log.warn("font-family-italic not found: {s}", .{family});
|
||||
}
|
||||
if (config.@"font-family-bold-italic") |family| {
|
||||
var disco_it = try disco.discover(.{
|
||||
@ -268,7 +268,7 @@ pub fn init(
|
||||
if (try disco_it.next()) |face| {
|
||||
log.info("font bold+italic: {s}", .{try face.name()});
|
||||
try group.addFace(alloc, .bold_italic, face);
|
||||
}
|
||||
} else std.log.warn("font-family-bold-italic not found: {s}", .{family});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,10 @@ pub const Config = struct {
|
||||
else => 12,
|
||||
},
|
||||
|
||||
/// Draw fonts with a thicker stroke, if supported. This is only supported
|
||||
/// currently on macOS.
|
||||
@"font-thicken": bool = false,
|
||||
|
||||
/// Background color for the window.
|
||||
background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
|
||||
|
||||
|
@ -269,7 +269,7 @@ pub fn renderGlyph(
|
||||
atlas: *font.Atlas,
|
||||
index: FontIndex,
|
||||
glyph_index: u32,
|
||||
max_height: ?u16,
|
||||
opts: font.face.RenderOptions,
|
||||
) !Glyph {
|
||||
// Special-case fonts are rendered directly.
|
||||
if (index.special()) |sp| switch (sp) {
|
||||
@ -282,7 +282,9 @@ pub fn renderGlyph(
|
||||
|
||||
const face = &self.faces.get(index.style).items[@intCast(index.idx)];
|
||||
try face.load(self.lib, self.size);
|
||||
return try face.face.?.renderGlyph(alloc, atlas, glyph_index, max_height);
|
||||
const glyph = try face.face.?.renderGlyph(alloc, atlas, glyph_index, opts);
|
||||
// log.warn("GLYPH={}", .{glyph});
|
||||
return glyph;
|
||||
}
|
||||
|
||||
/// The wasm-compatible API.
|
||||
@ -381,7 +383,9 @@ pub const Wasm = struct {
|
||||
) !*Glyph {
|
||||
const idx = @as(FontIndex, @bitCast(@as(u8, @intCast(idx_))));
|
||||
const max_height = if (max_height_ <= 0) null else max_height_;
|
||||
const glyph = try self.renderGlyph(alloc, atlas, idx, cp, max_height);
|
||||
const glyph = try self.renderGlyph(alloc, atlas, idx, cp, .{
|
||||
.max_height = max_height,
|
||||
});
|
||||
|
||||
var result = try alloc.create(Glyph);
|
||||
errdefer alloc.destroy(result);
|
||||
@ -425,7 +429,7 @@ test {
|
||||
&atlas_greyscale,
|
||||
idx,
|
||||
glyph_index,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
@ -481,7 +485,7 @@ test "box glyph" {
|
||||
&atlas_greyscale,
|
||||
idx,
|
||||
0x2500,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
try testing.expectEqual(@as(u32, 36), glyph.height);
|
||||
}
|
||||
@ -512,7 +516,7 @@ test "resize" {
|
||||
&atlas_greyscale,
|
||||
idx,
|
||||
glyph_index,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
|
||||
try testing.expectEqual(@as(u32, 11), glyph.height);
|
||||
@ -529,7 +533,7 @@ test "resize" {
|
||||
&atlas_greyscale,
|
||||
idx,
|
||||
glyph_index,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
|
||||
try testing.expectEqual(@as(u32, 21), glyph.height);
|
||||
@ -572,7 +576,7 @@ test "discover monospace with fontconfig and freetype" {
|
||||
&atlas_greyscale,
|
||||
idx,
|
||||
glyph_index,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ pub fn renderGlyph(
|
||||
alloc: Allocator,
|
||||
index: Group.FontIndex,
|
||||
glyph_index: u32,
|
||||
max_height: ?u16,
|
||||
opts: font.face.RenderOptions,
|
||||
) !Glyph {
|
||||
const key: GlyphKey = .{ .index = index, .glyph = glyph_index };
|
||||
const gop = try self.glyphs.getOrPut(alloc, key);
|
||||
@ -140,7 +140,7 @@ pub fn renderGlyph(
|
||||
atlas,
|
||||
index,
|
||||
glyph_index,
|
||||
max_height,
|
||||
opts,
|
||||
) catch |err| switch (err) {
|
||||
// If the atlas is full, we resize it
|
||||
error.AtlasFull => blk: {
|
||||
@ -150,7 +150,7 @@ pub fn renderGlyph(
|
||||
atlas,
|
||||
index,
|
||||
glyph_index,
|
||||
max_height,
|
||||
opts,
|
||||
);
|
||||
},
|
||||
|
||||
@ -203,7 +203,7 @@ test {
|
||||
alloc,
|
||||
idx,
|
||||
glyph_index,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
@ -225,7 +225,7 @@ test {
|
||||
alloc,
|
||||
idx,
|
||||
glyph_index,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -300,7 +300,9 @@ pub const Wasm = struct {
|
||||
) !*Glyph {
|
||||
const idx = @as(Group.FontIndex, @bitCast(@as(u8, @intCast(idx_))));
|
||||
const max_height = if (max_height_ <= 0) null else max_height_;
|
||||
const glyph = try self.renderGlyph(alloc, idx, cp, max_height);
|
||||
const glyph = try self.renderGlyph(alloc, idx, cp, .{
|
||||
.max_height = max_height,
|
||||
});
|
||||
|
||||
var result = try alloc.create(Glyph);
|
||||
errdefer alloc.destroy(result);
|
||||
@ -352,7 +354,7 @@ test "resize" {
|
||||
alloc,
|
||||
idx,
|
||||
glyph_index,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
|
||||
try testing.expectEqual(@as(u32, 11), glyph.height);
|
||||
@ -368,7 +370,7 @@ test "resize" {
|
||||
alloc,
|
||||
idx,
|
||||
glyph_index,
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
|
||||
try testing.expectEqual(@as(u32, 21), glyph.height);
|
||||
|
@ -58,6 +58,20 @@ pub const Metrics = struct {
|
||||
strikethrough_thickness: f32,
|
||||
};
|
||||
|
||||
/// Additional options for rendering glyphs.
|
||||
pub const RenderOptions = struct {
|
||||
/// The maximum height of the glyph. If this is set, then any glyph
|
||||
/// larger than this height will be shrunk to this height. The scaling
|
||||
/// is typically naive, but ultimately up to the rasterizer.
|
||||
max_height: ?u16 = null,
|
||||
|
||||
/// Thicken the glyph. This draws the glyph with a thicker stroke width.
|
||||
/// This is purely an aesthetic setting.
|
||||
///
|
||||
/// This only works with CoreText currently.
|
||||
thicken: bool = false,
|
||||
};
|
||||
|
||||
pub const Foo = if (options.backend == .coretext) coretext.Face else void;
|
||||
|
||||
test {
|
||||
|
@ -5,6 +5,8 @@ const macos = @import("macos");
|
||||
const harfbuzz = @import("harfbuzz");
|
||||
const font = @import("../main.zig");
|
||||
|
||||
const log = std.log.scoped(.font_face);
|
||||
|
||||
pub const Face = struct {
|
||||
/// Our font face
|
||||
font: *macos.text.Font,
|
||||
@ -41,8 +43,10 @@ pub const Face = struct {
|
||||
/// 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(@floatFromInt(size.points), null);
|
||||
// Create a copy. The copyWithAttributes docs say the size is in points,
|
||||
// but we need to scale the points by the DPI and to do that we use our
|
||||
// function called "pixels".
|
||||
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null);
|
||||
errdefer ct_font.release();
|
||||
|
||||
var hb_font = try harfbuzz.coretext.createFont(ct_font);
|
||||
@ -93,39 +97,41 @@ pub const Face = struct {
|
||||
return @intCast(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: *font.Atlas,
|
||||
glyph_index: u32,
|
||||
max_height: ?u16,
|
||||
opts: font.face.RenderOptions,
|
||||
) !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(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 = @as(u32, @intFromFloat(@ceil(bounding[0].size.width)));
|
||||
const glyph_height = @as(u32, @intFromFloat(@ceil(bounding[0].size.height)));
|
||||
// Get the bounding rect for rendering this glyph.
|
||||
const rect = self.font.getBoundingRectForGlyphs(.horizontal, &glyphs, null);
|
||||
|
||||
// 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);
|
||||
// The x/y that we render the glyph at. The Y value has to be flipped
|
||||
// because our coordinates in 3D space are (0, 0) bottom left with
|
||||
// +y being up.
|
||||
const render_x = @floor(rect.origin.x);
|
||||
const render_y = @ceil(-rect.origin.y);
|
||||
|
||||
// The ascent is the amount of pixels above the baseline this glyph
|
||||
// is rendered. The ascent can be calculated by adding the full
|
||||
// glyph height to the origin.
|
||||
const glyph_ascent = @ceil(rect.size.height + rect.origin.y);
|
||||
|
||||
// The glyph height is basically rect.size.height but we do the
|
||||
// ascent plus the descent because both are rounded elements that
|
||||
// will make us more accurate.
|
||||
const height: u32 = @intFromFloat(glyph_ascent + render_y);
|
||||
|
||||
// The glyph width is our advertised bounding with plus the rounding
|
||||
// difference from our rendering X.
|
||||
const width: u32 = @intFromFloat(@ceil(rect.size.width + (rect.origin.x - render_x)));
|
||||
|
||||
// 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{
|
||||
if (width == 0 or height == 0) return font.Glyph{
|
||||
.width = 0,
|
||||
.height = 0,
|
||||
.offset_x = 0,
|
||||
@ -135,77 +141,157 @@ pub const Face = struct {
|
||||
.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);
|
||||
// Settings that are specific to if we are rendering text or emoji.
|
||||
const color: struct {
|
||||
color: bool,
|
||||
depth: u32,
|
||||
space: *macos.graphics.ColorSpace,
|
||||
context_opts: c_uint,
|
||||
} = if (self.presentation == .text) .{
|
||||
.color = false,
|
||||
.depth = 1,
|
||||
.space = try macos.graphics.ColorSpace.createDeviceGray(),
|
||||
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) &
|
||||
@intFromEnum(macos.graphics.ImageAlphaInfo.none),
|
||||
} else .{
|
||||
.color = true,
|
||||
.depth = 4,
|
||||
.space = try macos.graphics.ColorSpace.createDeviceRGB(),
|
||||
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.byte_order_32_little) |
|
||||
@intFromEnum(macos.graphics.ImageAlphaInfo.premultiplied_first),
|
||||
};
|
||||
defer color.space.release();
|
||||
|
||||
// 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);
|
||||
// This is just a safety check.
|
||||
if (atlas.format.depth() != color.depth) {
|
||||
log.warn("font atlas color depth doesn't equal font color depth atlas={} font={}", .{
|
||||
atlas.format.depth(),
|
||||
color.depth,
|
||||
});
|
||||
return error.InvalidAtlasFormat;
|
||||
}
|
||||
|
||||
// Our buffer for rendering. We could cache this but glyph rasterization
|
||||
// usually stabilizes pretty quickly and is very infrequent so I think
|
||||
// the allocation overhead is acceptable compared to the cost of
|
||||
// caching it forever or having to deal with a cache lifetime.
|
||||
var buf = try alloc.alloc(u8, width * height * color.depth);
|
||||
defer alloc.free(buf);
|
||||
std.mem.set(u8, buf, 0);
|
||||
|
||||
const space = try macos.graphics.ColorSpace.createDeviceGray();
|
||||
defer space.release();
|
||||
@memset(buf, 0);
|
||||
|
||||
const ctx = try macos.graphics.BitmapContext.create(
|
||||
buf,
|
||||
width,
|
||||
height,
|
||||
8,
|
||||
width,
|
||||
space,
|
||||
@intFromEnum(macos.graphics.BitmapInfo.alpha_mask) &
|
||||
@intFromEnum(macos.graphics.ImageAlphaInfo.none),
|
||||
width * color.depth,
|
||||
color.space,
|
||||
color.context_opts,
|
||||
);
|
||||
defer ctx.release();
|
||||
|
||||
// Perform an initial fill. This ensures that we don't have any
|
||||
// uninitialized pixels in the bitmap.
|
||||
if (color.color)
|
||||
ctx.setRGBFillColor(1, 1, 1, 0)
|
||||
else
|
||||
ctx.setGrayFillColor(0, 0);
|
||||
ctx.fillRect(.{
|
||||
.origin = .{ .x = 0, .y = 0 },
|
||||
.size = .{
|
||||
.width = @floatFromInt(width),
|
||||
.height = @floatFromInt(height),
|
||||
},
|
||||
});
|
||||
|
||||
ctx.setAllowsFontSmoothing(true);
|
||||
ctx.setShouldSmoothFonts(opts.thicken); // The amadeus "enthicken"
|
||||
ctx.setAllowsFontSubpixelQuantization(true);
|
||||
ctx.setShouldSubpixelQuantizeFonts(true);
|
||||
ctx.setAllowsFontSubpixelPositioning(true);
|
||||
ctx.setShouldSubpixelPositionFonts(true);
|
||||
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);
|
||||
|
||||
// Set our color for drawing
|
||||
if (color.color) {
|
||||
ctx.setRGBFillColor(1, 1, 1, 1);
|
||||
ctx.setRGBStrokeColor(1, 1, 1, 1);
|
||||
} else {
|
||||
ctx.setGrayFillColor(1, 1);
|
||||
ctx.setGrayStrokeColor(1, 1);
|
||||
}
|
||||
|
||||
// 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);
|
||||
// to get them to 0,0. We also add the padding so that they render
|
||||
// slightly off the edge of the bitmap.
|
||||
self.font.drawGlyphs(&glyphs, &.{
|
||||
.{
|
||||
.x = -1 * render_x,
|
||||
.y = render_y,
|
||||
},
|
||||
}, ctx);
|
||||
|
||||
const region = try atlas.reserve(alloc, width, height);
|
||||
const region = region: {
|
||||
// We need to add a 1px padding to the font so that we don't
|
||||
// get fuzzy issues when blending textures.
|
||||
const padding = 1;
|
||||
|
||||
// Get the full padded region
|
||||
var region = try atlas.reserve(
|
||||
alloc,
|
||||
width + (padding * 2), // * 2 because left+right
|
||||
height + (padding * 2), // * 2 because top+bottom
|
||||
);
|
||||
|
||||
// Modify the region so that we remove the padding so that
|
||||
// we write to the non-zero location. The data in an Altlas
|
||||
// is always initialized to zero (Atlas.clear) so we don't
|
||||
// need to worry about zero-ing that.
|
||||
region.x += padding;
|
||||
region.y += padding;
|
||||
region.width -= padding * 2;
|
||||
region.height -= padding * 2;
|
||||
break :region region;
|
||||
};
|
||||
atlas.set(region, buf);
|
||||
|
||||
const offset_y = offset_y: {
|
||||
const offset_y: i32 = 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;
|
||||
const baseline_with_offset = baseline_from_bottom + glyph_ascent;
|
||||
|
||||
// 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 @as(i32, @intCast(height)) + @as(i32, @intFromFloat(@ceil(baseline_with_offset)));
|
||||
break :offset_y @intFromFloat(@ceil(baseline_with_offset));
|
||||
};
|
||||
|
||||
return font.Glyph{
|
||||
.width = glyph_width,
|
||||
.height = glyph_height,
|
||||
.offset_x = @intFromFloat(@ceil(bounding[0].origin.x)),
|
||||
// log.warn("renderGlyph rect={} width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{
|
||||
// rect,
|
||||
// width,
|
||||
// height,
|
||||
// render_x,
|
||||
// render_y,
|
||||
// offset_y,
|
||||
// glyph_ascent,
|
||||
// self.metrics.cell_height,
|
||||
// self.metrics.cell_baseline,
|
||||
// });
|
||||
|
||||
return .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.offset_x = @intFromFloat(render_x),
|
||||
.offset_y = offset_y,
|
||||
.atlas_x = region.x + padding,
|
||||
.atlas_y = region.y + padding,
|
||||
.advance_x = @floatCast(advances[0].width),
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
|
||||
// This is not used, so we don't bother calculating it. If we
|
||||
// ever need it, we can calculate it using getAdvancesForGlyph.
|
||||
.advance_x = 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -273,8 +359,32 @@ pub const Face = struct {
|
||||
defer fs.release();
|
||||
|
||||
// Create a rectangle to fit all of this and create a frame of it.
|
||||
// The rectangle needs to fit all of our text so we use some
|
||||
// heuristics based on cell_width to calculate it. We are
|
||||
// VERY generous with our rect here because the text must fit.
|
||||
const path_rect = rect: {
|
||||
// The cell width at this point is valid, so let's make it
|
||||
// fit 50 characters wide.
|
||||
const width = cell_width * 50;
|
||||
|
||||
// We are trying to calculate height so we don't know how
|
||||
// high to make our frame. Well-behaved fonts will probably
|
||||
// not have a height greater than 4x the width, so let's just
|
||||
// generously use that metric to ensure we fit the frame.
|
||||
const big_cell_height = cell_width * 4;
|
||||
|
||||
// If we are fitting about ~50 characters per row, we need
|
||||
// unit.len / 50 rows to fit all of our text.
|
||||
const rows = (unit.len / 50) * 2;
|
||||
|
||||
// Our final height is the number of rows times our generous height.
|
||||
const height = rows * big_cell_height;
|
||||
|
||||
break :rect macos.graphics.Rect.init(10, 10, width, height);
|
||||
};
|
||||
|
||||
const path = try macos.graphics.MutablePath.create();
|
||||
path.addRect(null, macos.graphics.Rect.init(10, 10, 200, 200));
|
||||
path.addRect(null, path_rect);
|
||||
defer path.release();
|
||||
const frame = try fs.createFrame(
|
||||
macos.foundation.Range.init(0, 0),
|
||||
@ -292,38 +402,53 @@ pub const Face = struct {
|
||||
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.
|
||||
// Get the bounds of the line to determine the ascent.
|
||||
const bounds = line.getBoundsWithOptions(.{ .exclude_leading = true });
|
||||
const bounds_ascent = bounds.size.height + bounds.origin.y;
|
||||
const baseline = @floor(bounds_ascent + 0.5);
|
||||
|
||||
var ascent: f64 = 0;
|
||||
var descent: f64 = 0;
|
||||
var leading: f64 = 0;
|
||||
_ = line.getTypographicBounds(&ascent, &descent, &leading);
|
||||
// This is an alternate approach to the above to calculate the
|
||||
// baseline by simply using the ascender. Using this approach led
|
||||
// to less accurate results, but I'm leaving it here for reference.
|
||||
// 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(points[0].y - points[1].y),
|
||||
.ascent = @floatCast(ascent),
|
||||
.ascent = @floatCast(baseline),
|
||||
};
|
||||
};
|
||||
|
||||
// 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 -
|
||||
@as(f32, @floatCast(ct_font.getUnderlinePosition())));
|
||||
const underline_thickness = @ceil(@as(f32, @floatCast(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}", .{
|
||||
// Underline position is based on our baseline because the font advertised
|
||||
// underline position is based on a zero baseline. We add a small amount
|
||||
// to the underline position to make it look better.
|
||||
const underline_position = @ceil(cell_baseline -
|
||||
@as(f32, @floatCast(ct_font.getUnderlinePosition())) +
|
||||
1);
|
||||
|
||||
// Note: is this useful?
|
||||
// const units_per_em = ct_font.getUnitsPerEm();
|
||||
// const units_per_point = @intToFloat(f64, units_per_em) / ct_font.getSize();
|
||||
|
||||
// std.log.warn("font size size={d}", .{ct_font.getSize()});
|
||||
// std.log.warn("font metrics 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,
|
||||
@ -359,7 +484,7 @@ test {
|
||||
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);
|
||||
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,6 +528,6 @@ test "in-memory" {
|
||||
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);
|
||||
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ pub const Face = struct {
|
||||
alloc: Allocator,
|
||||
atlas: *font.Atlas,
|
||||
glyph_index: u32,
|
||||
max_height: ?u16,
|
||||
opts: font.face.RenderOptions,
|
||||
) !Glyph {
|
||||
// If our glyph has color, we want to render the color
|
||||
try self.face.loadGlyph(glyph_index, .{
|
||||
@ -188,7 +188,7 @@ pub const Face = struct {
|
||||
// and copy the atlas.
|
||||
const bitmap_original = bitmap_converted orelse bitmap_ft;
|
||||
const bitmap_resized: ?freetype.c.struct_FT_Bitmap_ = resized: {
|
||||
const max = max_height orelse break :resized null;
|
||||
const max = opts.max_height orelse break :resized null;
|
||||
const bm = bitmap_original;
|
||||
if (bm.rows <= max) break :resized null;
|
||||
|
||||
@ -328,6 +328,14 @@ pub const Face = struct {
|
||||
break :offset_y glyph_metrics.bitmap_top + @as(c_int, @intFromFloat(self.metrics.cell_baseline));
|
||||
};
|
||||
|
||||
// log.warn("renderGlyph width={} height={} offset_x={} offset_y={} glyph_metrics={}", .{
|
||||
// tgt_w,
|
||||
// tgt_h,
|
||||
// glyph_metrics.bitmap_left,
|
||||
// offset_y,
|
||||
// glyph_metrics,
|
||||
// });
|
||||
|
||||
// Store glyph metadata
|
||||
return Glyph{
|
||||
.width = tgt_w,
|
||||
@ -514,16 +522,16 @@ test {
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?, null);
|
||||
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?, .{});
|
||||
}
|
||||
|
||||
// Test resizing
|
||||
{
|
||||
const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, null);
|
||||
const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{});
|
||||
try testing.expectEqual(@as(u32, 11), g1.height);
|
||||
|
||||
try ft_font.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
|
||||
const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, null);
|
||||
const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{});
|
||||
try testing.expectEqual(@as(u32, 21), g2.height);
|
||||
}
|
||||
}
|
||||
@ -543,11 +551,13 @@ test "color emoji" {
|
||||
|
||||
try testing.expectEqual(Presentation.emoji, ft_font.presentation);
|
||||
|
||||
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, null);
|
||||
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{});
|
||||
|
||||
// resize
|
||||
{
|
||||
const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, 24);
|
||||
const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{
|
||||
.max_height = 24,
|
||||
});
|
||||
try testing.expectEqual(@as(u32, 24), glyph.height);
|
||||
}
|
||||
}
|
||||
@ -602,5 +612,5 @@ test "mono to rgba" {
|
||||
defer ft_font.deinit();
|
||||
|
||||
// glyph 3 is mono in Noto
|
||||
_ = try ft_font.renderGlyph(alloc, &atlas, 3, null);
|
||||
_ = try ft_font.renderGlyph(alloc, &atlas, 3, .{});
|
||||
}
|
||||
|
@ -190,9 +190,9 @@ pub const Face = struct {
|
||||
alloc: Allocator,
|
||||
atlas: *font.Atlas,
|
||||
glyph_index: u32,
|
||||
max_height: ?u16,
|
||||
opts: font.face.RenderOptions,
|
||||
) !font.Glyph {
|
||||
_ = max_height;
|
||||
_ = opts;
|
||||
|
||||
var render = try self.renderGlyphInternal(alloc, glyph_index);
|
||||
defer render.deinit();
|
||||
@ -551,7 +551,7 @@ pub const Wasm = struct {
|
||||
}
|
||||
|
||||
fn face_render_glyph_(face: *Face, atlas: *font.Atlas, codepoint: u32) !*font.Glyph {
|
||||
const glyph = try face.renderGlyph(alloc, atlas, codepoint, null);
|
||||
const glyph = try face.renderGlyph(alloc, atlas, codepoint, .{});
|
||||
|
||||
const result = try alloc.create(font.Glyph);
|
||||
errdefer alloc.destroy(result);
|
||||
|
@ -71,12 +71,10 @@ pub const Backend = enum {
|
||||
};
|
||||
}
|
||||
|
||||
return if (target.isDarwin()) darwin: {
|
||||
// On macOS right now, the coretext renderer is still pretty buggy
|
||||
// so we default to coretext for font discovery and freetype for
|
||||
// rasterization.
|
||||
break :darwin .coretext_freetype;
|
||||
} else .fontconfig_freetype;
|
||||
// macOS also supports "coretext_freetype" but there is no scenario
|
||||
// that is the default. It is only used by people who want to
|
||||
// self-compile Ghostty and prefer the freetype aesthetic.
|
||||
return if (target.isDarwin()) .coretext else .fontconfig_freetype;
|
||||
}
|
||||
|
||||
// All the functions below can be called at comptime or runtime to
|
||||
|
@ -340,7 +340,31 @@ const PixmanImpl = struct {
|
||||
|
||||
const width = @as(u32, @intCast(self.image.getWidth()));
|
||||
const height = @as(u32, @intCast(self.image.getHeight()));
|
||||
const region = try atlas.reserve(alloc, width, height);
|
||||
|
||||
// Allocate our texture atlas region
|
||||
const region = region: {
|
||||
// We need to add a 1px padding to the font so that we don't
|
||||
// get fuzzy issues when blending textures.
|
||||
const padding = 1;
|
||||
|
||||
// Get the full padded region
|
||||
var region = try atlas.reserve(
|
||||
alloc,
|
||||
width + (padding * 2), // * 2 because left+right
|
||||
height + (padding * 2), // * 2 because top+bottom
|
||||
);
|
||||
|
||||
// Modify the region so that we remove the padding so that
|
||||
// we write to the non-zero location. The data in an Altlas
|
||||
// is always initialized to zero (Atlas.clear) so we don't
|
||||
// need to worry about zero-ing that.
|
||||
region.x += padding;
|
||||
region.y += padding;
|
||||
region.width -= padding * 2;
|
||||
region.height -= padding * 2;
|
||||
break :region region;
|
||||
};
|
||||
|
||||
if (region.width > 0 and region.height > 0) {
|
||||
const depth = atlas.format.depth();
|
||||
|
||||
|
@ -129,6 +129,7 @@ const GPUCellMode = enum(u8) {
|
||||
/// configuration. This must be exported so that we don't need to
|
||||
/// pass around Config pointers which makes memory management a pain.
|
||||
pub const DerivedConfig = struct {
|
||||
font_thicken: bool,
|
||||
cursor_color: ?terminal.color.RGB,
|
||||
background: terminal.color.RGB,
|
||||
foreground: terminal.color.RGB,
|
||||
@ -142,6 +143,8 @@ pub const DerivedConfig = struct {
|
||||
_ = alloc_gpa;
|
||||
|
||||
return .{
|
||||
.font_thicken = config.@"font-thicken",
|
||||
|
||||
.cursor_color = if (config.@"cursor-color") |col|
|
||||
col.toTerminalRGB()
|
||||
else
|
||||
@ -738,6 +741,14 @@ fn drawCells(
|
||||
|
||||
/// Update the configuration.
|
||||
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
||||
// If font thickening settings change, we need to reset our
|
||||
// font texture completely because we need to re-render the glyphs.
|
||||
if (self.config.font_thicken != config.font_thicken) {
|
||||
self.font_group.reset();
|
||||
self.font_group.atlas_greyscale.clear();
|
||||
self.font_group.atlas_color.clear();
|
||||
}
|
||||
|
||||
self.config = config.*;
|
||||
}
|
||||
|
||||
@ -996,7 +1007,10 @@ pub fn updateCell(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
shaper_cell.glyph_index,
|
||||
@intFromFloat(@ceil(self.cell_size.height)),
|
||||
.{
|
||||
.max_height = @intFromFloat(@ceil(self.cell_size.height)),
|
||||
.thicken = self.config.font_thicken,
|
||||
},
|
||||
);
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
@ -1031,7 +1045,7 @@ pub fn updateCell(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
|
||||
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
|
||||
@ -1083,7 +1097,7 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void {
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
null,
|
||||
.{},
|
||||
) catch |err| {
|
||||
log.warn("error rendering cursor glyph err={}", .{err});
|
||||
return;
|
||||
|
@ -226,6 +226,7 @@ const GPUCellMode = enum(u8) {
|
||||
/// configuration. This must be exported so that we don't need to
|
||||
/// pass around Config pointers which makes memory management a pain.
|
||||
pub const DerivedConfig = struct {
|
||||
font_thicken: bool,
|
||||
cursor_color: ?terminal.color.RGB,
|
||||
background: terminal.color.RGB,
|
||||
foreground: terminal.color.RGB,
|
||||
@ -239,6 +240,8 @@ pub const DerivedConfig = struct {
|
||||
_ = alloc_gpa;
|
||||
|
||||
return .{
|
||||
.font_thicken = config.@"font-thicken",
|
||||
|
||||
.cursor_color = if (config.@"cursor-color") |col|
|
||||
col.toTerminalRGB()
|
||||
else
|
||||
@ -991,7 +994,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void {
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
null,
|
||||
.{},
|
||||
) catch |err| {
|
||||
log.warn("error rendering cursor glyph err={}", .{err});
|
||||
return;
|
||||
@ -1144,7 +1147,10 @@ pub fn updateCell(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
shaper_cell.glyph_index,
|
||||
@intFromFloat(@ceil(self.cell_size.height)),
|
||||
.{
|
||||
.max_height = @intFromFloat(@ceil(self.cell_size.height)),
|
||||
.thicken = self.config.font_thicken,
|
||||
},
|
||||
);
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
@ -1190,7 +1196,7 @@ pub fn updateCell(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
null,
|
||||
.{},
|
||||
);
|
||||
|
||||
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
|
||||
@ -1254,6 +1260,14 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid
|
||||
|
||||
/// Update the configuration.
|
||||
pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
|
||||
// If font thickening settings change, we need to reset our
|
||||
// font texture completely because we need to re-render the glyphs.
|
||||
if (self.config.font_thicken != config.font_thicken) {
|
||||
self.font_group.reset();
|
||||
self.font_group.atlas_greyscale.clear();
|
||||
self.font_group.atlas_color.clear();
|
||||
}
|
||||
|
||||
self.config = config.*;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user