Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ jobs:
# gpu-components is a TypeScript-core app, so its smoke build needs
# the frontend compiler and exact-pinned TypeScript toolchain.
- run: npm ci --prefix packages/core
# Platform-only framework tests include AppKit pixel regressions that
# are compiled out of the Linux Zig Core lane.
- run: zig build test-desktop-platform
# The mobile aggregate runs on Linux for Android. Exercise the other
# store-capable cross-target here against the real iPhone simulator SDK.
- run: zig build test-example-mobile-canvas-lib-ios-store
Expand Down
9 changes: 7 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ pub fn build(b: *std.Build) void {
else
&.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" };
desktop_mod.addCSourceFile(.{ .file = b.path("src/platform/macos/image_fit_test.m"), .flags = flags });
desktop_mod.addCSourceFile(.{ .file = b.path("src/platform/macos/text_baseline_test.m"), .flags = flags });
desktop_mod.linkFramework("AppKit", .{});
desktop_mod.linkFramework("CoreGraphics", .{});
desktop_mod.linkFramework("CoreText", .{});
desktop_mod.linkFramework("Foundation", .{});
desktop_mod.linkFramework("ImageIO", .{});
desktop_mod.linkSystemLibrary("objc", .{});
Expand Down Expand Up @@ -1348,8 +1352,9 @@ pub fn build(b: *std.Build) void {
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketTextLineBreakMode" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketTextAlignment" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "static BOOL NativeSdkPacketDrawAttributedText(" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "drawGlyphsForGlyphRange:glyphRange atPoint:" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "glyphRangeForTextContainer:container" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "return NativeSdkAppKitDrawAttributedText(value, attributes, x, baseline, width, height, NULL)" },
.{ .path = "src/platform/macos/appkit_text_baseline.h", .pattern = "drawGlyphsForGlyphRange:glyphRange atPoint:" },
.{ .path = "src/platform/macos/appkit_text_baseline.h", .pattern = "glyphRangeForTextContainer:container" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketNumber(layout[@\"maxWidth\"], 0)" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "native_sdk_appkit_measure_text_ink(" },
});
Expand Down
20 changes: 2 additions & 18 deletions src/platform/macos/appkit_host.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "appkit_host.h"
#import "appkit_text_baseline.h"

#import <AppKit/AppKit.h>
#import <AVFoundation/AVFoundation.h>
Expand Down Expand Up @@ -2053,24 +2054,7 @@ static BOOL NativeSdkPacketDrawAttributedText(
CGFloat width,
CGFloat height
) {
if (value.length == 0) return YES;

NSTextStorage *storage = [[NSTextStorage alloc] initWithString:value attributes:attributes];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *container = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(
width > 0 ? width : CGFLOAT_MAX,
height > 0 ? height : CGFLOAT_MAX
)];
container.lineFragmentPadding = 0;
[layoutManager addTextContainer:container];
[storage addLayoutManager:layoutManager];
[layoutManager ensureLayoutForTextContainer:container];

NSRange glyphRange = [layoutManager glyphRangeForTextContainer:container];
if (glyphRange.length == 0) return YES;
CGFloat firstLineOffset = [layoutManager locationForGlyphAtIndex:glyphRange.location].y;
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:NSMakePoint(x, baseline - firstLineOffset)];
return YES;
return NativeSdkAppKitDrawAttributedText(value, attributes, x, baseline, width, height, NULL);
}

// Italicizes a resolved sans face for the reserved italic span font ids
Expand Down
42 changes: 42 additions & 0 deletions src/platform/macos/appkit_text_baseline.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#ifndef NATIVE_SDK_APPKIT_TEXT_BASELINE_H
#define NATIVE_SDK_APPKIT_TEXT_BASELINE_H

#import <AppKit/AppKit.h>

/* Shared by the packet host and its pixel regression. TextKit owns both the
* fallback-face metrics and explicit-line-height placement, so build and draw
* one layout and translate its first baseline to the engine coordinate. The
* optional offset exposes that exact placement to the regression without
* giving the test a second rendering algorithm that can drift from the host. */
static inline BOOL NativeSdkAppKitDrawAttributedText(
NSString *value,
NSDictionary *attributes,
CGFloat x,
CGFloat baseline,
CGFloat width,
CGFloat height,
CGFloat *outFirstLineOffset
) {
if (outFirstLineOffset) *outFirstLineOffset = 0;
if (value.length == 0) return YES;

NSTextStorage *storage = [[NSTextStorage alloc] initWithString:value attributes:attributes];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *container = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(
width > 0 ? width : CGFLOAT_MAX,
height > 0 ? height : CGFLOAT_MAX
)];
container.lineFragmentPadding = 0;
[layoutManager addTextContainer:container];
[storage addLayoutManager:layoutManager];
[layoutManager ensureLayoutForTextContainer:container];

NSRange glyphRange = [layoutManager glyphRangeForTextContainer:container];
if (glyphRange.length == 0) return YES;
CGFloat firstLineOffset = [layoutManager locationForGlyphAtIndex:glyphRange.location].y;
if (outFirstLineOffset) *outFirstLineOffset = firstLineOffset;
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:NSMakePoint(x, baseline - firstLineOffset)];
return YES;
}

