diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index f83b51722..11f9fd23e 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -862,6 +862,41 @@ test "shape emoji width long" { try testing.expectEqual(@as(usize, 1), count); } +test "shaper selects font for entire grapheme" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var testdata = try testShaper(alloc); + defer testdata.deinit(); + + var t = try terminal.Terminal.init(io, alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + s.nextSlice("#\u{20E3}"); // Combining enclosing keycap + + const primary = (try testdata.grid.getIndex(alloc, '#', .regular, null)).?; + try testing.expect(testdata.grid.hasCodepoint(primary, '#', null)); + try testing.expect(!testdata.grid.hasCodepoint(primary, 0x20E3, null)); + + const additional = (try testdata.grid.getIndex(alloc, 0x20E3, .regular, null)).?; + try testing.expect(testdata.grid.hasCodepoint(additional, '#', null)); + try testing.expect(testdata.grid.hasCodepoint(additional, 0x20E3, null)); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + var it = testdata.shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + const run = (try it.next(alloc)).?; + try testing.expectEqual(additional, run.font_index); +} + test "shape variation selector VS15" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 45c5d38ca..9f33ba32e 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -348,47 +348,34 @@ pub const RunIterator = struct { // we just return whatever index for the cell codepoint. if (!cell.hasGrapheme()) return primary; - // If this is a grapheme, we need to find a font that supports - // all of the codepoints in the grapheme. - var candidates: std.ArrayList(font.Collection.Index) = try .initCapacity( - alloc, - graphemes.len + 1, - ); - defer candidates.deinit(alloc); - candidates.appendAssumeCapacity(primary); + // Try the primary font followed by the font selected for each + // additional codepoint until one supports the entire grapheme. + candidate: for (0..graphemes.len + 1) |i| { + const idx = if (i == 0) primary else idx: { + const cp = graphemes[i - 1]; - for (graphemes) |cp| { - // Ignore Emoji ZWJs - if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; + // Ignore Emoji ZWJs + if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue :candidate; - // Find a font that supports this codepoint. If none support this - // then the whole grapheme can't be rendered so we return null. - // - // We explicitly do not require the additional grapheme components - // to support the base presentation, since it is common for emoji - // fonts to support the base emoji with emoji presentation but not - // certain ZWJ-combined characters like the male and female signs. - const idx = try self.opts.grid.getIndex( - alloc, - cp, - style, - null, - ) orelse return null; - candidates.appendAssumeCapacity(idx); - } + // We explicitly do not require the additional grapheme components + // to support the base presentation, since it is common for emoji + // fonts to support the base emoji with emoji presentation but not + // certain ZWJ-combined characters like the male and female signs. + break :idx try self.opts.grid.getIndex( + alloc, + cp, + style, + null, + ) orelse return null; + }; - // We need to find a candidate that has ALL of our codepoints - for (candidates.items) |idx| { if (!self.opts.grid.hasCodepoint(idx, primary_cp, presentation)) continue; for (graphemes) |cp| { // Ignore Emoji ZWJs if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; - if (!self.opts.grid.hasCodepoint(idx, cp, null)) break; - } else { - // If the while completed, then we have a candidate that - // supports all of our codepoints. - return idx; + if (!self.opts.grid.hasCodepoint(idx, cp, null)) continue :candidate; } + return idx; } return null;