From b858aea1241206fa2d2c510a0ef13017a31d7b6d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Nov 2022 15:05:09 -0800 Subject: [PATCH] Start scaffolding web_canvas backend --- example/index.html | 19 +++++++------ src/font/Atlas.zig | 54 +++++++++++++++++++++++------------- src/font/DeferredFace.zig | 11 ++++++++ src/font/face.zig | 2 ++ src/font/face/web_canvas.zig | 22 +++++++++++++++ src/font/main.zig | 40 ++++++++++++++++++++------ src/main_wasm.zig | 4 +++ 7 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 src/font/face/web_canvas.zig diff --git a/example/index.html b/example/index.html index f93da37d7..314a78cb1 100644 --- a/example/index.html +++ b/example/index.html @@ -13,21 +13,24 @@ } }; - fetch('ghostty-term.wasm').then(response => + fetch('ghostty-wasm.wasm').then(response => response.arrayBuffer() ).then(bytes => WebAssembly.instantiate(bytes, importObject) ).then(results => { const { - terminal_new, - terminal_free, - terminal_print, + atlas_new, + atlas_free, + atlas_reserve, + free, + memory, } = results.instance.exports; - const term = terminal_new(80, 80); - console.log(term); - terminal_free(term); - terminal_print('a'); + const atlas = atlas_new(512, 0); + const reg = atlas_reserve(atlas, 10, 10); + console.log(reg); + free(reg); + atlas_free(atlas); }); diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 9bce5e7cb..4259d5dda 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -49,10 +49,10 @@ modified: bool = false, /// updated in-place. resized: bool = false, -pub const Format = enum { - greyscale, - rgb, - rgba, +pub const Format = enum(u8) { + greyscale = 0, + rgb = 1, + rgba = 2, pub fn depth(self: Format) u8 { return switch (self) { @@ -310,33 +310,33 @@ pub const Wasm = struct { const wasm = @import("../wasm.zig"); const alloc = wasm.alloc; - const FormatInt = @typeInfo(Format).Enum.tag_type; - export const ATLAS_FORMAT_GREYSCALE: u8 = @enumToInt(Format.greyscale); - export const ATLAS_FORMAT_RGB: u8 = @enumToInt(Format.rgb); - export const ATLAS_FORMAT_RGBA: u8 = @enumToInt(Format.rgba); - export fn atlas_new(size: u32, format: u8) ?*Atlas { const atlas = init( alloc, size, - @intToEnum(Format, @intCast(FormatInt, format)), + @intToEnum(Format, format), ) catch return null; const result = alloc.create(Atlas) catch return null; result.* = atlas; return result; } - export fn atlas_reserve(self: *Atlas, width: u32, height: u32) Region { - return self.reserve(alloc, width, height) catch .{ - .x = 0, - .y = 0, - .width = 0, - .height = 0, - }; + /// The return value for this should be freed by the caller with "free". + export fn atlas_reserve(self: *Atlas, width: u32, height: u32) ?*Region { + return atlas_reserve_(self, width, height) catch return null; } - export fn atlas_set(self: *Atlas, reg: Region, data: [*]const u8, len: usize) void { - self.set(reg, data[0..len]); + fn atlas_reserve_(self: *Atlas, width: u32, height: u32) !*Region { + const reg = try self.reserve(alloc, width, height); + const result = try alloc.create(Region); + errdefer alloc.destroy(result); + _ = try wasm.toHostOwned(result); + result.* = reg; + return result; + } + + export fn atlas_set(self: *Atlas, reg: *Region, data: [*]const u8, len: usize) void { + self.set(reg.*, data[0..len]); } export fn atlas_grow(self: *Atlas, size_new: u32) bool { @@ -354,6 +354,22 @@ pub const Wasm = struct { alloc.destroy(v); } } + + test "happy path" { + var atlas = atlas_new(512, @enumToInt(Format.greyscale)).?; + defer atlas_free(atlas); + + const reg = atlas_reserve(atlas, 2, 2).?; + defer alloc.destroy(reg); + try testing.expect(wasm.isHostOwned(reg)); + defer wasm.toModuleOwned(reg); + try testing.expect(reg.width > 0); + + const data = &[_]u8{ 1, 2, 3, 4 }; + try testing.expect(!atlas.modified); + atlas_set(atlas, reg, data, data.len); + try testing.expect(atlas.modified); + } }; test "exact fit" { diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 64a9f6e94..9ae816fa2 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -68,6 +68,8 @@ pub fn deinit(self: *DeferredFace) void { .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), .coretext => if (self.ct) |*ct| ct.deinit(), .freetype => {}, + // TODO + .web_canvas => unreachable, } self.* = undefined; } @@ -90,6 +92,9 @@ pub fn name(self: DeferredFace) ![:0]const u8 { }, .freetype => {}, + + // TODO + .web_canvas => unreachable, } return "TODO: built-in font names"; @@ -122,6 +127,9 @@ pub fn load( return; }, + // TODO + .web_canvas => unreachable, + // Unreachable because we must be already loaded or have the // proper configuration for one of the other deferred mechanisms. .freetype => unreachable, @@ -245,6 +253,9 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { } }, + // TODO + .web_canvas => unreachable, + .freetype => {}, } diff --git a/src/font/face.zig b/src/font/face.zig index d4d4ba1d2..3986d7aa9 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -2,12 +2,14 @@ const builtin = @import("builtin"); const options = @import("main.zig").options; const freetype = @import("face/freetype.zig"); const coretext = @import("face/coretext.zig"); +const web_canvas = @import("face/web_canvas.zig"); /// Face implementation for the compile options. pub const Face = switch (options.backend) { .fontconfig_freetype => freetype.Face, .coretext => freetype.Face, //.coretext => coretext.Face, + .web_canvas => web_canvas.Face, else => unreachable, }; diff --git a/src/font/face/web_canvas.zig b/src/font/face/web_canvas.zig new file mode 100644 index 000000000..a09e2e105 --- /dev/null +++ b/src/font/face/web_canvas.zig @@ -0,0 +1,22 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const freetype = @import("freetype"); +const assert = std.debug.assert; +const testing = std.testing; +const Allocator = std.mem.Allocator; +const font = @import("../main.zig"); +const Glyph = font.Glyph; +const Library = font.Library; +const Presentation = font.Presentation; +const convert = @import("freetype_convert.zig"); + +const log = std.log.scoped(.font_face); + +pub const Face = struct { + /// The presentation for this font. This is a heuristic since fonts don't have + /// a way to declare this. We just assume a font with color is an emoji font. + presentation: Presentation, + + /// Metrics for this font face. These are useful for renderers. + metrics: font.face.Metrics, +}; diff --git a/src/font/main.zig b/src/font/main.zig index 7f93c4299..4226f732c 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -25,12 +25,7 @@ pub usingnamespace if (builtin.target.isWasm()) struct { pub const options: struct { backend: Backend, } = .{ - .backend = if (build_options.coretext) - .coretext - else if (build_options.fontconfig) - .fontconfig_freetype - else - .freetype, + .backend = Backend.default(), }; pub const Backend = enum { @@ -43,14 +38,37 @@ pub const Backend = enum { /// CoreText for both font discovery and rendering (macOS). coretext, + /// Use the browser font system and the Canvas API (wasm). This limits + /// the available fonts to browser fonts (anything Canvas natively + /// supports). + web_canvas, + + /// Returns the default backend for a build environment. This is + /// meant to be called at comptime. + pub fn default() Backend { + // Wasm only supports browser at the moment. + if (builtin.target.isWasm()) return .web_canvas; + + return if (build_options.coretext) + .coretext + else if (build_options.fontconfig) + .fontconfig_freetype + else + .freetype; + } + /// Helper that just returns true if we should be using freetype. This /// is used for tests. pub fn freetype(self: Backend) bool { return switch (self) { .freetype, .fontconfig_freetype => true, - .coretext => false, + .coretext, .web_canvas => false, }; } + + test "default can run at comptime" { + _ = comptime default(); + } }; /// The styles that a family can take. @@ -71,5 +89,11 @@ pub const Presentation = enum(u1) { pub const sprite_index = Group.FontIndex.initSpecial(.sprite); test { - @import("std").testing.refAllDecls(@This()); + // For non-wasm we want to test everything we can + if (!comptime builtin.target.isWasm()) { + @import("std").testing.refAllDecls(@This()); + return; + } + + _ = Atlas; } diff --git a/src/main_wasm.zig b/src/main_wasm.zig index 06fbe4456..f2bcc9c60 100644 --- a/src/main_wasm.zig +++ b/src/main_wasm.zig @@ -1,4 +1,8 @@ // This is the main file for the WASM module. The WASM module has to // export a C ABI compatible API. +pub usingnamespace @import("wasm.zig"); pub usingnamespace @import("font/main.zig"); + +// TODO: temporary while we dev this +pub usingnamespace @import("font/face/web_canvas.zig");