mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #1056 from mitchellh/ct-shaping
CoreText font shaper, fix glyph offset handling (in Harfbuzz, too)
This commit is contained in:
@ -17,4 +17,12 @@ pub const Font = struct {
|
||||
pub fn destroy(self: *Font) void {
|
||||
c.hb_font_destroy(self.handle);
|
||||
}
|
||||
|
||||
pub fn setScale(self: *Font, x: u32, y: u32) void {
|
||||
c.hb_font_set_scale(
|
||||
self.handle,
|
||||
@intCast(x),
|
||||
@intCast(y),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -6,6 +6,17 @@ const text = @import("../text.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
pub const AttributedString = opaque {
|
||||
pub fn create(
|
||||
str: *foundation.String,
|
||||
attributes: *foundation.Dictionary,
|
||||
) Allocator.Error!*AttributedString {
|
||||
return @constCast(@ptrCast(c.CFAttributedStringCreate(
|
||||
null,
|
||||
@ptrCast(str),
|
||||
@ptrCast(attributes),
|
||||
) orelse return Allocator.Error.OutOfMemory));
|
||||
}
|
||||
|
||||
pub fn release(self: *AttributedString) void {
|
||||
foundation.CFRelease(self);
|
||||
}
|
||||
|
@ -39,6 +39,23 @@ pub const Dictionary = opaque {
|
||||
key,
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn getKeysAndValues(self: *Dictionary, alloc: Allocator) !struct {
|
||||
keys: []?*const anyopaque,
|
||||
values: []?*const anyopaque,
|
||||
} {
|
||||
const count = self.getCount();
|
||||
const keys = try alloc.alloc(?*const anyopaque, count);
|
||||
errdefer alloc.free(keys);
|
||||
const values = try alloc.alloc(?*const anyopaque, count);
|
||||
errdefer alloc.free(values);
|
||||
c.CFDictionaryGetKeysAndValues(
|
||||
@ptrCast(self),
|
||||
@ptrCast(keys.ptr),
|
||||
@ptrCast(values.ptr),
|
||||
);
|
||||
return .{ .keys = keys, .values = values };
|
||||
}
|
||||
};
|
||||
|
||||
pub const MutableDictionary = opaque {
|
||||
|
@ -66,6 +66,31 @@ pub const String = opaque {
|
||||
}
|
||||
};
|
||||
|
||||
pub const MutableString = opaque {
|
||||
pub fn create(cap: usize) !*MutableString {
|
||||
return @ptrCast(c.CFStringCreateMutable(
|
||||
null,
|
||||
@intCast(cap),
|
||||
) orelse return Allocator.Error.OutOfMemory);
|
||||
}
|
||||
|
||||
pub fn release(self: *MutableString) void {
|
||||
foundation.CFRelease(self);
|
||||
}
|
||||
|
||||
pub fn string(self: *MutableString) *String {
|
||||
return @ptrCast(self);
|
||||
}
|
||||
|
||||
pub fn appendCharacters(self: *MutableString, chars: []const u16) void {
|
||||
c.CFStringAppendCharacters(
|
||||
@ptrCast(self),
|
||||
chars.ptr,
|
||||
@intCast(chars.len),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub const StringComparison = packed struct {
|
||||
case_insensitive: bool = false,
|
||||
_unused_2: bool = false,
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub const c = @import("text/c.zig");
|
||||
pub usingnamespace @import("text/font.zig");
|
||||
pub usingnamespace @import("text/font_collection.zig");
|
||||
pub usingnamespace @import("text/font_descriptor.zig");
|
||||
@ -5,6 +6,7 @@ pub usingnamespace @import("text/font_manager.zig");
|
||||
pub usingnamespace @import("text/frame.zig");
|
||||
pub usingnamespace @import("text/framesetter.zig");
|
||||
pub usingnamespace @import("text/line.zig");
|
||||
pub usingnamespace @import("text/run.zig");
|
||||
pub usingnamespace @import("text/stylized_strings.zig");
|
||||
|
||||
test {
|
||||
|
@ -47,6 +47,10 @@ pub const Font = opaque {
|
||||
return @ptrCast(@constCast(c.CTFontCopyFontDescriptor(@ptrCast(self))));
|
||||
}
|
||||
|
||||
pub fn copyFeatures(self: *Font) *foundation.Array {
|
||||
return @ptrCast(@constCast(c.CTFontCopyFeatures(@ptrCast(self))));
|
||||
}
|
||||
|
||||
pub fn getGlyphCount(self: *Font) usize {
|
||||
return @intCast(c.CTFontGetGlyphCount(@ptrCast(self)));
|
||||
}
|
||||
|
@ -49,6 +49,10 @@ pub const Line = opaque {
|
||||
leading,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getGlyphRuns(self: *Line) *foundation.Array {
|
||||
return @constCast(@ptrCast(c.CTLineGetGlyphRuns(@ptrCast(self))));
|
||||
}
|
||||
};
|
||||
|
||||
pub const LineBoundsOptions = packed struct {
|
||||
|
93
pkg/macos/text/run.zig
Normal file
93
pkg/macos/text/run.zig
Normal file
@ -0,0 +1,93 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const foundation = @import("../foundation.zig");
|
||||
const graphics = @import("../graphics.zig");
|
||||
const text = @import("../text.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
pub const Run = opaque {
|
||||
pub fn release(self: *Run) void {
|
||||
foundation.CFRelease(self);
|
||||
}
|
||||
|
||||
pub fn getGlyphCount(self: *Run) usize {
|
||||
return @intCast(c.CTRunGetGlyphCount(@ptrCast(self)));
|
||||
}
|
||||
|
||||
pub fn getGlyphsPtr(self: *Run) []const graphics.Glyph {
|
||||
const len = self.getGlyphCount();
|
||||
if (len == 0) return &.{};
|
||||
const ptr = c.CTRunGetGlyphsPtr(@ptrCast(self)) orelse &.{};
|
||||
return ptr[0..len];
|
||||
}
|
||||
|
||||
pub fn getGlyphs(self: *Run, alloc: Allocator) ![]const graphics.Glyph {
|
||||
const len = self.getGlyphCount();
|
||||
const ptr = try alloc.alloc(graphics.Glyph, len);
|
||||
errdefer alloc.free(ptr);
|
||||
c.CTRunGetGlyphs(
|
||||
@ptrCast(self),
|
||||
.{ .location = 0, .length = 0 },
|
||||
@ptrCast(ptr.ptr),
|
||||
);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn getPositionsPtr(self: *Run) []const graphics.Point {
|
||||
const len = self.getGlyphCount();
|
||||
if (len == 0) return &.{};
|
||||
const ptr = c.CTRunGetPositionsPtr(@ptrCast(self)) orelse &.{};
|
||||
return ptr[0..len];
|
||||
}
|
||||
|
||||
pub fn getPositions(self: *Run, alloc: Allocator) ![]const graphics.Point {
|
||||
const len = self.getGlyphCount();
|
||||
const ptr = try alloc.alloc(graphics.Point, len);
|
||||
errdefer alloc.free(ptr);
|
||||
c.CTRunGetPositions(
|
||||
@ptrCast(self),
|
||||
.{ .location = 0, .length = 0 },
|
||||
@ptrCast(ptr.ptr),
|
||||
);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn getAdvancesPtr(self: *Run) []const graphics.Size {
|
||||
const len = self.getGlyphCount();
|
||||
if (len == 0) return &.{};
|
||||
const ptr = c.CTRunGetAdvancesPtr(@ptrCast(self)) orelse &.{};
|
||||
return ptr[0..len];
|
||||
}
|
||||
|
||||
pub fn getAdvances(self: *Run, alloc: Allocator) ![]const graphics.Size {
|
||||
const len = self.getGlyphCount();
|
||||
const ptr = try alloc.alloc(graphics.Size, len);
|
||||
errdefer alloc.free(ptr);
|
||||
c.CTRunGetAdvances(
|
||||
@ptrCast(self),
|
||||
.{ .location = 0, .length = 0 },
|
||||
@ptrCast(ptr.ptr),
|
||||
);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn getStringIndicesPtr(self: *Run) []const usize {
|
||||
const len = self.getGlyphCount();
|
||||
if (len == 0) return &.{};
|
||||
const ptr = c.CTRunGetStringIndicesPtr(@ptrCast(self)) orelse &.{};
|
||||
return ptr[0..len];
|
||||
}
|
||||
|
||||
pub fn getStringIndices(self: *Run, alloc: Allocator) ![]const usize {
|
||||
const len = self.getGlyphCount();
|
||||
const ptr = try alloc.alloc(usize, len);
|
||||
errdefer alloc.free(ptr);
|
||||
c.CTRunGetStringIndices(
|
||||
@ptrCast(self),
|
||||
.{ .location = 0, .length = 0 },
|
||||
@ptrCast(ptr.ptr),
|
||||
);
|
||||
return ptr;
|
||||
}
|
||||
};
|
@ -77,6 +77,7 @@ pub const Face = struct {
|
||||
pub fn initFont(ct_font: *macos.text.Font, opts: font.face.Options) !Face {
|
||||
var hb_font = try harfbuzz.coretext.createFont(ct_font);
|
||||
errdefer hb_font.destroy();
|
||||
hb_font.setScale(opts.size.pixels(), opts.size.pixels());
|
||||
|
||||
const traits = ct_font.getSymbolicTraits();
|
||||
const metrics = metrics: {
|
||||
|
@ -1,6 +1,7 @@
|
||||
const builtin = @import("builtin");
|
||||
const options = @import("main.zig").options;
|
||||
const harfbuzz = @import("shaper/harfbuzz.zig");
|
||||
const coretext = @import("shaper/coretext.zig");
|
||||
pub const web_canvas = @import("shaper/web_canvas.zig");
|
||||
pub usingnamespace @import("shaper/run.zig");
|
||||
|
||||
@ -12,6 +13,10 @@ pub const Shaper = switch (options.backend) {
|
||||
.coretext,
|
||||
=> harfbuzz.Shaper,
|
||||
|
||||
// Has missing features, can't be used yet. See the comments in
|
||||
// the coretext.zig file for more details.
|
||||
//.coretext => coretext.Shaper,
|
||||
|
||||
.web_canvas => web_canvas.Shaper,
|
||||
};
|
||||
|
||||
@ -24,6 +29,10 @@ pub const Cell = struct {
|
||||
/// caller has access to the original screen cell.
|
||||
x: u16,
|
||||
|
||||
/// An additional offset to apply to the rendering.
|
||||
x_offset: i16 = 0,
|
||||
y_offset: i16 = 0,
|
||||
|
||||
/// The glyph index for this cell. The font index to use alongside
|
||||
/// this cell is available in the text run. This glyph index is only
|
||||
/// valid for a given GroupCache and FontIndex that was used to create
|
||||
|
1026
src/font/shaper/coretext.zig
Normal file
1026
src/font/shaper/coretext.zig
Normal file
File diff suppressed because it is too large
Load Diff
@ -133,18 +133,44 @@ pub const Shaper = struct {
|
||||
// If it isn't true, I'd like to catch it and learn more.
|
||||
assert(info.len == pos.len);
|
||||
|
||||
// This keeps track of the current offsets within a single cell.
|
||||
var cell_offset: struct {
|
||||
cluster: u32 = 0,
|
||||
x: i32 = 0,
|
||||
y: i32 = 0,
|
||||
} = .{};
|
||||
|
||||
// Convert all our info/pos to cells and set it.
|
||||
self.cell_buf.clearRetainingCapacity();
|
||||
try self.cell_buf.ensureTotalCapacity(self.alloc, info.len);
|
||||
for (info) |v| {
|
||||
for (info, pos) |info_v, pos_v| {
|
||||
if (info_v.cluster != cell_offset.cluster) cell_offset = .{
|
||||
.cluster = info_v.cluster,
|
||||
};
|
||||
|
||||
self.cell_buf.appendAssumeCapacity(.{
|
||||
.x = @intCast(v.cluster),
|
||||
.glyph_index = v.codepoint,
|
||||
.x = @intCast(info_v.cluster),
|
||||
.x_offset = @intCast(cell_offset.x),
|
||||
.y_offset = @intCast(cell_offset.y),
|
||||
.glyph_index = info_v.codepoint,
|
||||
});
|
||||
|
||||
// log.warn("i={} info={} pos={} cell={}", .{ i, v, pos[i], self.cell_buf[i] });
|
||||
if (font.options.backend.hasFreetype()) {
|
||||
// Freetype returns 26.6 fixed point values, so we need to
|
||||
// divide by 64 to get the actual value. I can't find any
|
||||
// HB API to stop this.
|
||||
cell_offset.x += pos_v.x_advance >> 6;
|
||||
cell_offset.y += pos_v.y_advance >> 6;
|
||||
} else {
|
||||
cell_offset.x += pos_v.x_advance;
|
||||
cell_offset.y += pos_v.y_advance;
|
||||
}
|
||||
|
||||
// const i = self.cell_buf.items.len - 1;
|
||||
// log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] });
|
||||
}
|
||||
//log.warn("----------------", .{});
|
||||
|
||||
return self.cell_buf.items;
|
||||
}
|
||||
|
||||
|
@ -209,7 +209,10 @@ pub const RunIterator = struct {
|
||||
}
|
||||
|
||||
// Add all the codepoints for our grapheme
|
||||
try self.hooks.addCodepoint(cell.char, @intCast(cluster));
|
||||
try self.hooks.addCodepoint(
|
||||
if (cell.char == 0) ' ' else cell.char,
|
||||
@intCast(cluster),
|
||||
);
|
||||
if (cell.attrs.grapheme) {
|
||||
var it = self.row.codepointIterator(j);
|
||||
while (it.next()) |cp| {
|
||||
|
@ -1787,15 +1787,6 @@ pub fn updateCell(
|
||||
.emoji => .fg_color,
|
||||
};
|
||||
|
||||
// If this glyph doesn't have an advance, then we assume it is
|
||||
// connected to the previous glyph (perhaps an unsafe assumption...)
|
||||
// and offset by the cell width.
|
||||
// Related: https://github.com/mitchellh/ghostty/issues/1046
|
||||
const extra_offset: i32 = if (glyph.advance_x == 0)
|
||||
@intCast(self.grid_metrics.cell_width)
|
||||
else
|
||||
0;
|
||||
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = mode,
|
||||
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
|
||||
@ -1804,7 +1795,10 @@ pub fn updateCell(
|
||||
.bg_color = bg,
|
||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
||||
.glyph_size = .{ glyph.width, glyph.height },
|
||||
.glyph_offset = .{ glyph.offset_x + extra_offset, glyph.offset_y },
|
||||
.glyph_offset = .{
|
||||
glyph.offset_x + shaper_cell.x_offset,
|
||||
glyph.offset_y + shaper_cell.y_offset,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1490,15 +1490,6 @@ pub fn updateCell(
|
||||
.emoji => .fg_color,
|
||||
};
|
||||
|
||||
// If this glyph doesn't have an advance, then we assume it is
|
||||
// connected to the previous glyph (perhaps an unsafe assumption...)
|
||||
// and offset by the cell width.
|
||||
// Related: https://github.com/mitchellh/ghostty/issues/1046
|
||||
const extra_offset: i32 = if (glyph.advance_x == 0)
|
||||
@intCast(self.grid_metrics.cell_width)
|
||||
else
|
||||
0;
|
||||
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(x),
|
||||
@ -1508,8 +1499,8 @@ pub fn updateCell(
|
||||
.glyph_y = glyph.atlas_y,
|
||||
.glyph_width = glyph.width,
|
||||
.glyph_height = glyph.height,
|
||||
.glyph_offset_x = glyph.offset_x + extra_offset,
|
||||
.glyph_offset_y = glyph.offset_y,
|
||||
.glyph_offset_x = glyph.offset_x + shaper_cell.x_offset,
|
||||
.glyph_offset_y = glyph.offset_y + shaper_cell.y_offset,
|
||||
.r = colors.fg.r,
|
||||
.g = colors.fg.g,
|
||||
.b = colors.fg.b,
|
||||
|
Reference in New Issue
Block a user