Merge pull request #16 from mitchellh/macos

Mac Core Foundation and Core Text Bindings
This commit is contained in:
Mitchell Hashimoto
2022-10-01 16:17:22 -07:00
committed by GitHub
15 changed files with 756 additions and 0 deletions

View File

@ -9,6 +9,7 @@ const harfbuzz = @import("pkg/harfbuzz/build.zig");
const libxml2 = @import("vendor/zig-libxml2/libxml2.zig"); const libxml2 = @import("vendor/zig-libxml2/libxml2.zig");
const libuv = @import("pkg/libuv/build.zig"); const libuv = @import("pkg/libuv/build.zig");
const libpng = @import("pkg/libpng/build.zig"); const libpng = @import("pkg/libpng/build.zig");
const macos = @import("pkg/macos/build.zig");
const utf8proc = @import("pkg/utf8proc/build.zig"); const utf8proc = @import("pkg/utf8proc/build.zig");
const zlib = @import("pkg/zlib/build.zig"); const zlib = @import("pkg/zlib/build.zig");
const tracylib = @import("pkg/tracy/build.zig"); const tracylib = @import("pkg/tracy/build.zig");
@ -184,6 +185,12 @@ fn addDeps(
step.addPackage(libuv.pkg); step.addPackage(libuv.pkg);
step.addPackage(utf8proc.pkg); step.addPackage(utf8proc.pkg);
// Mac Stuff
if (step.target.isDarwin()) {
step.addPackage(macos.pkg);
_ = try macos.link(b, step, .{});
}
// We always statically compile glad // We always statically compile glad
step.addIncludePath("vendor/glad/include/"); step.addIncludePath("vendor/glad/include/");
step.addCSourceFile("vendor/glad/src/gl.c", &.{}); step.addCSourceFile("vendor/glad/src/gl.c", &.{});

25
pkg/macos/build.zig Normal file
View File

@ -0,0 +1,25 @@
const std = @import("std");
const builtin = @import("builtin");
pub const pkg = std.build.Pkg{
.name = "macos",
.source = .{ .path = thisDir() ++ "/main.zig" },
};
fn thisDir() []const u8 {
return std.fs.path.dirname(@src().file) orelse ".";
}
pub const Options = struct {};
pub fn link(
b: *std.build.Builder,
step: *std.build.LibExeObjStep,
opt: Options,
) !*std.build.LibExeObjStep {
_ = opt;
const lib = b.addStaticLibrary("macos", null);
step.linkFramework("CoreFoundation");
step.linkFramework("CoreText");
return lib;
}

12
pkg/macos/foundation.zig Normal file
View File

@ -0,0 +1,12 @@
pub const c = @import("foundation/c.zig");
pub usingnamespace @import("foundation/array.zig");
pub usingnamespace @import("foundation/base.zig");
pub usingnamespace @import("foundation/dictionary.zig");
pub usingnamespace @import("foundation/number.zig");
pub usingnamespace @import("foundation/string.zig");
pub usingnamespace @import("foundation/type.zig");
pub usingnamespace @import("foundation/url.zig");
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -0,0 +1,55 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const cftype = @import("type.zig");
pub const Array = opaque {
pub fn create(comptime T: type, values: []*const T) Allocator.Error!*Array {
return CFArrayCreate(
null,
@ptrCast([*]*const anyopaque, values.ptr),
@intCast(usize, values.len),
null,
) orelse error.OutOfMemory;
}
pub fn release(self: *Array) void {
cftype.CFRelease(self);
}
pub fn getCount(self: *Array) usize {
return CFArrayGetCount(self);
}
/// Note the return type is actually a `*const T` but we strip the
/// constness so that further API calls work correctly. The Foundation
/// API doesn't properly mark things const/non-const.
pub fn getValueAtIndex(self: *Array, comptime T: type, idx: usize) *T {
return @ptrCast(*T, CFArrayGetValueAtIndex(self, idx));
}
pub extern "c" fn CFArrayCreate(
allocator: ?*anyopaque,
values: [*]*const anyopaque,
num_values: usize,
callbacks: ?*const anyopaque,
) ?*Array;
pub extern "c" fn CFArrayGetCount(*Array) usize;
pub extern "c" fn CFArrayGetValueAtIndex(*Array, usize) *anyopaque;
extern "c" var kCFTypeArrayCallBacks: anyopaque;
};
test "array" {
const testing = std.testing;
const str = "hello";
var values = [_]*const u8{ &str[0], &str[1] };
const arr = try Array.create(u8, &values);
defer arr.release();
try testing.expectEqual(@as(usize, 2), arr.getCount());
{
const ch = arr.getValueAtIndex(u8, 0);
try testing.expectEqual(@as(u8, 'h'), ch.*);
}
}

View File

@ -0,0 +1,5 @@
pub const ComparisonResult = enum(c_int) {
less = -1,
equal = 0,
greater = 1,
};

View File

@ -0,0 +1,3 @@
pub usingnamespace @cImport({
@cInclude("CoreFoundation/CoreFoundation.h");
});

View File

@ -0,0 +1,58 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const foundation = @import("../foundation.zig");
const c = @import("c.zig");
pub const Dictionary = opaque {
pub fn create(
keys: ?[]?*const anyopaque,
values: ?[]?*const anyopaque,
) Allocator.Error!*Dictionary {
if (keys != null or values != null) {
assert(keys != null);
assert(values != null);
assert(keys.?.len == values.?.len);
}
return @intToPtr(?*Dictionary, @ptrToInt(c.CFDictionaryCreate(
null,
@ptrCast([*c]?*const anyopaque, if (keys) |slice| slice.ptr else null),
@ptrCast([*c]?*const anyopaque, if (values) |slice| slice.ptr else null),
@intCast(c.CFIndex, if (keys) |slice| slice.len else 0),
&c.kCFTypeDictionaryKeyCallBacks,
&c.kCFTypeDictionaryValueCallBacks,
))) orelse Allocator.Error.OutOfMemory;
}
pub fn release(self: *Dictionary) void {
foundation.CFRelease(self);
}
pub fn getCount(self: *Dictionary) usize {
return @intCast(usize, c.CFDictionaryGetCount(@ptrCast(c.CFDictionaryRef, self)));
}
pub fn getValue(self: *Dictionary, comptime V: type, key: ?*const anyopaque) ?*V {
return @intToPtr(?*V, @ptrToInt(c.CFDictionaryGetValue(
@ptrCast(c.CFDictionaryRef, self),
key,
)));
}
};
test "dictionary" {
const testing = std.testing;
const str = try foundation.String.createWithBytes("hello", .unicode, false);
defer str.release();
var keys = [_]?*const anyopaque{c.kCFURLIsPurgeableKey};
var values = [_]?*const anyopaque{str};
const dict = try Dictionary.create(&keys, &values);
defer dict.release();
try testing.expectEqual(@as(usize, 1), dict.getCount());
try testing.expect(dict.getValue(foundation.String, c.kCFURLIsPurgeableKey) != null);
try testing.expect(dict.getValue(foundation.String, c.kCFURLIsVolumeKey) == null);
}

View File

@ -0,0 +1,79 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const foundation = @import("../foundation.zig");
const c = @import("c.zig");
pub const Number = opaque {
pub fn create(
comptime type_: NumberType,
value: *const type_.ValueType(),
) Allocator.Error!*Number {
return @intToPtr(?*Number, @ptrToInt(c.CFNumberCreate(
null,
@enumToInt(type_),
value,
))) orelse Allocator.Error.OutOfMemory;
}
pub fn getValue(self: *Number, comptime t: NumberType, ptr: *t.ValueType()) bool {
return c.CFNumberGetValue(
@ptrCast(c.CFNumberRef, self),
@enumToInt(t),
ptr,
) == 1;
}
pub fn release(self: *Number) void {
c.CFRelease(self);
}
};
pub const NumberType = enum(c.CFNumberType) {
sint8 = c.kCFNumberSInt8Type,
sint16 = c.kCFNumberSInt16Type,
sint32 = c.kCFNumberSInt32Type,
sint64 = c.kCFNumberSInt64Type,
float32 = c.kCFNumberFloat32Type,
float64 = c.kCFNumberFloat64Type,
char = c.kCFNumberCharType,
short = c.kCFNumberShortType,
int = c.kCFNumberIntType,
long = c.kCFNumberLongType,
long_long = c.kCFNumberLongLongType,
float = c.kCFNumberFloatType,
double = c.kCFNumberDoubleType,
cf_index = c.kCFNumberCFIndexType,
ns_integer = c.kCFNumberNSIntegerType,
cg_float = c.kCFNumberCGFloatType,
pub fn ValueType(self: NumberType) type {
return switch (self) {
.sint8 => i8,
.sint16 => i16,
.sint32 => i32,
.sint64 => i64,
.float32 => f32,
.float64 => f64,
.char => u8,
.short => c_short,
.int => c_int,
.long => c_long,
.long_long => c_longlong,
.float => f32,
.double => f64,
else => unreachable, // TODO
};
}
};
test {
const testing = std.testing;
const inner: i8 = 42;
const v = try Number.create(.sint8, &inner);
defer v.release();
var result: i8 = undefined;
try testing.expect(v.getValue(.sint8, &result));
try testing.expectEqual(result, inner);
}

View File

@ -0,0 +1,121 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const foundation = @import("../foundation.zig");
const c = @import("c.zig");
pub const String = opaque {
pub fn createWithBytes(
bs: []const u8,
encoding: StringEncoding,
external: bool,
) Allocator.Error!*String {
return @intToPtr(?*String, @ptrToInt(c.CFStringCreateWithBytes(
null,
bs.ptr,
@intCast(c_long, bs.len),
@enumToInt(encoding),
@boolToInt(external),
))) orelse Allocator.Error.OutOfMemory;
}
pub fn release(self: *String) void {
c.CFRelease(self);
}
pub fn hasPrefix(self: *String, prefix: *String) bool {
return c.CFStringHasPrefix(
@ptrCast(c.CFStringRef, self),
@ptrCast(c.CFStringRef, prefix),
) == 1;
}
pub fn compare(
self: *String,
other: *String,
options: StringComparison,
) foundation.ComparisonResult {
return @intToEnum(
foundation.ComparisonResult,
c.CFStringCompare(
@ptrCast(c.CFStringRef, self),
@ptrCast(c.CFStringRef, other),
@intCast(c_ulong, @bitCast(c_int, options)),
),
);
}
pub fn cstring(self: *String, buf: []u8, encoding: StringEncoding) ?[]const u8 {
if (c.CFStringGetCString(
@ptrCast(c.CFStringRef, self),
buf.ptr,
@intCast(c_long, buf.len),
@enumToInt(encoding),
) == 0) return null;
return std.mem.sliceTo(buf, 0);
}
pub fn cstringPtr(self: *String, encoding: StringEncoding) ?[:0]const u8 {
const ptr = c.CFStringGetCStringPtr(
@ptrCast(c.CFStringRef, self),
@enumToInt(encoding),
);
if (ptr == null) return null;
return std.mem.sliceTo(ptr, 0);
}
};
pub const StringComparison = packed struct {
case_insensitive: bool = false,
_unused_2: bool = false,
backwards: bool = false,
anchored: bool = false,
nonliteral: bool = false,
localized: bool = false,
numerically: bool = false,
diacritic_insensitive: bool = false,
width_insensitive: bool = false,
forced_ordering: bool = false,
_padding: u22 = 0,
test {
try std.testing.expectEqual(@bitSizeOf(c_int), @bitSizeOf(StringComparison));
}
};
/// https://developer.apple.com/documentation/corefoundation/cfstringencoding?language=objc
pub const StringEncoding = enum(u32) {
invalid = 0xffffffff,
mac_roman = 0,
windows_latin1 = 0x0500,
iso_latin1 = 0x0201,
nextstep_latin = 0x0B01,
ascii = 0x0600,
unicode = 0x0100,
utf8 = 0x08000100,
non_lossy_ascii = 0x0BFF,
utf16_be = 0x10000100,
utf16_le = 0x14000100,
utf32 = 0x0c000100,
utf32_be = 0x18000100,
utf32_le = 0x1c000100,
};
test "string" {
const testing = std.testing;
const str = try String.createWithBytes("hello world", .ascii, false);
defer str.release();
const prefix = try String.createWithBytes("hello", .ascii, false);
defer prefix.release();
try testing.expect(str.hasPrefix(prefix));
try testing.expectEqual(foundation.ComparisonResult.equal, str.compare(str, .{}));
try testing.expectEqualStrings("hello world", str.cstringPtr(.ascii).?);
{
var buf: [128]u8 = undefined;
const cstr = str.cstring(&buf, .ascii).?;
try testing.expectEqualStrings("hello world", cstr);
}
}

View File

@ -0,0 +1 @@
pub extern "c" fn CFRelease(*anyopaque) void;

View File

@ -0,0 +1,63 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const foundation = @import("../foundation.zig");
pub const URL = opaque {
pub fn createWithString(str: *foundation.String, base: ?*URL) Allocator.Error!*URL {
return CFURLCreateWithString(
null,
str,
base,
) orelse error.OutOfMemory;
}
pub fn createStringByReplacingPercentEscapes(
str: *foundation.String,
escape: *foundation.String,
) Allocator.Error!*foundation.String {
return CFURLCreateStringByReplacingPercentEscapes(
null,
str,
escape,
) orelse return error.OutOfMemory;
}
pub fn release(self: *URL) void {
foundation.CFRelease(self);
}
pub fn copyPath(self: *URL) ?*foundation.String {
return CFURLCopyPath(self);
}
pub extern "c" fn CFURLCreateWithString(
allocator: ?*anyopaque,
url_string: *const anyopaque,
base_url: ?*const anyopaque,
) ?*URL;
pub extern "c" fn CFURLCopyPath(*URL) ?*foundation.String;
pub extern "c" fn CFURLCreateStringByReplacingPercentEscapes(
allocator: ?*anyopaque,
original: *const anyopaque,
escape: *const anyopaque,
) ?*foundation.String;
};
test {
const testing = std.testing;
const str = try foundation.String.createWithBytes("http://www.example.com/foo", .utf8, false);
defer str.release();
const url = try URL.createWithString(str, null);
defer url.release();
{
const path = url.copyPath().?;
defer path.release();
var buf: [128]u8 = undefined;
const cstr = path.cstring(&buf, .utf8).?;
try testing.expectEqualStrings("/foo", cstr);
}
}

6
pkg/macos/main.zig Normal file
View File

@ -0,0 +1,6 @@
pub const foundation = @import("foundation.zig");
pub const text = @import("text.zig");
test {
@import("std").testing.refAllDecls(@This());
}

6
pkg/macos/text.zig Normal file
View File

@ -0,0 +1,6 @@
pub usingnamespace @import("text/font_collection.zig");
pub usingnamespace @import("text/font_descriptor.zig");
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -0,0 +1,110 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const foundation = @import("../foundation.zig");
const text = @import("../text.zig");
const c = @import("c.zig");
pub const FontCollection = opaque {
pub fn createFromAvailableFonts() Allocator.Error!*FontCollection {
return @intToPtr(
?*FontCollection,
@ptrToInt(c.CTFontCollectionCreateFromAvailableFonts(null)),
) orelse Allocator.Error.OutOfMemory;
}
pub fn createWithFontDescriptors(descs: *foundation.Array) Allocator.Error!*FontCollection {
return @intToPtr(
?*FontCollection,
@ptrToInt(c.CTFontCollectionCreateWithFontDescriptors(
@ptrCast(c.CFArrayRef, descs),
null,
)),
) orelse Allocator.Error.OutOfMemory;
}
pub fn release(self: *FontCollection) void {
c.CFRelease(self);
}
pub fn createMatchingFontDescriptors(self: *FontCollection) *foundation.Array {
return @intToPtr(
*foundation.Array,
@ptrToInt(c.CTFontCollectionCreateMatchingFontDescriptors(
@ptrCast(c.CTFontCollectionRef, self),
)),
);
}
};
fn debugDumpList(list: *foundation.Array) !void {
var i: usize = 0;
while (i < list.getCount()) : (i += 1) {
const desc = list.getValueAtIndex(text.FontDescriptor, i);
{
var buf: [128]u8 = undefined;
const name = desc.copyAttribute(.name);
defer name.release();
const cstr = name.cstring(&buf, .utf8).?;
var buf2: [128]u8 = undefined;
const url = desc.copyAttribute(.url);
defer url.release();
const path = path: {
const blank = try foundation.String.createWithBytes("", .utf8, false);
defer blank.release();
const path = url.copyPath() orelse break :path "<no path>";
defer path.release();
const decoded = try foundation.URL.createStringByReplacingPercentEscapes(
path,
blank,
);
defer decoded.release();
break :path decoded.cstring(&buf2, .utf8) orelse
"<path cannot be converted to string>";
};
std.log.warn("i={d} name={s} path={s}", .{ i, cstr, path });
}
}
}
test "collection" {
const testing = std.testing;
const v = try FontCollection.createFromAvailableFonts();
defer v.release();
const list = v.createMatchingFontDescriptors();
defer list.release();
try testing.expect(list.getCount() > 0);
}
test "from descriptors" {
const testing = std.testing;
const name = try foundation.String.createWithBytes("AppleColorEmoji", .utf8, false);
defer name.release();
const desc = try text.FontDescriptor.createWithNameAndSize(name, 12);
defer desc.release();
const arr = try foundation.Array.create(
text.FontDescriptor,
&[_]*const text.FontDescriptor{desc},
);
defer arr.release();
const v = try FontCollection.createWithFontDescriptors(arr);
defer v.release();
const list = v.createMatchingFontDescriptors();
defer list.release();
try testing.expect(list.getCount() > 0);
//try debugDumpList(list);
}

View File

@ -0,0 +1,205 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const foundation = @import("../foundation.zig");
const c = @import("c.zig");
pub const FontDescriptor = opaque {
pub fn createWithNameAndSize(name: *foundation.String, size: f64) Allocator.Error!*FontDescriptor {
return @intToPtr(
?*FontDescriptor,
@ptrToInt(c.CTFontDescriptorCreateWithNameAndSize(@ptrCast(c.CFStringRef, name), size)),
) orelse Allocator.Error.OutOfMemory;
}
pub fn release(self: *FontDescriptor) void {
c.CFRelease(self);
}
pub fn copyAttribute(self: *FontDescriptor, comptime attr: FontAttribute) attr.Value() {
return @intToPtr(attr.Value(), @ptrToInt(c.CTFontDescriptorCopyAttribute(
@ptrCast(c.CTFontDescriptorRef, self),
@ptrCast(c.CFStringRef, attr.key()),
)));
}
};
pub const FontAttribute = enum {
url,
name,
display_name,
family_name,
style_name,
traits,
variation,
size,
matrix,
cascade_list,
character_set,
languages,
baseline_adjust,
macintosh_encodings,
features,
feature_settings,
fixed_advance,
orientation,
format,
registration_scope,
priority,
enabled,
downloadable,
downloaded,
pub fn key(self: FontAttribute) *foundation.String {
return @intToPtr(*foundation.String, @ptrToInt(switch (self) {
.url => c.kCTFontURLAttribute,
.name => c.kCTFontNameAttribute,
.display_name => c.kCTFontDisplayNameAttribute,
.family_name => c.kCTFontFamilyNameAttribute,
.style_name => c.kCTFontStyleNameAttribute,
.traits => c.kCTFontTraitsAttribute,
.variation => c.kCTFontVariationAttribute,
.size => c.kCTFontSizeAttribute,
.matrix => c.kCTFontMatrixAttribute,
.cascade_list => c.kCTFontCascadeListAttribute,
.character_set => c.kCTFontCharacterSetAttribute,
.languages => c.kCTFontLanguagesAttribute,
.baseline_adjust => c.kCTFontBaselineAdjustAttribute,
.macintosh_encodings => c.kCTFontMacintoshEncodingsAttribute,
.features => c.kCTFontFeaturesAttribute,
.feature_settings => c.kCTFontFeatureSettingsAttribute,
.fixed_advance => c.kCTFontFixedAdvanceAttribute,
.orientation => c.kCTFontOrientationAttribute,
.format => c.kCTFontFormatAttribute,
.registration_scope => c.kCTFontRegistrationScopeAttribute,
.priority => c.kCTFontPriorityAttribute,
.enabled => c.kCTFontEnabledAttribute,
.downloadable => c.kCTFontDownloadableAttribute,
.downloaded => c.kCTFontDownloadedAttribute,
}));
}
pub fn Value(self: FontAttribute) type {
return switch (self) {
.url => *foundation.URL,
.name => *foundation.String,
.display_name => *foundation.String,
.family_name => *foundation.String,
.style_name => *foundation.String,
.traits => *foundation.Dictionary,
.variation => *foundation.Dictionary,
.size => *foundation.Number,
.matrix => *anyopaque, // CFDataRef
.cascade_list => *foundation.Array,
.character_set => *anyopaque, // CFCharacterSetRef
.languages => *foundation.Array,
.baseline_adjust => *foundation.Number,
.macintosh_encodings => *foundation.Number,
.features => *foundation.Array,
.feature_settings => *foundation.Array,
.fixed_advance => *foundation.Number,
.orientation => *foundation.Number,
.format => *foundation.Number,
.registration_scope => *foundation.Number,
.priority => *foundation.Number,
.enabled => *foundation.Number,
.downloadable => *anyopaque, // CFBoolean
.downloaded => *anyopaque, // CFBoolean
};
}
};
pub const FontTraitKey = enum {
symbolic,
weight,
width,
slant,
pub fn key(self: FontTraitKey) *foundation.String {
return @intToPtr(*foundation.String, @ptrToInt(switch (self) {
.symbolic => c.kCTFontSymbolicTrait,
.weight => c.kCTFontWeightTrait,
.width => c.kCTFontWidthTrait,
.slant => c.kCTFontFontSlantTrait,
}));
}
pub fn Value(self: FontTraitKey) type {
return switch (self) {
.symbolic => *foundation.Number,
.weight => *foundation.Number,
.width => *foundation.Number,
.slant => *foundation.Number,
};
}
};
pub const FontSymbolicTraits = packed struct {
italic: bool = false,
bold: bool = false,
_unused1: u3 = 0,
expanded: bool = false,
condensed: bool = false,
_unused2: u3 = 0,
monospace: bool = false,
vertical: bool = false,
ui_optimized: bool = false,
color_glyphs: bool = false,
composite: bool = false,
_padding: u17 = 0,
pub fn init(num: *foundation.Number) FontSymbolicTraits {
var raw: i32 = undefined;
_ = num.getValue(.sint32, &raw);
return @bitCast(FontSymbolicTraits, raw);
}
test {
try std.testing.expectEqual(
@bitSizeOf(c.CTFontSymbolicTraits),
@bitSizeOf(FontSymbolicTraits),
);
}
test "bitcast" {
const actual: c.CTFontSymbolicTraits = c.kCTFontTraitMonoSpace | c.kCTFontTraitExpanded;
const expected: FontSymbolicTraits = .{
.monospace = true,
.expanded = true,
};
try std.testing.expectEqual(actual, @bitCast(c.CTFontSymbolicTraits, expected));
}
test "number" {
const raw: i32 = c.kCTFontTraitMonoSpace | c.kCTFontTraitExpanded;
const num = try foundation.Number.create(.sint32, &raw);
defer num.release();
const expected: FontSymbolicTraits = .{ .monospace = true, .expanded = true };
const actual = FontSymbolicTraits.init(num);
try std.testing.expect(std.meta.eql(expected, actual));
}
};
test {
@import("std").testing.refAllDecls(@This());
}
test "descriptor" {
const testing = std.testing;
const name = try foundation.String.createWithBytes("foo", .utf8, false);
defer name.release();
const v = try FontDescriptor.createWithNameAndSize(name, 12);
defer v.release();
const copy_name = v.copyAttribute(.name);
defer copy_name.release();
{
var buf: [128]u8 = undefined;
const cstr = copy_name.cstring(&buf, .utf8).?;
try testing.expectEqualStrings("foo", cstr);
}
}