font/shaper: start converting run to new terminal

This commit is contained in:
Mitchell Hashimoto
2024-03-08 09:28:22 -08:00
parent 312eb050f3
commit e3230cf1e6
5 changed files with 541 additions and 458 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const font = @import("../main.zig"); const font = @import("../main.zig");
const shape = @import("../shape.zig"); const shape = @import("../shape.zig");
const terminal = @import("../../terminal/main.zig"); const terminal = @import("../../terminal/main.zig").new;
/// A single text run. A text run is only valid for one Shaper instance and /// A single text run. A text run is only valid for one Shaper instance and
/// until the next run is created. A text run never goes across multiple /// until the next run is created. A text run never goes across multiple
@ -26,17 +26,22 @@ pub const TextRun = struct {
pub const RunIterator = struct { pub const RunIterator = struct {
hooks: font.Shaper.RunIteratorHook, hooks: font.Shaper.RunIteratorHook,
group: *font.GroupCache, group: *font.GroupCache,
row: terminal.Screen.Row, row: terminal.Pin,
selection: ?terminal.Selection = null, selection: ?terminal.Selection = null,
cursor_x: ?usize = null, cursor_x: ?usize = null,
i: usize = 0, i: usize = 0,
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
const cells = self.row.cells(.all);
// Trim the right side of a row that might be empty // Trim the right side of a row that might be empty
const max: usize = max: { const max: usize = max: {
var j: usize = self.row.lenCells(); for (0..cells.len) |i| {
while (j > 0) : (j -= 1) if (!self.row.getCell(j - 1).empty()) break; const rev_i = cells.len - i - 1;
break :max j; if (!cells[rev_i].isEmpty()) break :max rev_i + 1;
}
break :max 0;
}; };
// We're over at the max // We're over at the max
@ -52,63 +57,60 @@ pub const RunIterator = struct {
var j: usize = self.i; var j: usize = self.i;
while (j < max) : (j += 1) { while (j < max) : (j += 1) {
const cluster = j; const cluster = j;
const cell = self.row.getCell(j); const cell = &cells[j];
// If we have a selection and we're at a boundary point, then // If we have a selection and we're at a boundary point, then
// we break the run here. // we break the run here.
if (self.selection) |unordered_sel| { // TODO(paged-terminal)
if (j > self.i) { // if (self.selection) |unordered_sel| {
const sel = unordered_sel.ordered(.forward); // if (j > self.i) {
// const sel = unordered_sel.ordered(.forward);
if (sel.start.x > 0 and //
j == sel.start.x and // if (sel.start.x > 0 and
self.row.graphemeBreak(sel.start.x)) break; // j == sel.start.x and
// self.row.graphemeBreak(sel.start.x)) break;
if (sel.end.x > 0 and //
j == sel.end.x + 1 and // if (sel.end.x > 0 and
self.row.graphemeBreak(sel.end.x)) break; // j == sel.end.x + 1 and
} // self.row.graphemeBreak(sel.end.x)) break;
} // }
// }
// If we're a spacer, then we ignore it // If we're a spacer, then we ignore it
if (cell.attrs.wide_spacer_tail) continue; switch (cell.wide) {
.narrow, .wide => {},
.spacer_head, .spacer_tail => continue,
}
// If our cell attributes are changing, then we split the run. // If our cell attributes are changing, then we split the run.
// This prevents a single glyph for ">=" to be rendered with // This prevents a single glyph for ">=" to be rendered with
// one color when the two components have different styling. // one color when the two components have different styling.
if (j > self.i) { if (j > self.i) {
const prev_cell = self.row.getCell(j - 1); const prev_cell = cells[j - 1];
const Attrs = @TypeOf(cell.attrs); if (prev_cell.style_id != cell.style_id) break;
const Int = @typeInfo(Attrs).Struct.backing_integer.?;
const prev_attrs: Int = @bitCast(prev_cell.attrs.styleAttrs());
const attrs: Int = @bitCast(cell.attrs.styleAttrs());
if (prev_attrs != attrs) break;
if (!cell.bg.eql(prev_cell.bg)) break;
if (!cell.fg.eql(prev_cell.fg)) break;
} }
// Text runs break when font styles change so we need to get // Text runs break when font styles change so we need to get
// the proper style. // the proper style.
const style: font.Style = style: { const style: font.Style = style: {
if (cell.attrs.bold) { // TODO(paged-terminal)
if (cell.attrs.italic) break :style .bold_italic; // if (cell.attrs.bold) {
break :style .bold; // if (cell.attrs.italic) break :style .bold_italic;
} // break :style .bold;
// }
if (cell.attrs.italic) break :style .italic; //
// if (cell.attrs.italic) break :style .italic;
break :style .regular; break :style .regular;
}; };
// Determine the presentation format for this glyph. // Determine the presentation format for this glyph.
const presentation: ?font.Presentation = if (cell.attrs.grapheme) p: { const presentation: ?font.Presentation = if (cell.hasGrapheme()) p: {
// We only check the FIRST codepoint because I believe the // We only check the FIRST codepoint because I believe the
// presentation format must be directly adjacent to the codepoint. // presentation format must be directly adjacent to the codepoint.
var it = self.row.codepointIterator(j); const cps = self.row.grapheme(cell) orelse break :p null;
if (it.next()) |cp| { assert(cps.len > 0);
if (cp == 0xFE0E) break :p .text; if (cps[0] == 0xFE0E) break :p .text;
if (cp == 0xFE0F) break :p .emoji; if (cps[0] == 0xFE0F) break :p .emoji;
}
break :p null; break :p null;
} else emoji: { } else emoji: {
// If we're not a grapheme, our individual char could be // If we're not a grapheme, our individual char could be
@ -128,7 +130,7 @@ pub const RunIterator = struct {
// such as a skin-tone emoji is fine, but hovering over the // such as a skin-tone emoji is fine, but hovering over the
// joiners will show the joiners allowing you to modify the // joiners will show the joiners allowing you to modify the
// emoji. // emoji.
if (!cell.attrs.grapheme) { if (!cell.hasGrapheme()) {
if (self.cursor_x) |cursor_x| { if (self.cursor_x) |cursor_x| {
// Exactly: self.i is the cursor and we iterated once. This // Exactly: self.i is the cursor and we iterated once. This
// means that we started exactly at the cursor and did at // means that we started exactly at the cursor and did at
@ -163,7 +165,6 @@ pub const RunIterator = struct {
// then we use that. // then we use that.
if (try self.indexForCell( if (try self.indexForCell(
alloc, alloc,
j,
cell, cell,
style, style,
presentation, presentation,
@ -206,12 +207,12 @@ pub const RunIterator = struct {
// Add all the codepoints for our grapheme // Add all the codepoints for our grapheme
try self.hooks.addCodepoint( try self.hooks.addCodepoint(
if (cell.char == 0) ' ' else cell.char, if (cell.codepoint() == 0) ' ' else cell.codepoint(),
@intCast(cluster), @intCast(cluster),
); );
if (cell.attrs.grapheme) { if (cell.hasGrapheme()) {
var it = self.row.codepointIterator(j); const cps = self.row.grapheme(cell).?;
while (it.next()) |cp| { for (cps) |cp| {
// Do not send presentation modifiers // Do not send presentation modifiers
if (cp == 0xFE0E or cp == 0xFE0F) continue; if (cp == 0xFE0E or cp == 0xFE0F) continue;
try self.hooks.addCodepoint(cp, @intCast(cluster)); try self.hooks.addCodepoint(cp, @intCast(cluster));
@ -242,13 +243,12 @@ pub const RunIterator = struct {
fn indexForCell( fn indexForCell(
self: *RunIterator, self: *RunIterator,
alloc: Allocator, alloc: Allocator,
j: usize, cell: *terminal.Cell,
cell: terminal.Screen.Cell,
style: font.Style, style: font.Style,
presentation: ?font.Presentation, presentation: ?font.Presentation,
) !?font.Group.FontIndex { ) !?font.Group.FontIndex {
// Get the font index for the primary codepoint. // Get the font index for the primary codepoint.
const primary_cp: u32 = if (cell.empty() or cell.char == 0) ' ' else cell.char; const primary_cp: u32 = if (cell.isEmpty() or cell.codepoint() == 0) ' ' else cell.codepoint();
const primary = try self.group.indexForCodepoint( const primary = try self.group.indexForCodepoint(
alloc, alloc,
primary_cp, primary_cp,
@ -258,16 +258,16 @@ pub const RunIterator = struct {
// Easy, and common: we aren't a multi-codepoint grapheme, so // Easy, and common: we aren't a multi-codepoint grapheme, so
// we just return whatever index for the cell codepoint. // we just return whatever index for the cell codepoint.
if (!cell.attrs.grapheme) return primary; if (!cell.hasGrapheme()) return primary;
// If this is a grapheme, we need to find a font that supports // If this is a grapheme, we need to find a font that supports
// all of the codepoints in the grapheme. // all of the codepoints in the grapheme.
var it = self.row.codepointIterator(j); const cps = self.row.grapheme(cell) orelse return primary;
var candidates = try std.ArrayList(font.Group.FontIndex).initCapacity(alloc, it.len() + 1); var candidates = try std.ArrayList(font.Group.FontIndex).initCapacity(alloc, cps.len + 1);
defer candidates.deinit(); defer candidates.deinit();
candidates.appendAssumeCapacity(primary); candidates.appendAssumeCapacity(primary);
while (it.next()) |cp| { for (cps) |cp| {
// Ignore Emoji ZWJs // Ignore Emoji ZWJs
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue;
@ -285,8 +285,7 @@ pub const RunIterator = struct {
// We need to find a candidate that has ALL of our codepoints // We need to find a candidate that has ALL of our codepoints
for (candidates.items) |idx| { for (candidates.items) |idx| {
if (!self.group.group.hasCodepoint(idx, primary_cp, presentation)) continue; if (!self.group.group.hasCodepoint(idx, primary_cp, presentation)) continue;
it.reset(); for (cps) |cp| {
while (it.next()) |cp| {
// Ignore Emoji ZWJs // Ignore Emoji ZWJs
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue;
if (!self.group.group.hasCodepoint(idx, cp, presentation)) break; if (!self.group.group.hasCodepoint(idx, cp, presentation)) break;

View File

@ -2053,6 +2053,12 @@ pub const Pin = struct {
}; };
} }
/// Returns the grapheme codepoints for the given cell. These are only
/// the EXTRA codepoints and not the first codepoint.
pub fn grapheme(self: Pin, cell: *pagepkg.Cell) ?[]u21 {
return self.page.data.lookupGrapheme(cell);
}
/// Iterators. These are the same as PageList iterator funcs but operate /// Iterators. These are the same as PageList iterator funcs but operate
/// on pins rather than points. This is MUCH more efficient than calling /// on pins rather than points. This is MUCH more efficient than calling
/// pointFromPin and building up the iterator from points. /// pointFromPin and building up the iterator from points.

View File

@ -22,6 +22,7 @@ pub const x11_color = @import("x11_color.zig");
pub const Charset = charsets.Charset; pub const Charset = charsets.Charset;
pub const CharsetSlot = charsets.Slots; pub const CharsetSlot = charsets.Slots;
pub const CharsetActiveSlot = charsets.ActiveSlot; pub const CharsetActiveSlot = charsets.ActiveSlot;
pub const Cell = page.Cell;
pub const CSI = Parser.Action.CSI; pub const CSI = Parser.Action.CSI;
pub const DCS = Parser.Action.DCS; pub const DCS = Parser.Action.DCS;
pub const MouseShape = @import("mouse_shape.zig").MouseShape; pub const MouseShape = @import("mouse_shape.zig").MouseShape;

View File

@ -748,10 +748,10 @@ pub const Cell = packed struct(u64) {
}; };
/// Helper to make a cell that just has a codepoint. /// Helper to make a cell that just has a codepoint.
pub fn init(codepoint: u21) Cell { pub fn init(cp: u21) Cell {
return .{ return .{
.content_tag = .codepoint, .content_tag = .codepoint,
.content = .{ .codepoint = codepoint }, .content = .{ .codepoint = cp },
}; };
} }
@ -767,6 +767,18 @@ pub const Cell = packed struct(u64) {
}; };
} }
pub fn codepoint(self: Cell) u21 {
return switch (self.content_tag) {
.codepoint,
.codepoint_grapheme,
=> self.content.codepoint,
.bg_color_palette,
.bg_color_rgb,
=> 0,
};
}
pub fn hasStyling(self: Cell) bool { pub fn hasStyling(self: Cell) bool {
return self.style_id != style.default_id; return self.style_id != style.default_id;
} }