Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/clayterm.c
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,21 @@ static void present_cups(struct Clayterm *ct, int row) {
if (w < 1)
w = 1;

/* If a narrow glyph was floated onto a trailing column of this wide
* char, render_text left the lead cell but the trailing cell now
* holds a non-space glyph. Collapse the orphaned lead to a space and
* treat it as width-1 so the overlay glyph emits on the next pass. */
if (w > 1) {
for (int i = 1; i < w && x + i < ct->w; i++) {
Cell *bw = cell_at(ct, ct->back, x + i, y);
if (bw->ch != ' ') {
back->ch = ' ';
w = 1;
break;
}
}
}

if (cell_cmp(back, front)) {
/* copy to front */
*front = *back;
Expand Down Expand Up @@ -222,6 +237,21 @@ static void present_lines(struct Clayterm *ct) {
if (w < 1)
w = 1;

/* If a narrow glyph was floated onto a trailing column of this wide
* char, render_text left the lead cell but the trailing cell now
* holds a non-space glyph. Collapse the orphaned lead to a space and
* treat it as width-1 so the overlay glyph emits on the next pass. */
if (w > 1) {
for (int i = 1; i < w && x + i < ct->w; i++) {
Cell *bw = cell_at(ct, ct->back, x + i, y);
if (bw->ch != ' ') {
back->ch = ' ';
w = 1;
break;
}
}
}

*front = *back;

emit_attr(ct, back->fg, back->bg);
Expand Down
54 changes: 54 additions & 0 deletions test/width.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it } from "./suite.ts";
import { createTerm, type Term } from "../term.ts";
import { close, fixed, grow, open, text } from "../ops.ts";
import { print } from "./print.ts";

const decode = (b: Uint8Array) => new TextDecoder().decode(b);

describe("wide characters", () => {
let term: Term;
beforeEach(async () => {
term = await createTerm({ width: 12, height: 1 });
});

it("overlay on a wide char's trailing column clears the orphaned lead to a space", () => {
// X is floated onto col 1, the trailing column of 你 (cols 0-1).
let out = print(
decode(
term.render([
open("root", { layout: { width: grow(), height: grow() } }),
text("你好"),
open("ov", {
floating: { x: 1, y: 0, attachTo: 3 },
layout: { width: fixed(1), height: fixed(1) },
}),
text("X"),
close(),
close(),
]).output,
),
12,
1,
);
// half-overwritten 你 collapses to a space, X lands at col 1, 好 untouched at 2-3.
expect(out).toBe(" X好 ");
});

it("emits an explicit byte for an overlay landing on a placeholder column", () => {
let ansi = decode(
term.render([
open("root", { layout: { width: grow(), height: grow() } }),
text("你好"),
open("ov", {
floating: { x: 1, y: 0, attachTo: 3 },
layout: { width: fixed(1), height: fixed(1) },
}),
text("X"),
close(),
close(),
]).output,
);
// the overlay glyph reaches the byte stream rather than being swallowed by the column skip.
expect(ansi).toContain("X");
});
});
Loading