#endif
88 changes: 86 additions & 2 deletions src/platform/macos/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,28 @@ extern fn native_sdk_test_imageio_thumbnail_dimensions(
out_height: *usize,
) c_int;

extern fn native_sdk_test_appkit_text_baselines(
regular_bytes: [*]const u8,
regular_len: usize,
mono_bytes: [*]const u8,
mono_len: usize,
size: f64,
baseline: f64,
out_regular_offset: *f64,
out_mono_offset: *f64,
out_compact_regular_offset: *f64,
out_compact_mono_offset: *f64,
out_old_regular_first: *c_int,
out_old_mono_first: *c_int,
out_fixed_regular_first: *c_int,
out_fixed_mono_first: *c_int,
out_wrapped_regular_first: *c_int,
out_wrapped_mono_first: *c_int,
out_compact_regular_first: *c_int,
out_compact_mono_first: *c_int,
out_fallback_first: *c_int,
) c_int;

const shortcut_modifier_primary: u32 = 1 << 0;
const shortcut_modifier_command: u32 = 1 << 1;
const shortcut_modifier_control: u32 = 1 << 2;
Expand Down Expand Up @@ -1264,6 +1286,66 @@ fn decodeImage(context: ?*anyopaque, bytes: []const u8, buffer: []u8, max_pixels
};
}

test "mac packet text keeps mixed-face runs on the engine baseline" {
if (comptime builtin.os.tag != .macos) return error.SkipZigTest;

var regular_offset: f64 = 0;
var mono_offset: f64 = 0;
var compact_regular_offset: f64 = 0;
var compact_mono_offset: f64 = 0;
var old_regular_first: c_int = -1;
var old_mono_first: c_int = -1;
var fixed_regular_first: c_int = -1;
var fixed_mono_first: c_int = -1;
var wrapped_regular_first: c_int = -1;
var wrapped_mono_first: c_int = -1;
var compact_regular_first: c_int = -1;
var compact_mono_first: c_int = -1;
var fallback_first: c_int = -1;
try std.testing.expectEqual(@as(c_int, 1), native_sdk_test_appkit_text_baselines(
canvas.font_ttf.geist_regular_bytes.ptr,
canvas.font_ttf.geist_regular_bytes.len,
canvas.font_ttf.geist_mono_bytes.ptr,
canvas.font_ttf.geist_mono_bytes.len,
14.5,
40,
&regular_offset,
&mono_offset,
&compact_regular_offset,
&compact_mono_offset,
&old_regular_first,
&old_mono_first,
&fixed_regular_first,
&fixed_mono_first,
&wrapped_regular_first,
&wrapped_mono_first,
&compact_regular_first,
&compact_mono_first,
&fallback_first,
));

// The fixtures genuinely exercise different line-fragment metrics,
// and the former point-size conversion visibly split their ink rows.
try std.testing.expectEqual(@as(f64, 13), regular_offset);
try std.testing.expectEqual(@as(f64, 15), mono_offset);
try std.testing.expect(old_regular_first != old_mono_first);

// Explicit line heights smaller than the font box validly place the
// first baseline above TextKit's container origin. Those negative
// offsets must not be replaced by the face-dependent ascent fallback.
try std.testing.expect(compact_regular_offset < 0);
try std.testing.expect(compact_mono_offset < 0);

// Both the engine-measured-line path and the rare host-wrapping fallback
// now place the identical cap outline on one shared engine baseline.
try std.testing.expectEqual(fixed_regular_first, fixed_mono_first);
try std.testing.expectEqual(fixed_regular_first, wrapped_regular_first);
try std.testing.expectEqual(fixed_regular_first, wrapped_mono_first);
try std.testing.expectEqual(fixed_regular_first, compact_regular_first);
try std.testing.expectEqual(fixed_regular_first, compact_mono_first);
try std.testing.expectEqual(fixed_regular_first, fallback_first);
}

test "mac image decoder keeps ImageIO thumbnail rounding inside the pixel cap" {
// The Objective-C probe is compiled and linked only for macOS test
// artifacts (build.zig's target-gated image_fit_test.m source). Keep
Expand Down Expand Up @@ -2901,9 +2983,11 @@ test "mac platform module exports type" {

test "mac AppKit packet text anchors use the resolved font ascent" {
const host_source = @embedFile("appkit_host.m");
const baseline_source = @embedFile("appkit_text_baseline.h");
try std.testing.expect(std.mem.indexOf(u8, host_source, "static BOOL NativeSdkPacketDrawAttributedText(") != null);
try std.testing.expect(std.mem.indexOf(u8, host_source, "drawGlyphsForGlyphRange:glyphRange atPoint:") != null);
try std.testing.expect(std.mem.indexOf(u8, host_source, "glyphRangeForTextContainer:container") != null);
try std.testing.expect(std.mem.indexOf(u8, host_source, "return NativeSdkAppKitDrawAttributedText(value, attributes, x, baseline, width, height, NULL)") != null);
try std.testing.expect(std.mem.indexOf(u8, baseline_source, "drawGlyphsForGlyphRange:glyphRange atPoint:") != null);
try std.testing.expect(std.mem.indexOf(u8, baseline_source, "glyphRangeForTextContainer:container") != null);
try std.testing.expect(std.mem.indexOf(u8, host_source, "native_sdk_appkit_measure_text_ink(") != null);
}

Expand Down
Loading
Loading