mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
terminal/kitty: preparing to build runs of placements
This commit is contained in:
@ -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,
|
||||||
|
@ -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| {
|
||||||
|
// 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
|
||||||
|
// this later we have to redo the logic; tests should cover;
|
||||||
|
var run: ?IncompletePlacement = null;
|
||||||
|
|
||||||
// A row must have graphemes to possibly have virtual placements
|
// A row must have graphemes to possibly have virtual placements
|
||||||
// since virtual placements are done via diacritics.
|
// since virtual placements are done via diacritics.
|
||||||
if (row.rowAndCell().row.grapheme) {
|
if (row.rowAndCell().row.grapheme) {
|
||||||
// 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
|
|
||||||
// this later we have to redo the logic; tests should cover;
|
|
||||||
const run: ?Placement = null;
|
|
||||||
_ = run;
|
|
||||||
|
|
||||||
// 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;
|
||||||
|
Reference in New Issue
Block a user