diff --git a/pkg/macos/foundation/string.zig b/pkg/macos/foundation/string.zig index ce06fa410..164b139b0 100644 --- a/pkg/macos/foundation/string.zig +++ b/pkg/macos/foundation/string.zig @@ -23,6 +23,10 @@ pub const String = opaque { c.CFRelease(self); } + pub fn getLength(self: *String) usize { + return @intCast(usize, c.CFStringGetLength(@ptrCast(c.CFStringRef, self))); + } + pub fn hasPrefix(self: *String, prefix: *String) bool { return c.CFStringHasPrefix( @ptrCast(c.CFStringRef, self), diff --git a/pkg/macos/graphics/geometry.zig b/pkg/macos/graphics/geometry.zig index bd3caf0db..ca67a7130 100644 --- a/pkg/macos/graphics/geometry.zig +++ b/pkg/macos/graphics/geometry.zig @@ -20,6 +20,10 @@ pub const Rect = extern struct { pub fn cval(self: Rect) c.struct_CGRect { return @bitCast(c.struct_CGRect, self); } + + pub fn isNull(self: Rect) bool { + return c.CGRectIsNull(self.cval()); + } }; pub const Size = extern struct { diff --git a/pkg/macos/text.zig b/pkg/macos/text.zig index ee6018312..58bfaa632 100644 --- a/pkg/macos/text.zig +++ b/pkg/macos/text.zig @@ -4,6 +4,7 @@ pub usingnamespace @import("text/font_descriptor.zig"); 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/stylized_strings.zig"); test { diff --git a/pkg/macos/text/line.zig b/pkg/macos/text/line.zig new file mode 100644 index 000000000..014d64cb9 --- /dev/null +++ b/pkg/macos/text/line.zig @@ -0,0 +1,123 @@ +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 Line = opaque { + pub fn createWithAttributedString(str: *foundation.AttributedString) Allocator.Error!*Line { + return @intToPtr( + ?*Line, + @ptrToInt(c.CTLineCreateWithAttributedString( + @ptrCast(c.CFAttributedStringRef, str), + )), + ) orelse Allocator.Error.OutOfMemory; + } + + pub fn release(self: *Line) void { + foundation.CFRelease(self); + } + + pub fn getGlyphCount(self: *Line) usize { + return @intCast(usize, c.CTLineGetGlyphCount( + @ptrCast(c.CTLineRef, self), + )); + } + + pub fn getBoundsWithOptions( + self: *Line, + opts: LineBoundsOptions, + ) graphics.Rect { + return @bitCast(c.CGRect, c.CTLineGetBoundsWithOptions( + @ptrCast(c.CTLineRef, self), + opts.cval(), + )); + } + + pub fn getTypographicBounds( + self: *Line, + ascent: ?*f64, + descent: ?*f64, + leading: ?*f64, + ) f64 { + return c.CTLineGetTypographicBounds( + @ptrCast(c.CTLineRef, self), + ascent, + descent, + leading, + ); + } +}; + +pub const LineBoundsOptions = packed struct { + exclude_leading: bool = false, + exclude_shifts: bool = false, + hanging_punctuation: bool = false, + glyph_path_bounds: bool = false, + use_optical_bounds: bool = false, + language_extents: bool = false, + _padding: u58 = 0, + + pub fn cval(self: LineBoundsOptions) c.CTLineBoundsOptions { + return @bitCast(c.CTLineBoundsOptions, self); + } + + test { + try std.testing.expectEqual( + @bitSizeOf(c.CTLineBoundsOptions), + @bitSizeOf(LineBoundsOptions), + ); + } + + test "bitcast" { + const actual: c.CTLineBoundsOptions = c.kCTLineBoundsExcludeTypographicShifts | + c.kCTLineBoundsUseOpticalBounds; + const expected: LineBoundsOptions = .{ + .exclude_shifts = true, + .use_optical_bounds = true, + }; + + try std.testing.expectEqual(actual, @bitCast(c.CTLineBoundsOptions, expected)); + } +}; + +test { + @import("std").testing.refAllDecls(@This()); +} + +test "line" { + const testing = std.testing; + + const font = font: { + const name = try foundation.String.createWithBytes("Monaco", .utf8, false); + defer name.release(); + const desc = try text.FontDescriptor.createWithNameAndSize(name, 12); + defer desc.release(); + + break :font try text.Font.createWithFontDescriptor(desc, 12); + }; + defer font.release(); + + const rep = try foundation.String.createWithBytes("hello", .utf8, false); + defer rep.release(); + const str = try foundation.MutableAttributedString.create(rep.getLength()); + defer str.release(); + str.replaceString(foundation.Range.init(0, 0), rep); + str.setAttribute( + foundation.Range.init(0, rep.getLength()), + text.StringAttribute.font, + font, + ); + + const line = try Line.createWithAttributedString(@ptrCast(*foundation.AttributedString, str)); + defer line.release(); + + try testing.expectEqual(@as(usize, 5), line.getGlyphCount()); + + // TODO: this is a garbage value but should work... + const bounds = line.getBoundsWithOptions(.{}); + _ = bounds; + //std.log.warn("bounds={}", .{bounds}); +} diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 7421c796f..083a34426 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -122,7 +122,10 @@ pub const Face = struct { // 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 cell_height: f32 = cell_height: { + 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 @@ -156,24 +159,55 @@ pub const Face = struct { ); defer frame.release(); - // Get the two points where the lines start in order to determine - // the line height. + // 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..]); - break :cell_height @floatCast(f32, points[0].y - points[1].y); + 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), + }; }; - std.log.warn("width={}, height={}", .{ cell_width, cell_height }); + // 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 = 0, - .underline_position = 0, - .underline_thickness = 0, - .strikethrough_position = 0, - .strikethrough_thickness = 0, + .cell_baseline = cell_baseline, + .underline_position = underline_position, + .underline_thickness = underline_thickness, + .strikethrough_position = strikethrough_position, + .strikethrough_thickness = strikethrough_thickness, }; } }; @@ -188,7 +222,7 @@ test { const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); defer ct_font.release(); - var face = try Face.initFontCopy(ct_font, .{ .points = 18 }); + var face = try Face.initFontCopy(ct_font, .{ .points = 12 }); defer face.deinit(); try testing.expectEqual(font.Presentation.text, face.presentation);