mirror of
https://github.com/aphrody-labs/opentui.git
synced 2026-10-13 21:08:53 +00:00
native: reject invalid blended cell coordinates (#1556)
Negative JavaScript coordinates arrive through Bun FFI as wrapped u32 values. Reject them before signed conversion so off-screen blended writes cannot overflow or address an invalid cell. Fix #1552
This commit is contained in:
1 parent
cbec2687c1
commit
ddb7fbd691
7 files changed
+287
-67
No files matched your search
@@ -26,6 +26,46 @@ describe("OptimizedBuffer", () => {
|
||||
expect([...buffer.buffers.attributes.slice(0, attributes.length)]).toEqual(attributes)
|
||||
})
|
||||
|
||||
it("clips draws at negative positions", () => {
|
||||
// Node FFI rejects a negative u32 argument, so every call here also checks that positions cross FFI as i32.
|
||||
const target = OptimizedBuffer.create(3, 2, "unicode", { id: "negative-positions" })
|
||||
try {
|
||||
const white = RGBA.fromInts(255, 255, 255)
|
||||
const black = RGBA.fromInts(0, 0, 0)
|
||||
target.clear(black)
|
||||
|
||||
target.setCell(-1, 0, "S", white, black)
|
||||
target.setCellWithAlphaBlending(0, -1, "A", white, black)
|
||||
target.drawChar("D".codePointAt(0)!, -1, -1, white, black)
|
||||
target.drawSuperSampleBuffer(-1, -1, new Uint8Array(16), 16, "rgba8unorm", 8)
|
||||
target.drawPackedBuffer(new Uint8Array(48), 48, -1, -1, 1, 1)
|
||||
target.drawText("ABCD", -2, 1, white, black)
|
||||
target.fillRect(-1, -1, 2, 2, RGBA.fromInts(255, 0, 0))
|
||||
|
||||
expect(new TextDecoder().decode(target.getRealCharBytes(true))).toBe(" \nCD \n")
|
||||
expect([0, 1, 3].map((cell) => target.buffers.bg[cell * 4] & 0xff)).toEqual([255, 0, 0])
|
||||
} finally {
|
||||
target.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it("fills nothing for a non-positive extent", () => {
|
||||
// Bun wraps a negative u32 argument into a huge extent, so the wrapper must return before the FFI call.
|
||||
const target = OptimizedBuffer.create(3, 2, "unicode", { id: "empty-extents" })
|
||||
try {
|
||||
const red = RGBA.fromInts(255, 0, 0)
|
||||
target.clear(RGBA.fromInts(0, 0, 0))
|
||||
|
||||
target.fillRect(1, 0, -1, 1, red)
|
||||
target.fillRect(1, 0, 1, -1, red)
|
||||
target.fillRect(1, 0, 0, 1, red)
|
||||
|
||||
expect([0, 1, 2, 3, 4, 5].map((cell) => target.buffers.bg[cell * 4] & 0xff)).toEqual([0, 0, 0, 0, 0, 0])
|
||||
} finally {
|
||||
target.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it("draws images as reserved cells with resolved fallback glyphs", () => {
|
||||
const image = NativeImage.fromRgba(
|
||||
Uint8Array.of(255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255),
|
||||
|
||||
@@ -302,6 +302,8 @@ export class OptimizedBuffer {
|
||||
|
||||
public fillRect(x: number, y: number, width: number, height: number, bg: RGBA): void {
|
||||
this.guard()
|
||||
// An empty rectangle draws nothing. A negative extent would wrap to a huge u32 at the FFI boundary.
|
||||
if (width <= 0 || height <= 0) return
|
||||
this.lib.bufferFillRect(this.bufferPtr, x, y, width, height, bg)
|
||||
}
|
||||
|
||||
|
||||
@@ -702,19 +702,19 @@ function getOpenTUILib(libPath?: string) {
|
||||
},
|
||||
|
||||
bufferDrawText: {
|
||||
args: ["u32", "ptr", "u32", "u32", "u32", "buffer", "ptr", "u32"],
|
||||
args: ["u32", "ptr", "u32", "i32", "i32", "buffer", "ptr", "u32"],
|
||||
returns: "void",
|
||||
},
|
||||
bufferSetCellWithAlphaBlending: {
|
||||
args: ["u32", "u32", "u32", "u32", "buffer", "buffer", "u32"],
|
||||
args: ["u32", "i32", "i32", "u32", "buffer", "buffer", "u32"],
|
||||
returns: "void",
|
||||
},
|
||||
bufferSetCell: {
|
||||
args: ["u32", "u32", "u32", "u32", "buffer", "buffer", "u32"],
|
||||
args: ["u32", "i32", "i32", "u32", "buffer", "buffer", "u32"],
|
||||
returns: "void",
|
||||
},
|
||||
bufferFillRect: {
|
||||
args: ["u32", "u32", "u32", "u32", "u32", "buffer"],
|
||||
args: ["u32", "i32", "i32", "u32", "u32", "buffer"],
|
||||
returns: "void",
|
||||
},
|
||||
bufferColorMatrix: {
|
||||
@@ -874,7 +874,7 @@ function getOpenTUILib(libPath?: string) {
|
||||
},
|
||||
|
||||
bufferDrawSuperSampleBuffer: {
|
||||
args: ["u32", "u32", "u32", "ptr", "u32", "u8", "u32"],
|
||||
args: ["u32", "i32", "i32", "ptr", "u32", "u8", "u32"],
|
||||
returns: "void",
|
||||
},
|
||||
bufferDrawImage: {
|
||||
@@ -882,7 +882,7 @@ function getOpenTUILib(libPath?: string) {
|
||||
returns: "u8",
|
||||
},
|
||||
bufferDrawPackedBuffer: {
|
||||
args: ["u32", "ptr", "u32", "u32", "u32", "u32", "u32"],
|
||||
args: ["u32", "ptr", "u32", "i32", "i32", "u32", "u32"],
|
||||
returns: "void",
|
||||
},
|
||||
bufferDrawGrayscaleBuffer: {
|
||||
@@ -1698,7 +1698,7 @@ function getOpenTUILib(libPath?: string) {
|
||||
returns: "void",
|
||||
},
|
||||
bufferDrawChar: {
|
||||
args: ["u32", "u32", "u32", "u32", "buffer", "buffer", "u32"],
|
||||
args: ["u32", "u32", "i32", "i32", "buffer", "buffer", "u32"],
|
||||
returns: "void",
|
||||
},
|
||||
|
||||
|
||||
@@ -330,6 +330,14 @@ pub const OptimizedBuffer = struct {
|
||||
y >= scissor.y and y < scissor.y + @as(i32, @intCast(scissor.height));
|
||||
}
|
||||
|
||||
/// Reports whether a cell is inside the buffer and the active scissor. The bounds check runs
|
||||
/// first because a u32 coordinate at or above 2^31 does not fit in the signed scissor
|
||||
/// coordinates.
|
||||
fn isCellInBufferAndScissor(self: *const OptimizedBuffer, x: u32, y: u32) bool {
|
||||
if (x >= self.width or y >= self.height) return false;
|
||||
return self.isPointInScissor(@intCast(x), @intCast(y));
|
||||
}
|
||||
|
||||
pub fn isRectInScissor(self: *const OptimizedBuffer, x: i32, y: i32, width: u32, height: u32) bool {
|
||||
const scissor = self.getCurrentScissorRect() orelse return true;
|
||||
|
||||
@@ -652,8 +660,7 @@ pub const OptimizedBuffer = struct {
|
||||
|
||||
/// Validate coordinates and return buffer index, or null if out of bounds / scissor.
|
||||
fn validateAndIndex(self: *OptimizedBuffer, x: u32, y: u32) ?u32 {
|
||||
if (x >= self.width or y >= self.height) return null;
|
||||
if (!self.isPointInScissor(@intCast(x), @intCast(y))) return null;
|
||||
if (!self.isCellInBufferAndScissor(x, y)) return null;
|
||||
return self.coordsToIndex(x, y);
|
||||
}
|
||||
|
||||
@@ -927,7 +934,7 @@ pub const OptimizedBuffer = struct {
|
||||
}
|
||||
|
||||
inline fn setCellWithAlphaBlendingCellWithoutImages(self: *OptimizedBuffer, x: u32, y: u32, cell: Cell) void {
|
||||
if (!self.isPointInScissor(@intCast(x), @intCast(y))) return;
|
||||
if (!self.isCellInBufferAndScissor(x, y)) return;
|
||||
const opacity = self.getCurrentOpacity();
|
||||
if (isFullyTransparent(opacity, cell.fg, cell.bg)) return;
|
||||
if (isFullyOpaque(opacity, cell.fg, cell.bg)) {
|
||||
@@ -946,7 +953,7 @@ pub const OptimizedBuffer = struct {
|
||||
}
|
||||
|
||||
inline fn setVisibleCellWithAlphaBlending(self: *OptimizedBuffer, x: u32, y: u32, cell: Cell, opacity: f32, fully_transparent: bool) void {
|
||||
if (!self.isPointInScissor(@intCast(x), @intCast(y))) return;
|
||||
if (!self.isCellInBufferAndScissor(x, y)) return;
|
||||
if (isFullyOpaque(opacity, cell.fg, cell.bg)) {
|
||||
self.set(x, y, cell);
|
||||
return;
|
||||
@@ -982,7 +989,7 @@ pub const OptimizedBuffer = struct {
|
||||
}
|
||||
|
||||
fn setCellWithAlphaBlendingRawCell(self: *OptimizedBuffer, x: u32, y: u32, cell: Cell) void {
|
||||
if (!self.isPointInScissor(@intCast(x), @intCast(y))) return;
|
||||
if (!self.isCellInBufferAndScissor(x, y)) return;
|
||||
|
||||
const opacity = self.getCurrentOpacity();
|
||||
if (opacity == 0.0) return;
|
||||
@@ -1205,7 +1212,8 @@ pub const OptimizedBuffer = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn fillRectClipped(
|
||||
/// Fills the part of a rectangle at a signed position that is inside the buffer.
|
||||
pub fn fillRectClipped(
|
||||
self: *OptimizedBuffer,
|
||||
x: i32,
|
||||
y: i32,
|
||||
@@ -1213,20 +1221,19 @@ pub const OptimizedBuffer = struct {
|
||||
height: u32,
|
||||
bg: RGBA,
|
||||
) void {
|
||||
if (width == 0 or height == 0) return;
|
||||
// i64 holds any i32 position plus any u32 extent.
|
||||
const start_x = @max(0, @as(i64, x));
|
||||
const start_y = @max(0, @as(i64, y));
|
||||
const end_x = @min(@as(i64, self.width), @as(i64, x) + width);
|
||||
const end_y = @min(@as(i64, self.height), @as(i64, y) + height);
|
||||
|
||||
const startX = @max(0, x);
|
||||
const startY = @max(0, y);
|
||||
const endX = @min(@as(i32, @intCast(self.width)) - 1, x + @as(i32, @intCast(width)) - 1);
|
||||
const endY = @min(@as(i32, @intCast(self.height)) - 1, y + @as(i32, @intCast(height)) - 1);
|
||||
|
||||
if (startX > endX or startY > endY) return;
|
||||
if (start_x >= end_x or start_y >= end_y) return;
|
||||
|
||||
self.fillRect(
|
||||
@intCast(startX),
|
||||
@intCast(startY),
|
||||
@intCast(endX - startX + 1),
|
||||
@intCast(endY - startY + 1),
|
||||
@intCast(start_x),
|
||||
@intCast(start_y),
|
||||
@intCast(end_x - start_x),
|
||||
@intCast(end_y - start_y),
|
||||
bg,
|
||||
);
|
||||
}
|
||||
@@ -1252,9 +1259,24 @@ pub const OptimizedBuffer = struct {
|
||||
bg: ?RGBA,
|
||||
attributes: u32,
|
||||
) BufferError!void {
|
||||
if (x >= self.width or y >= self.height) return;
|
||||
return self.drawTextClipped(text, @intCast(x), @intCast(y), fg, bg, attributes);
|
||||
}
|
||||
|
||||
/// Draws the part of a text at a signed position that is inside the buffer.
|
||||
pub inline fn drawTextClipped(
|
||||
self: *OptimizedBuffer,
|
||||
text: []const u8,
|
||||
x: i32,
|
||||
y: i32,
|
||||
fg: RGBA,
|
||||
bg: ?RGBA,
|
||||
attributes: u32,
|
||||
) BufferError!void {
|
||||
if (y < 0) return;
|
||||
const opacity = self.getCurrentOpacity();
|
||||
if (isFullyTransparent(opacity, fg, bg orelse ansi.rgbColor(0, 0, 0, 0)) and (opacity == 0.0 or self.image_placements.items.len == 0)) return;
|
||||
return self.drawVisibleText(text, x, y, fg, bg, attributes);
|
||||
return self.drawVisibleText(text, x, @intCast(y), fg, bg, attributes);
|
||||
}
|
||||
|
||||
/// Draw one already-segmented grapheme with an authoritative terminal-cell width.
|
||||
@@ -1286,7 +1308,7 @@ pub const OptimizedBuffer = struct {
|
||||
fn drawVisibleText(
|
||||
self: *OptimizedBuffer,
|
||||
text: []const u8,
|
||||
x: u32,
|
||||
x: i32,
|
||||
y: u32,
|
||||
fg: RGBA,
|
||||
bg: ?RGBA,
|
||||
@@ -1310,10 +1332,13 @@ pub const OptimizedBuffer = struct {
|
||||
}
|
||||
if (printable) {
|
||||
const background = bg.?;
|
||||
for (text, 0..) |byte, offset| {
|
||||
const char_x = x + @as(u32, @intCast(offset));
|
||||
// Each printable ASCII byte is one cell, so the bytes left of column 0 are clipped.
|
||||
const clipped_byte_count: usize = if (x < 0) @min(text.len, @abs(x)) else 0;
|
||||
var char_x: u32 = @intCast(@max(x, 0));
|
||||
for (text[clipped_byte_count..]) |byte| {
|
||||
if (char_x >= self.width) break;
|
||||
self.set(char_x, y, makeCell(byte, fg, background, attributes));
|
||||
char_x += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1332,8 +1357,11 @@ pub const OptimizedBuffer = struct {
|
||||
var special_idx: usize = 0;
|
||||
|
||||
text_loop: while (byte_offset < text.len) {
|
||||
const charX = x + advance_cells;
|
||||
if (charX >= self.width) break;
|
||||
const char_x_wide = @as(i64, x) + advance_cells;
|
||||
if (char_x_wide >= self.width) break;
|
||||
const char_x: i32 = @intCast(char_x_wide);
|
||||
// Only a tab is drawn from a column left of 0; it clips each of its cells.
|
||||
const cell_x: u32 = @intCast(@max(char_x, 0));
|
||||
|
||||
const at_special = special_idx < render_clusters.len and render_clusters[special_idx].col_start == col;
|
||||
|
||||
@@ -1354,7 +1382,15 @@ pub const OptimizedBuffer = struct {
|
||||
}
|
||||
|
||||
const is_tab = grapheme_bytes.len == 1 and grapheme_bytes[0] == '\t';
|
||||
if (!is_tab and !self.isPointInScissor(@intCast(charX), @intCast(y))) {
|
||||
const cluster_byte_start = if (at_special) render_clusters[special_idx - 1].byte_start else byte_offset - 1;
|
||||
if (!is_tab and char_x < 0) {
|
||||
// Clip a glyph that starts left of column 0, even a wide glyph that reaches column 0.
|
||||
// Advance as a drawn glyph does, so the visible glyphs keep their columns.
|
||||
advance_cells += utf8.getWidthAt(text, cluster_byte_start, tab_width, self.width_method);
|
||||
col += cluster_width_cols;
|
||||
continue;
|
||||
}
|
||||
if (!is_tab and !self.isPointInScissor(char_x, @intCast(y))) {
|
||||
advance_cells += cluster_width_cols;
|
||||
col += cluster_width_cols;
|
||||
continue;
|
||||
@@ -1363,25 +1399,25 @@ pub const OptimizedBuffer = struct {
|
||||
var bgColor: RGBA = undefined;
|
||||
if (bg) |b| {
|
||||
bgColor = b;
|
||||
} else if (self.get(charX, y)) |existingCell| {
|
||||
} else if (self.get(cell_x, y)) |existingCell| {
|
||||
bgColor = existingCell.bg;
|
||||
} else {
|
||||
bgColor = ansi.rgbColor(0, 0, 0, 255);
|
||||
}
|
||||
|
||||
const cell_width = utf8.getWidthAt(text, if (at_special) render_clusters[special_idx - 1].byte_start else byte_offset - 1, tab_width, self.width_method);
|
||||
const cell_width = utf8.getWidthAt(text, cluster_byte_start, tab_width, self.width_method);
|
||||
if (cell_width == 0) {
|
||||
col += cluster_width_cols;
|
||||
continue;
|
||||
}
|
||||
if (cell_width > 1 and !is_tab) {
|
||||
if (charX + cell_width > self.width) {
|
||||
if (cell_x + cell_width > self.width) {
|
||||
advance_cells += cluster_width_cols;
|
||||
col += cluster_width_cols;
|
||||
continue;
|
||||
}
|
||||
for (1..cell_width) |span_offset| {
|
||||
if (!self.isPointInScissor(@intCast(charX + @as(u32, @intCast(span_offset))), @intCast(y))) {
|
||||
if (!self.isPointInScissor(char_x + @as(i32, @intCast(span_offset)), @intCast(y))) {
|
||||
advance_cells += cluster_width_cols;
|
||||
col += cluster_width_cols;
|
||||
continue :text_loop;
|
||||
@@ -1392,12 +1428,14 @@ pub const OptimizedBuffer = struct {
|
||||
if (is_tab) {
|
||||
var tab_col: u32 = 0;
|
||||
while (tab_col < cluster_width_cols) : (tab_col += 1) {
|
||||
const tab_x = charX + tab_col;
|
||||
const tab_x = @as(i64, char_x) + tab_col;
|
||||
if (tab_x < 0) continue;
|
||||
if (tab_x >= self.width) break;
|
||||
if (!self.isPointInScissor(@intCast(tab_x), @intCast(y))) continue;
|
||||
|
||||
const tab_cell_x: u32 = @intCast(tab_x);
|
||||
const cell = makeCell(DEFAULT_SPACE_CHAR, fg, bgColor, attributes);
|
||||
if (explicit_colors_opaque) self.set(tab_x, y, cell) else self.setTextCell(tab_x, y, cell);
|
||||
if (explicit_colors_opaque) self.set(tab_cell_x, y, cell) else self.setTextCell(tab_cell_x, y, cell);
|
||||
}
|
||||
advance_cells += cluster_width_cols;
|
||||
col += cluster_width_cols;
|
||||
@@ -1413,7 +1451,7 @@ pub const OptimizedBuffer = struct {
|
||||
}
|
||||
|
||||
const cell = makeCell(encoded_char, fg, bgColor, attributes);
|
||||
if (explicit_colors_opaque) self.set(charX, y, cell) else self.setTextCell(charX, y, cell);
|
||||
if (explicit_colors_opaque) self.set(cell_x, y, cell) else self.setTextCell(cell_x, y, cell);
|
||||
|
||||
advance_cells += cell_width;
|
||||
col += cluster_width_cols;
|
||||
@@ -2714,8 +2752,8 @@ pub const OptimizedBuffer = struct {
|
||||
/// alignedBytesPerRow: The number of bytes per row in the pixelData buffer, considering alignment/padding.
|
||||
pub fn drawSuperSampleBuffer(
|
||||
self: *OptimizedBuffer,
|
||||
posX: u32,
|
||||
posY: u32,
|
||||
posX: i32,
|
||||
posY: i32,
|
||||
pixelData: [*]const u8,
|
||||
len: usize,
|
||||
format: u8, // 0: bgra8unorm, 1: rgba8unorm
|
||||
@@ -2726,23 +2764,29 @@ pub const OptimizedBuffer = struct {
|
||||
|
||||
// TODO: A more robust implementation might take source width/height explicitly.
|
||||
|
||||
var y_cell = posY;
|
||||
// Start at the first cell inside the buffer. A negative position clips the source.
|
||||
var y_cell: u32 = @intCast(@max(posY, 0));
|
||||
while (y_cell < self.height) : (y_cell += 1) {
|
||||
var x_cell = posX;
|
||||
var x_cell: u32 = @intCast(@max(posX, 0));
|
||||
while (x_cell < self.width) : (x_cell += 1) {
|
||||
if (!self.isPointInScissor(@intCast(x_cell), @intCast(y_cell))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderX: u32 = (x_cell - posX) * 2;
|
||||
const renderY: u32 = (y_cell - posY) * 2;
|
||||
const renderX: u64 = @intCast((@as(i64, x_cell) - posX) * 2);
|
||||
const renderY: u64 = @intCast((@as(i64, y_cell) - posY) * 2);
|
||||
|
||||
const tlIndex: usize = @intCast(renderY * alignedBytesPerRow + renderX * bytesPerPixel);
|
||||
const trIndex: usize = tlIndex + bytesPerPixel;
|
||||
const blIndex: usize = @intCast((renderY + 1) * alignedBytesPerRow + renderX * bytesPerPixel);
|
||||
const brIndex: usize = blIndex + bytesPerPixel;
|
||||
// A far negative position puts these indices past the pixel data. Saturate and clamp
|
||||
// them to len, which getPixelColor reads as an out-of-bounds pixel.
|
||||
const tlIndex = renderY *| alignedBytesPerRow +| renderX *| bytesPerPixel;
|
||||
const blIndex = (renderY + 1) *| alignedBytesPerRow +| renderX *| bytesPerPixel;
|
||||
|
||||
const indices = [_]usize{ tlIndex, trIndex, blIndex, brIndex };
|
||||
const indices = [_]usize{
|
||||
@intCast(@min(tlIndex, len)),
|
||||
@intCast(@min(tlIndex +| bytesPerPixel, len)),
|
||||
@intCast(@min(blIndex, len)),
|
||||
@intCast(@min(blIndex +| bytesPerPixel, len)),
|
||||
};
|
||||
|
||||
// Get RGBA colors for TL, TR, BL, BR
|
||||
var pixelsRgba: [4]RGBA = undefined;
|
||||
@@ -2772,22 +2816,24 @@ pub const OptimizedBuffer = struct {
|
||||
self: *OptimizedBuffer,
|
||||
data: [*]const u8,
|
||||
dataLen: usize,
|
||||
posX: u32,
|
||||
posY: u32,
|
||||
posX: i32,
|
||||
posY: i32,
|
||||
terminalWidthCells: u32,
|
||||
terminalHeightCells: u32,
|
||||
) void {
|
||||
const cellResultSize = 48;
|
||||
const numCells = dataLen / cellResultSize;
|
||||
const bufferWidthCells = terminalWidthCells;
|
||||
if (bufferWidthCells == 0) return;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < numCells) : (i += 1) {
|
||||
const cellDataOffset = i * cellResultSize;
|
||||
|
||||
const cellX = posX + @as(u32, @intCast(i % bufferWidthCells));
|
||||
const cellY = posY + @as(u32, @intCast(i / bufferWidthCells));
|
||||
const cellX = @as(i64, posX) + @as(i64, @intCast(i % bufferWidthCells));
|
||||
const cellY = @as(i64, posY) + @as(i64, @intCast(i / bufferWidthCells));
|
||||
|
||||
if (cellX < 0 or cellY < 0) continue;
|
||||
if (cellX >= terminalWidthCells or cellY >= terminalHeightCells) continue;
|
||||
if (cellX >= self.width or cellY >= self.height) continue;
|
||||
|
||||
@@ -2810,7 +2856,7 @@ pub const OptimizedBuffer = struct {
|
||||
char = BLOCK_CHAR;
|
||||
}
|
||||
|
||||
self.setCellWithAlphaBlending(cellX, cellY, char, fg, bg, 0);
|
||||
self.setCellWithAlphaBlending(@intCast(cellX), @intCast(cellY), char, fg, bg, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-13
@@ -1797,9 +1797,9 @@ export fn bufferWriteResolvedChars(buffer_handle: NativeHandle, outputPtr: ?[*]u
|
||||
return object_ptr.writeResolvedChars(output_slice, addLineBreaks) catch 0;
|
||||
}
|
||||
|
||||
export fn bufferDrawText(buffer_handle: NativeHandle, text: ?[*]const u8, textLen: u32, x: u32, y: u32, fg: [*]const u16, bg: ?[*]const u16, attributes: u32) void {
|
||||
export fn bufferDrawText(buffer_handle: NativeHandle, text: ?[*]const u8, textLen: u32, x: i32, y: i32, fg: [*]const u16, bg: ?[*]const u16, attributes: u32) void {
|
||||
const object_ptr = acquireBuffer(buffer_handle) orelse return;
|
||||
object_ptr.drawText(
|
||||
object_ptr.drawTextClipped(
|
||||
sliceFromPtrLen(text, textLen),
|
||||
x,
|
||||
y,
|
||||
@@ -1809,14 +1809,16 @@ export fn bufferDrawText(buffer_handle: NativeHandle, text: ?[*]const u8, textLe
|
||||
) catch {};
|
||||
}
|
||||
|
||||
export fn bufferSetCellWithAlphaBlending(buffer_handle: NativeHandle, x: u32, y: u32, char: u32, fg: [*]const u16, bg: [*]const u16, attributes: u32) void {
|
||||
export fn bufferSetCellWithAlphaBlending(buffer_handle: NativeHandle, x: i32, y: i32, char: u32, fg: [*]const u16, bg: [*]const u16, attributes: u32) void {
|
||||
const object_ptr = acquireBuffer(buffer_handle) orelse return;
|
||||
object_ptr.setCellWithAlphaBlending(x, y, char, ptrToRGBA(fg), ptrToRGBA(bg), attributes);
|
||||
if (x < 0 or y < 0) return;
|
||||
object_ptr.setCellWithAlphaBlending(@intCast(x), @intCast(y), char, ptrToRGBA(fg), ptrToRGBA(bg), attributes);
|
||||
}
|
||||
|
||||
export fn bufferSetCell(buffer_handle: NativeHandle, x: u32, y: u32, char: u32, fg: [*]const u16, bg: [*]const u16, attributes: u32) void {
|
||||
export fn bufferSetCell(buffer_handle: NativeHandle, x: i32, y: i32, char: u32, fg: [*]const u16, bg: [*]const u16, attributes: u32) void {
|
||||
const object_ptr = acquireBuffer(buffer_handle) orelse return;
|
||||
object_ptr.set(x, y, .{
|
||||
if (x < 0 or y < 0) return;
|
||||
object_ptr.set(@intCast(x), @intCast(y), .{
|
||||
.char = char,
|
||||
.fg = ptrToRGBA(fg),
|
||||
.bg = ptrToRGBA(bg),
|
||||
@@ -1824,9 +1826,9 @@ export fn bufferSetCell(buffer_handle: NativeHandle, x: u32, y: u32, char: u32,
|
||||
});
|
||||
}
|
||||
|
||||
export fn bufferFillRect(buffer_handle: NativeHandle, x: u32, y: u32, width: u32, height: u32, bg: [*]const u16) void {
|
||||
export fn bufferFillRect(buffer_handle: NativeHandle, x: i32, y: i32, width: u32, height: u32, bg: [*]const u16) void {
|
||||
const object_ptr = acquireBuffer(buffer_handle) orelse return;
|
||||
object_ptr.fillRect(x, y, width, height, ptrToRGBA(bg));
|
||||
object_ptr.fillRectClipped(x, y, width, height, ptrToRGBA(bg));
|
||||
}
|
||||
|
||||
export fn bufferColorMatrix(buffer_handle: NativeHandle, matrixPtr: [*]const f32, cellMaskPtr: [*]const f32, cellMaskCount: u32, strength: f32, target: u8) void {
|
||||
@@ -1846,7 +1848,7 @@ export fn bufferColorMatrixUniform(buffer_handle: NativeHandle, matrixPtr: [*]co
|
||||
buffer_effects.colorMatrixUniform(object_ptr, matrix, strength, targetEnum);
|
||||
}
|
||||
|
||||
export fn bufferDrawPackedBuffer(buffer_handle: NativeHandle, data: [*]const u8, dataLen: u32, posX: u32, posY: u32, terminalWidthCells: u32, terminalHeightCells: u32) void {
|
||||
export fn bufferDrawPackedBuffer(buffer_handle: NativeHandle, data: [*]const u8, dataLen: u32, posX: i32, posY: i32, terminalWidthCells: u32, terminalHeightCells: u32) void {
|
||||
const object_ptr = acquireBuffer(buffer_handle) orelse return;
|
||||
object_ptr.drawPackedBuffer(data, dataLen, posX, posY, terminalWidthCells, terminalHeightCells);
|
||||
}
|
||||
@@ -1913,7 +1915,7 @@ export fn bufferClearOpacity(buffer_handle: NativeHandle) void {
|
||||
object_ptr.clearOpacity();
|
||||
}
|
||||
|
||||
export fn bufferDrawSuperSampleBuffer(buffer_handle: NativeHandle, x: u32, y: u32, pixelData: [*]const u8, len: u32, format: u8, alignedBytesPerRow: u32) void {
|
||||
export fn bufferDrawSuperSampleBuffer(buffer_handle: NativeHandle, x: i32, y: i32, pixelData: [*]const u8, len: u32, format: u8, alignedBytesPerRow: u32) void {
|
||||
const object_ptr = acquireBuffer(buffer_handle) orelse return;
|
||||
object_ptr.drawSuperSampleBuffer(x, y, pixelData, len, format, alignedBytesPerRow);
|
||||
}
|
||||
@@ -3992,12 +3994,13 @@ export fn freeUnicode(charsPtr: ?[*]const EncodedChar, charsLen: u32) void {
|
||||
export fn bufferDrawChar(
|
||||
buffer_handle: NativeHandle,
|
||||
char: u32,
|
||||
x: u32,
|
||||
y: u32,
|
||||
x: i32,
|
||||
y: i32,
|
||||
fg: [*]const u16,
|
||||
bg: [*]const u16,
|
||||
attributes: u32,
|
||||
) void {
|
||||
const object_ptr = acquireBuffer(buffer_handle) orelse return;
|
||||
object_ptr.drawChar(char, x, y, ptrToRGBA(fg), ptrToRGBA(bg), attributes);
|
||||
if (x < 0 or y < 0) return;
|
||||
object_ptr.drawChar(char, @intCast(x), @intCast(y), ptrToRGBA(fg), ptrToRGBA(bg), attributes);
|
||||
}
|
||||
@@ -773,6 +773,131 @@ test "OptimizedBuffer - alpha blending downgrades blended metadata to rgb" {
|
||||
try std.testing.expectEqual(ansi.ColorIntent.rgb, ansi.intent(bg_blended_cell.bg));
|
||||
}
|
||||
|
||||
test "OptimizedBuffer - blending cell setters skip coordinates outside the buffer" {
|
||||
const pool = gp.initGlobalPool(std.testing.allocator);
|
||||
defer gp.deinitGlobalPool();
|
||||
|
||||
var buf = try OptimizedBuffer.init(std.testing.allocator, 2, 2, .{ .pool = pool, .id = "outside-cell-setters" });
|
||||
defer buf.deinit();
|
||||
|
||||
const base_bg = ansi.rgbaFromFloats(0.0, 0.0, 0.0, 1.0);
|
||||
buf.clear(base_bg, null);
|
||||
const opaque_fg = ansi.rgbaFromFloats(1.0, 1.0, 1.0, 1.0);
|
||||
const translucent_bg = ansi.rgbaFromFloats(1.0, 0.0, 0.0, 0.5);
|
||||
|
||||
// A u32 coordinate at or above 2^31 has no i32 value for the scissor check.
|
||||
const outside = [_]u32{ 2, std.math.maxInt(i32) + 1, std.math.maxInt(u32) };
|
||||
for ([_]bool{ false, true }) |scissored| {
|
||||
if (scissored) try buf.pushScissorRect(0, 0, 2, 2);
|
||||
for (outside) |coordinate| {
|
||||
const points = [_][2]u32{ .{ coordinate, 0 }, .{ 0, coordinate } };
|
||||
for (points) |point| {
|
||||
buf.drawChar('X', point[0], point[1], opaque_fg, base_bg, 0);
|
||||
buf.drawChar('X', point[0], point[1], opaque_fg, translucent_bg, 0);
|
||||
buf.setCellWithAlphaBlending(point[0], point[1], 'X', opaque_fg, translucent_bg, 0);
|
||||
buf.setCellWithAlphaBlendingRaw(point[0], point[1], 'X', opaque_fg, translucent_bg, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (0..2) |y| {
|
||||
for (0..2) |x| {
|
||||
try std.testing.expectEqual(buffer_mod.DEFAULT_SPACE_CHAR, buf.get(@intCast(x), @intCast(y)).?.char);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expectRowChars(buf: *OptimizedBuffer, y: u32, expected: []const u8) !void {
|
||||
for (expected, 0..) |char, x| {
|
||||
try std.testing.expectEqual(@as(u32, char), buf.get(@intCast(x), y).?.char);
|
||||
}
|
||||
}
|
||||
|
||||
test "OptimizedBuffer - text and rectangles at signed positions draw their visible part" {
|
||||
const pool = gp.initGlobalPool(std.testing.allocator);
|
||||
defer gp.deinitGlobalPool();
|
||||
|
||||
var buf = try OptimizedBuffer.init(std.testing.allocator, 4, 5, .{ .pool = pool, .id = "signed-draw-positions" });
|
||||
defer buf.deinit();
|
||||
|
||||
const black = ansi.rgbaFromFloats(0.0, 0.0, 0.0, 1.0);
|
||||
const white = ansi.rgbaFromFloats(1.0, 1.0, 1.0, 1.0);
|
||||
const red = ansi.rgbaFromFloats(1.0, 0.0, 0.0, 1.0);
|
||||
const min = std.math.minInt(i32);
|
||||
buf.clear(black, null);
|
||||
|
||||
// Opaque printable ASCII uses the byte fast path. Text without a background uses the cluster path.
|
||||
try buf.drawTextClipped("ABCDE", -2, 0, white, black, 0);
|
||||
// The wide glyph covers columns -1 and 0, so it is clipped and column 0 keeps its cell.
|
||||
try buf.drawTextClipped("A世BC", -2, 1, white, null, 0);
|
||||
try buf.drawTextClipped("\tX", -1, 2, white, red, 0);
|
||||
try buf.drawTextClipped("e\u{301}XY", -1, 3, white, null, 0);
|
||||
try buf.pushScissorRect(1, 4, 2, 1);
|
||||
try buf.drawTextClipped("WXYZ", -1, 4, white, null, 0);
|
||||
buf.popScissorRect();
|
||||
// These draws have no cell inside the buffer.
|
||||
try buf.drawTextClipped("ZZZZ", 0, -1, white, black, 0);
|
||||
try buf.drawTextClipped("ZZZZ", min, 0, white, black, 0);
|
||||
try buf.drawTextClipped("Z世ZZ", min, 0, white, null, 0);
|
||||
|
||||
try expectRowChars(buf, 0, "CDE ");
|
||||
try expectRowChars(buf, 1, " BC ");
|
||||
try expectRowChars(buf, 2, " X ");
|
||||
try std.testing.expect(buffer_mod.rgbaEqual(red, buf.get(0, 2).?.bg));
|
||||
try expectRowChars(buf, 3, "XY ");
|
||||
try expectRowChars(buf, 4, " YZ ");
|
||||
|
||||
buf.fillRectClipped(-1, -1, 2, 2, red);
|
||||
buf.fillRectClipped(std.math.maxInt(i32), min, std.math.maxInt(u32), std.math.maxInt(u32), white);
|
||||
buf.fillRectClipped(min, 4, std.math.maxInt(u32), 1, red);
|
||||
|
||||
try std.testing.expect(buffer_mod.rgbaEqual(red, buf.get(0, 0).?.bg));
|
||||
try std.testing.expect(buffer_mod.rgbaEqual(black, buf.get(1, 0).?.bg));
|
||||
try std.testing.expect(buffer_mod.rgbaEqual(black, buf.get(0, 1).?.bg));
|
||||
try std.testing.expect(buffer_mod.rgbaEqual(red, buf.get(3, 4).?.bg));
|
||||
}
|
||||
|
||||
test "OptimizedBuffer - pixel buffers at signed positions draw their visible part" {
|
||||
const pool = gp.initGlobalPool(std.testing.allocator);
|
||||
defer gp.deinitGlobalPool();
|
||||
|
||||
var buf = try OptimizedBuffer.init(std.testing.allocator, 2, 1, .{ .pool = pool, .id = "signed-pixel-buffers" });
|
||||
defer buf.deinit();
|
||||
const black = ansi.rgbaFromFloats(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
// Two cells of 2x2 RGBA pixels: a red cell, then a blue cell.
|
||||
const red = [4]u8{ 255, 0, 0, 255 };
|
||||
const blue = [4]u8{ 0, 0, 255, 255 };
|
||||
const pixels = red ++ red ++ blue ++ blue ++ red ++ red ++ blue ++ blue;
|
||||
|
||||
buf.drawSuperSampleBuffer(0, 0, &pixels, pixels.len, 1, 16);
|
||||
const blue_cell = buf.get(1, 0).?;
|
||||
buf.clear(black, null);
|
||||
// The visible cells of the first two draws map past the end of the pixels.
|
||||
buf.drawSuperSampleBuffer(std.math.minInt(i32), 0, &pixels, pixels.len, 1, std.math.maxInt(u32));
|
||||
buf.drawSuperSampleBuffer(0, std.math.minInt(i32), &pixels, pixels.len, 1, std.math.maxInt(u32));
|
||||
buf.drawSuperSampleBuffer(-1, 0, &pixels, pixels.len, 1, 16);
|
||||
|
||||
const cell = buf.get(0, 0).?;
|
||||
try std.testing.expectEqual(blue_cell.char, cell.char);
|
||||
try std.testing.expect(buffer_mod.rgbaEqual(blue_cell.fg, cell.fg));
|
||||
try std.testing.expect(buffer_mod.rgbaEqual(blue_cell.bg, cell.bg));
|
||||
|
||||
// Each packed cell is bg(4 f32), fg(4 f32), char(u32), and 12 padding bytes.
|
||||
const packed_cells = [2][12]f32{
|
||||
.{ 0, 0, 0, 1, 1, 1, 1, 1, @bitCast(@as(u32, 'A')), 0, 0, 0 },
|
||||
.{ 0, 0, 0, 1, 1, 1, 1, 1, @bitCast(@as(u32, 'B')), 0, 0, 0 },
|
||||
};
|
||||
const packed_bytes = std.mem.sliceAsBytes(&packed_cells);
|
||||
|
||||
buf.clear(black, null);
|
||||
buf.drawPackedBuffer(packed_bytes.ptr, packed_bytes.len, -1, 0, 2, 1);
|
||||
buf.drawPackedBuffer(packed_bytes.ptr, packed_bytes.len, 0, -1, 2, 1);
|
||||
buf.drawPackedBuffer(packed_bytes.ptr, packed_bytes.len, 0, 0, 0, 1);
|
||||
|
||||
try expectRowChars(buf, 0, "B ");
|
||||
}
|
||||
|
||||
test "OptimizedBuffer - transparent framebuffer cell background stays transparent over backdrop" {
|
||||
const pool = gp.initGlobalPool(std.testing.allocator);
|
||||
defer gp.deinitGlobalPool();
|
||||
|
||||
@@ -117,6 +117,10 @@ draw the tagged values returned in `data` and maintain their continuation cells.
|
||||
| `drawText` | `drawText(text, x, y, fg, bg?, attributes = 0, selection?)` segments and clips Unicode text |
|
||||
| `fillRect` | `fillRect(x, y, width, height, bg)` paints the rectangle through tracker-aware cell paths |
|
||||
|
||||
Positions are integer cells from `-2^31` to `2^31 - 1`, so they can be negative. These methods draw only the cells
|
||||
inside the buffer. `drawText()` skips a wide grapheme that starts left of column 0. `fillRect()` draws nothing when
|
||||
`width` or `height` is 0 or less.
|
||||
|
||||
The optional `drawText()` selection object has `{ start, end, bgColor?, fgColor? }`. Its implementation slices the
|
||||
JavaScript string with UTF-16 indexes and adds the same values to the cell x-coordinate. It is reliable only for
|
||||
one-cell Basic Multilingual Plane text. Use `TextBufferView` or `EditorView` selection for general Unicode text.
|
||||
|
||||
Reference in new issue
Block a user