terminal/kitty: preparing to build runs of placements

This commit is contained in:
Mitchell Hashimoto
2024-07-25 14:56:46 -07:00
parent 7c6ae90300
commit cf6463fec0
2 changed files with 183 additions and 74 deletions

View File

@ -3229,7 +3229,7 @@ pub const Pin = struct {
} }
/// Returns the style for the given cell in this pin. /// Returns the style for the given cell in this pin.
pub fn style(self: Pin, cell: *pagepkg.Cell) stylepkg.Style { pub fn style(self: Pin, cell: *const pagepkg.Cell) stylepkg.Style {
if (cell.style_id == stylepkg.default_id) return .{}; if (cell.style_id == stylepkg.default_id) return .{};
return self.page.data.styles.get( return self.page.data.styles.get(
self.page.data.memory, self.page.data.memory,

View File

@ -6,6 +6,8 @@ const assert = std.debug.assert;
const testing = std.testing; const testing = std.testing;
const terminal = @import("../main.zig"); const terminal = @import("../main.zig");
const log = std.log.scoped(.kitty_gfx);
/// Codepoint for the unicode placeholder character. /// Codepoint for the unicode placeholder character.
pub const placeholder: u21 = 0x10EEEE; pub const placeholder: u21 = 0x10EEEE;
@ -22,23 +24,6 @@ pub fn placementIterator(
return .{ .row_it = row_it, .row = row }; return .{ .row_it = row_it, .row = row };
} }
/// Convert a style color to a Kitty image protocol ID. This works by
/// taking the 24 most significant bits of the color, which lets it work
/// for both palette and rgb-based colors.
fn colorToId(c: terminal.Style.Color) u32 {
// TODO: test this
return switch (c) {
.none => 0,
.palette => |v| @intCast(v),
.rgb => |rgb| rgb: {
const r: u24 = @intCast(rgb.r);
const g: u24 = @intCast(rgb.g);
const b: u24 = @intCast(rgb.b);
break :rgb (r << 16) | (g << 8) | b;
},
};
}
/// Iterator over unicode virtual placements. /// Iterator over unicode virtual placements.
pub const PlacementIterator = struct { pub const PlacementIterator = struct {
row_it: terminal.PageList.RowIterator, row_it: terminal.PageList.RowIterator,
@ -46,80 +31,62 @@ pub const PlacementIterator = struct {
pub fn next(self: *PlacementIterator) ?Placement { pub fn next(self: *PlacementIterator) ?Placement {
while (self.row) |*row| { while (self.row) |*row| {
// A row must have graphemes to possibly have virtual placements
// since virtual placements are done via diacritics.
if (row.rowAndCell().row.grapheme) {
// Our current run. A run is always only a single row. This // Our current run. A run is always only a single row. This
// assumption is built-in to our logic so if we want to change // assumption is built-in to our logic so if we want to change
// this later we have to redo the logic; tests should cover; // this later we have to redo the logic; tests should cover;
const run: ?Placement = null; var run: ?IncompletePlacement = null;
_ = run;
// A row must have graphemes to possibly have virtual placements
// since virtual placements are done via diacritics.
if (row.rowAndCell().row.grapheme) {
// Iterate over our remaining cells and find one with a placeholder. // Iterate over our remaining cells and find one with a placeholder.
const cells = row.cells(.right); const cells = row.cells(.right);
for (cells, row.x..) |*cell, x| { for (cells, row.x..) |*cell, x| {
if (cell.codepoint() != placeholder) continue; // "row" now points to the top-left pin of the placement.
// We need this temporary state to build our incomplete
// placement.
assert(@intFromPtr(row) == @intFromPtr(&self.row));
row.x = @intCast(x);
// If this cell doesn't have the placeholder, then we
// complete the run if we have it otherwise we just move
// on and keep searching.
if (cell.codepoint() != placeholder) {
if (run) |prev| return prev.complete();
continue;
}
// TODO: we need to support non-grapheme cells that just // TODO: we need to support non-grapheme cells that just
// do continuations all the way through. // do continuations all the way through.
assert(cell.hasGrapheme()); assert(cell.hasGrapheme());
// "row" now points to the top-left pin of the placement. // If we don't have a previous run, then we save this
row.x = @intCast(x); // incomplete one, start a run, and move on.
const curr = IncompletePlacement.init(row, cell);
// Determine our image ID and placement ID from the style. if (run) |*prev| {
const style = row.style(cell); // If we can't append, then we complete the previous
const image_id = colorToId(style.fg_color); // run and return it.
const placement_id = colorToId(style.underline_color); if (!prev.append(&curr)) {
// Note: self.row is already updated due to the
// Build our placement // row pointer above. It points back at this same
var p: Placement = .{ // cell so we can continue the new placements from
.pin = row.*, // here.
.image_id = image_id, return prev.complete();
.placement_id = placement_id,
// Filled in below. Marked as undefined so we can catch
// bugs with safety checks.
.col = undefined,
.row = undefined,
// For now we don't build runs and we always produce
// single cell placements.
.width = 1,
.height = 1,
};
// Determine our row/col by looking at the diacritics.
// If the cell doesn't have graphemes that's okay because
// of continuations.
const cps: []const u21 = row.grapheme(cell) orelse &.{};
if (cps.len > 0) {
p.row = getIndex(cps[0]) orelse @panic("TODO: invalid");
if (cps.len > 1) {
p.col = getIndex(cps[1]) orelse @panic("TODO: invalid");
if (cps.len > 2) {
const high = getIndex(cps[2]) orelse @panic("TODO: invalid");
p.image_id += high << 24;
} }
}
} else @panic("TODO: continuations");
if (x == cells.len - 1) { // append is mutating so if we reached this point
// We are at the end of this row so move to the next row // then prev has been updated.
self.row = self.row_it.next();
} else { } else {
// We can move right to the next cell. row is a pointer run = curr;
// to self.row so we can modify it directly.
assert(@intFromPtr(row) == @intFromPtr(&self.row));
row.x += 1;
} }
return p;
} }
} }
// We didn't find any placements. Move to the next row. // We move to the next row no matter what
self.row = self.row_it.next(); self.row = self.row_it.next();
// If we have a run, we complete it here.
if (run) |prev| return prev.complete();
} }
return null; return null;
@ -150,8 +117,150 @@ pub const Placement = struct {
height: u32, height: u32,
}; };
/// IncompletePlacement is the placement information present in a single
/// cell. It is "incomplete" because the specification allows for missing
/// diacritics and so on that continue from previous valid placements.
const IncompletePlacement = struct {
/// The pin of the cell that created this incomplete placement.
pin: terminal.Pin,
/// Lower 24 bits of the image ID. This is specified in the fg color
/// and is always required.
image_id_low: u24,
/// Higher 8 bits of the image ID specified using the 3rd diacritic.
/// This is optional.
image_id_high: ?u8 = null,
/// Placement ID is optionally specified in the underline color.
placement_id: ?u24 = null,
/// The row/col index for the image. These are 0-indexed. These
/// are specified using diacritics. The row is first and the col
/// is second. Both are optional. If not specified, they can continue
/// a previous placement under certain conditions.
row: ?u32 = null,
col: ?u32 = null,
/// Parse the incomplete placement information from a row and cell.
///
/// The cell could be derived from the row but in our usage we already
/// have the cell and we don't want to waste cycles recomputing it.
pub fn init(
row: *const terminal.Pin,
cell: *const terminal.Cell,
) IncompletePlacement {
assert(cell.codepoint() == placeholder);
const style = row.style(cell);
var result: IncompletePlacement = .{
.pin = row.*,
.image_id_low = colorToId(style.fg_color),
.placement_id = placement_id: {
const id = colorToId(style.underline_color);
break :placement_id if (id != 0) id else null;
},
};
// Try to decode all our diacritics. Any invalid diacritics are
// treated as if they don't exist. This isn't explicitly specified
// at the time of writing this but it appears to be how Kitty behaves.
const cps: []const u21 = row.grapheme(cell) orelse &.{};
if (cps.len > 0) {
result.row = getIndex(cps[0]) orelse value: {
log.warn("virtual placement with invalid row diacritic cp={X}", .{cps[0]});
break :value null;
};
if (cps.len > 1) {
result.col = getIndex(cps[1]) orelse value: {
log.warn("virtual placement with invalid col diacritic cp={X}", .{cps[1]});
break :value null;
};
if (cps.len > 2) {
const high_ = getIndex(cps[2]) orelse value: {
log.warn("virtual placement with invalid high diacritic cp={X}", .{cps[2]});
break :value null;
};
if (high_) |high| {
result.image_id_high = std.math.cast(
u8,
high,
) orelse value: {
log.warn("virtual placement with invalid high diacritic cp={X} value={}", .{
cps[2],
high,
});
break :value null;
};
}
// Any additional diacritics are ignored.
}
}
}
return result;
}
/// Append this incomplete placement to an existing placement to
/// create a run. This returns true if the placements are compatible
/// and were combined. If this returns false, the other placement is
/// unchanged.
pub fn append(self: *IncompletePlacement, other: *const IncompletePlacement) bool {
return self.canAppend(other);
}
fn canAppend(self: *const IncompletePlacement, other: *const IncompletePlacement) bool {
if (self.image_id_low != other.image_id_low) return false;
if (self.placement_id != other.placement_id) return false;
return false;
}
/// Complete the incomplete placement to create a full placement.
/// This creates a new placement that isn't continuous with any previous
/// placements.
///
/// The pin is the pin of the cell that created this incomplete placement.
pub fn complete(self: *const IncompletePlacement) Placement {
return .{
.pin = self.pin,
.image_id = image_id: {
const low: u32 = @intCast(self.image_id_low);
const high: u32 = @intCast(self.image_id_high orelse 0);
break :image_id low | (high << 24);
},
.placement_id = self.placement_id orelse 0,
.col = self.col orelse 0,
.row = self.row orelse 0,
.width = 1,
.height = 1,
};
}
/// Convert a style color to a Kitty image protocol ID. This works by
/// taking the 24 most significant bits of the color, which lets it work
/// for both palette and rgb-based colors.
fn colorToId(c: terminal.Style.Color) u24 {
// TODO: test this
return switch (c) {
.none => 0,
.palette => |v| @intCast(v),
.rgb => |rgb| rgb: {
const r: u24 = @intCast(rgb.r);
const g: u24 = @intCast(rgb.g);
const b: u24 = @intCast(rgb.b);
break :rgb (r << 16) | (g << 8) | b;
},
};
}
};
/// Get the row/col index for a diacritic codepoint. These are 0-indexed. /// Get the row/col index for a diacritic codepoint. These are 0-indexed.
pub fn getIndex(cp: u21) ?u32 { fn getIndex(cp: u21) ?u32 {
const idx = std.sort.binarySearch(u21, cp, diacritics, {}, (struct { const idx = std.sort.binarySearch(u21, cp, diacritics, {}, (struct {
fn order(context: void, lhs: u21, rhs: u21) std.math.Order { fn order(context: void, lhs: u21, rhs: u21) std.math.Order {
_ = context; _ = context;