font: web canvas tracks glyph indexes for grapheme clusters

This commit is contained in:
Mitchell Hashimoto
2022-12-12 11:31:30 -08:00
parent 2b3478ce3d
commit c06ca04a1a
2 changed files with 85 additions and 3 deletions

View File

@ -9,6 +9,10 @@ const font = @import("../main.zig");
const log = std.log.scoped(.font_face); const log = std.log.scoped(.font_face);
pub const Face = struct { pub const Face = struct {
/// See graphemes field for more details.
const grapheme_start: u32 = 0x10FFFF + 1;
const grapheme_end: u32 = std.math.maxInt(u32);
/// The web canvas face makes use of an allocator when interacting /// The web canvas face makes use of an allocator when interacting
/// with the JS environment. /// with the JS environment.
alloc: Allocator, alloc: Allocator,
@ -28,6 +32,13 @@ pub const Face = struct {
/// The canvas element that we will reuse to render glyphs /// The canvas element that we will reuse to render glyphs
canvas: js.Object, canvas: js.Object,
/// The map to store multi-codepoint grapheme clusters that are rendered.
/// We use 1 above the maximum unicode codepoint up to the max 32-bit
/// unsigned integer to store the "glyph index" for graphemes.
grapheme_to_glyph: std.StringHashMapUnmanaged(u32) = .{},
glyph_to_grapheme: std.AutoHashMapUnmanaged(u32, []u8) = .{},
grapheme_next: u32 = grapheme_start,
/// Initialize a web canvas font with a "raw" value. The "raw" value can /// Initialize a web canvas font with a "raw" value. The "raw" value can
/// be any valid value for a CSS "font" property EXCLUDING the size. The /// be any valid value for a CSS "font" property EXCLUDING the size. The
/// size is always added via the `size` parameter. /// size is always added via the `size` parameter.
@ -72,6 +83,12 @@ pub const Face = struct {
pub fn deinit(self: *Face) void { pub fn deinit(self: *Face) void {
self.alloc.free(self.font_str); self.alloc.free(self.font_str);
self.grapheme_to_glyph.deinit(self.alloc);
{
var it = self.glyph_to_grapheme.valueIterator();
while (it.next()) |value| self.alloc.free(value.*);
self.glyph_to_grapheme.deinit(self.alloc);
}
self.canvas.deinit(); self.canvas.deinit();
self.* = undefined; self.* = undefined;
} }
@ -132,6 +149,34 @@ pub const Face = struct {
return .text; return .text;
} }
/// Returns the glyph index for the given grapheme cluster. The same
/// cluster will always map to the same glyph index. This does not render
/// the grapheme at this time, only reserves the index.
pub fn graphemeGlyphIndex(self: *Face, cluster: []const u8) error{OutOfMemory}!u32 {
// If we already have this stored then return it
const gop = try self.grapheme_to_glyph.getOrPut(self.alloc, cluster);
if (gop.found_existing) return gop.value_ptr.*;
errdefer _ = self.grapheme_to_glyph.remove(cluster);
// We don't have it stored. Ensure we have space to store. The
// next will be "0" if we're out of space due to unsigned int wrapping.
if (self.grapheme_next == 0) return error.OutOfMemory;
// Copy the cluster for our reverse mapping
const copy = try self.alloc.dupe(u8, cluster);
errdefer self.alloc.free(copy);
// Grow space for the reverse mapping
try self.glyph_to_grapheme.ensureUnusedCapacity(self.alloc, 1);
// Store it
gop.value_ptr.* = self.grapheme_next;
self.glyph_to_grapheme.putAssumeCapacity(self.grapheme_next, copy);
self.grapheme_next +%= 1;
return gop.value_ptr.*;
}
/// Render a glyph using the glyph index. The rendered glyph is stored /// Render a glyph using the glyph index. The rendered glyph is stored
/// in the given texture atlas. /// in the given texture atlas.
pub fn renderGlyph( pub fn renderGlyph(

View File

@ -71,7 +71,6 @@ pub const Shaper = struct {
/// returned. /// returned.
pub fn shape(self: *Shaper, run: font.shape.TextRun) ![]font.shape.Cell { pub fn shape(self: *Shaper, run: font.shape.TextRun) ![]font.shape.Cell {
// TODO: memory check that cell_buf can fit results // TODO: memory check that cell_buf can fit results
_ = run;
const codepoints = self.run_buf.items(.codepoint); const codepoints = self.run_buf.items(.codepoint);
const clusters = self.run_buf.items(.cluster); const clusters = self.run_buf.items(.cluster);
@ -143,9 +142,47 @@ pub const Shaper = struct {
.glyph_index = codepoints[start], .glyph_index = codepoints[start],
}, },
// We must have multiple codepoints (see assert above). In
// this case we UTF-8 encode the codepoints and send them
// to the face to reserve a private glyph index.
else => { else => {
unreachable; // UTF-8 encode the codepoints in this cluster.
// TODO; const cluster = cluster: {
const cluster_points = codepoints[start..i];
assert(cluster_points.len == len);
const buf_len = buf_len: {
var acc: usize = 0;
for (cluster_points) |cp| {
acc += try std.unicode.utf8CodepointSequenceLength(
@intCast(u21, cp),
);
}
break :buf_len acc;
};
var buf = try self.alloc.alloc(u8, buf_len);
errdefer self.alloc.free(buf);
var buf_i: usize = 0;
for (cluster_points) |cp| {
buf_i += try std.unicode.utf8Encode(
@intCast(u21, cp),
buf[buf_i..],
);
}
break :cluster buf;
};
defer self.alloc.free(cluster);
var face = try run.group.group.faceFromIndex(run.font_index);
const index = try face.graphemeGlyphIndex(cluster);
self.cell_buf[cur] = .{
.x = @intCast(u16, clusters[start]),
.glyph_index = index,
};
}, },
} }