diff --git a/build.zig b/build.zig index fbd4a9a76..f227ee657 100644 --- a/build.zig +++ b/build.zig @@ -9,6 +9,7 @@ const harfbuzz = @import("pkg/harfbuzz/build.zig"); const libxml2 = @import("vendor/zig-libxml2/libxml2.zig"); const libuv = @import("pkg/libuv/build.zig"); const libpng = @import("pkg/libpng/build.zig"); +const macos = @import("pkg/macos/build.zig"); const utf8proc = @import("pkg/utf8proc/build.zig"); const zlib = @import("pkg/zlib/build.zig"); const tracylib = @import("pkg/tracy/build.zig"); @@ -184,6 +185,12 @@ fn addDeps( step.addPackage(libuv.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 step.addIncludePath("vendor/glad/include/"); step.addCSourceFile("vendor/glad/src/gl.c", &.{}); diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig new file mode 100644 index 000000000..78df21ee8 --- /dev/null +++ b/pkg/macos/build.zig @@ -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; +} diff --git a/pkg/macos/foundation.zig b/pkg/macos/foundation.zig new file mode 100644 index 000000000..8b29ccfb3 --- /dev/null +++ b/pkg/macos/foundation.zig @@ -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()); +} diff --git a/pkg/macos/foundation/array.zig b/pkg/macos/foundation/array.zig new file mode 100644 index 000000000..9ab5fa293 --- /dev/null +++ b/pkg/macos/foundation/array.zig @@ -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.*); + } +} diff --git a/pkg/macos/foundation/base.zig b/pkg/macos/foundation/base.zig new file mode 100644 index 000000000..b48d6df38 --- /dev/null +++ b/pkg/macos/foundation/base.zig @@ -0,0 +1,5 @@ +pub const ComparisonResult = enum(c_int) { + less = -1, + equal = 0, + greater = 1, +}; diff --git a/pkg/macos/foundation/c.zig b/pkg/macos/foundation/c.zig new file mode 100644 index 000000000..9bd571adb --- /dev/null +++ b/pkg/macos/foundation/c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("CoreFoundation/CoreFoundation.h"); +}); diff --git a/pkg/macos/foundation/dictionary.zig b/pkg/macos/foundation/dictionary.zig new file mode 100644 index 000000000..8d74a90f1 --- /dev/null +++ b/pkg/macos/foundation/dictionary.zig @@ -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); +} diff --git a/pkg/macos/foundation/number.zig b/pkg/macos/foundation/number.zig new file mode 100644 index 000000000..289c9e8f1 --- /dev/null +++ b/pkg/macos/foundation/number.zig @@ -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); +} diff --git a/pkg/macos/foundation/string.zig b/pkg/macos/foundation/string.zig new file mode 100644 index 000000000..fb322c8b3 --- /dev/null +++ b/pkg/macos/foundation/string.zig @@ -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); + } +} diff --git a/pkg/macos/foundation/type.zig b/pkg/macos/foundation/type.zig new file mode 100644 index 000000000..e3ee150f2 --- /dev/null +++ b/pkg/macos/foundation/type.zig @@ -0,0 +1 @@ +pub extern "c" fn CFRelease(*anyopaque) void; diff --git a/pkg/macos/foundation/url.zig b/pkg/macos/foundation/url.zig new file mode 100644 index 000000000..f0d71e26a --- /dev/null +++ b/pkg/macos/foundation/url.zig @@ -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); + } +} diff --git a/pkg/macos/main.zig b/pkg/macos/main.zig new file mode 100644 index 000000000..d938a633d --- /dev/null +++ b/pkg/macos/main.zig @@ -0,0 +1,6 @@ +pub const foundation = @import("foundation.zig"); +pub const text = @import("text.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/pkg/macos/text.zig b/pkg/macos/text.zig new file mode 100644 index 000000000..4954e0afc --- /dev/null +++ b/pkg/macos/text.zig @@ -0,0 +1,6 @@ +pub usingnamespace @import("text/font_collection.zig"); +pub usingnamespace @import("text/font_descriptor.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/pkg/macos/text/font_collection.zig b/pkg/macos/text/font_collection.zig new file mode 100644 index 000000000..686db2be2 --- /dev/null +++ b/pkg/macos/text/font_collection.zig @@ -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 ""; + defer path.release(); + + const decoded = try foundation.URL.createStringByReplacingPercentEscapes( + path, + blank, + ); + defer decoded.release(); + + break :path decoded.cstring(&buf2, .utf8) orelse + ""; + }; + + 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); +} diff --git a/pkg/macos/text/font_descriptor.zig b/pkg/macos/text/font_descriptor.zig new file mode 100644 index 000000000..4de03a259 --- /dev/null +++ b/pkg/macos/text/font_descriptor.zig @@ -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); + } +}