From f53d9fb704cf546fe96e276b6d6658418b78eea3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Dec 2022 18:29:49 -0800 Subject: [PATCH] font: web canvas shaper yields runs --- example/app.ts | 24 +++++++++--- src/font/shaper/web_canvas.zig | 71 +++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/example/app.ts b/example/app.ts index a8d665ffc..c40ac2c74 100644 --- a/example/app.ts +++ b/example/app.ts @@ -46,6 +46,9 @@ fetch(url.href).then(response => atlas_new, atlas_free, atlas_debug_canvas, + shaper_new, + shaper_free, + shaper_test, } = results.instance.exports; // Give us access to the zjs value for debugging. globalThis.zjs = zjs; @@ -54,13 +57,19 @@ fetch(url.href).then(response => // Initialize our zig-js memory zjs.memory = memory; + // Helpers + const makeStr = (str) => { + const utf8 = new TextEncoder().encode(str); + const ptr = malloc(utf8.byteLength); + new Uint8Array(memory.buffer, ptr).set(utf8); + return { ptr: ptr, len: utf8.byteLength }; + }; + // Create our atlas // const atlas = atlas_new(512, 0 /* greyscale */); // Create some memory for our string - const font = new TextEncoder().encode("monospace"); - const font_ptr = malloc(font.byteLength); - new Uint8Array(memory.buffer, font_ptr).set(font); + const font_name = makeStr("monospace"); // Initialize our deferred face // const df = deferred_face_new(font_ptr, font.byteLength, 0 /* text */); @@ -73,8 +82,8 @@ fetch(url.href).then(response => // Create our group const group = group_new(72 /* size */); - group_add_face(group, 0 /* regular */, deferred_face_new(font_ptr, font.byteLength, 0 /* text */)); - group_add_face(group, 0 /* regular */, deferred_face_new(font_ptr, font.byteLength, 1 /* emoji */)); + group_add_face(group, 0 /* regular */, deferred_face_new(font_name.ptr, font_name.len, 0 /* text */)); + group_add_face(group, 0 /* regular */, deferred_face_new(font_name.ptr, font_name.len, 1 /* emoji */)); // Create our group cache const group_cache = group_cache_new(group); @@ -112,5 +121,10 @@ fetch(url.href).then(response => document.getElementById("atlas-color-canvas").append(zjs.deleteValue(id)); } + // Let's try shaping + const shaper = shaper_new(120); + const input = makeStr("hello"); + shaper_test(shaper, group_cache, input.ptr, input.len); + //face_free(face); }); diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index ab6af2da7..1d4190a84 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -40,6 +40,19 @@ pub const Shaper = struct { return .{ .hooks = .{ .shaper = self }, .group = group, .row = row }; } + /// Shape the given text run. The text run must be the immediately previous + /// text run that was iterated since the text run does share state with the + /// Shaper struct. + /// + /// The return value is only valid until the next shape call is called. + /// + /// If there is not enough space in the cell buffer, an error is returned. + pub fn shape(self: *Shaper, run: font.shape.TextRun) ![]font.shape.Cell { + _ = self; + _ = run; + return error.Unimplemented; + } + /// The hooks for RunIterator. pub const RunIteratorHook = struct { shaper: *Shaper, @@ -51,7 +64,7 @@ pub const Shaper = struct { pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void { _ = cluster; - try self.shaper.append(cp); + try self.shaper.run_buf.append(cp); } pub fn finalize(self: RunIteratorHook) !void { @@ -64,4 +77,60 @@ pub const Shaper = struct { pub const Wasm = struct { const wasm = @import("../../os/wasm.zig"); const alloc = wasm.alloc; + + export fn shaper_new(cap: usize) ?*Shaper { + return shaper_new_(cap) catch null; + } + + fn shaper_new_(cap: usize) !*Shaper { + var cell_buf = try alloc.alloc(font.shape.Cell, cap); + errdefer alloc.free(cell_buf); + + var shaper = try Shaper.init(alloc, cell_buf); + errdefer shaper.deinit(); + + var result = try alloc.create(Shaper); + errdefer alloc.destroy(result); + result.* = shaper; + return result; + } + + export fn shaper_free(ptr: ?*Shaper) void { + if (ptr) |v| { + alloc.free(v.cell_buf); + v.deinit(); + alloc.destroy(v); + } + } + + /// Runs a test to verify shaping works properly. + export fn shaper_test( + self: *Shaper, + group: *font.GroupCache, + str: [*]const u8, + len: usize, + ) void { + shaper_test_(self, group, str[0..len]) catch |err| { + log.warn("error during shaper test err={}", .{err}); + }; + } + + fn shaper_test_(self: *Shaper, group: *font.GroupCache, str: []const u8) !void { + // Create a terminal and print all our characters into it. + var term = try terminal.Terminal.init(alloc, self.cell_buf.len, 80); + defer term.deinit(alloc); + for (str) |c| try term.print(c); + + // Iterate over the rows and print out all the runs we get. + var rowIter = term.screen.rowIterator(.viewport); + var y: usize = 0; + while (rowIter.next()) |row| { + defer y += 1; + + var iter = self.runIterator(group, row); + while (try iter.next(alloc)) |run| { + log.info("y={} run={}", .{ y, run }); + } + } + } };