From 352cb27b450ebd16ef273ac062871b7f274e3510 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 4 Jun 2026 00:33:11 -0500 Subject: [PATCH 1/3] test: failing repro for wide-char-overlay --- test/wide-char-overlay.test.ts | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 test/wide-char-overlay.test.ts diff --git a/test/wide-char-overlay.test.ts b/test/wide-char-overlay.test.ts new file mode 100644 index 0000000..109db0f --- /dev/null +++ b/test/wide-char-overlay.test.ts @@ -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-char trailing overlay", () => { + 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, + ); + // pins: 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, + ); + // pins: the overlay glyph reaches the byte stream rather than being swallowed by the column skip. + expect(ansi).toContain("X"); + }); +}); From 7ebeb93bf6446f33fc794340e493c42cf101be99 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 5 Jun 2026 01:11:14 -0500 Subject: [PATCH 2/3] fix: collapse orphaned wide-char lead on trailing-column overlay When a narrow glyph is floated onto the trailing column of an already-placed wide char, the present loops skipped the trailing column and the overlay was dropped (the wide char painted over both cells). Detect the partial overwrite (non-space trailing cell) and collapse the orphaned lead to a space at width 1 so the overlay glyph emits. Applied symmetrically to present_cups and present_lines; rows without overlapping writes stay byte-for-byte identical. Fixes #75 --- src/clayterm.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/clayterm.c b/src/clayterm.c index 2af9afd..a1fe110 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -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; @@ -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); From c12cd67337936beb3c3aecdba6c14a5f38f572e2 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 5 Jun 2026 23:41:26 -0500 Subject: [PATCH 3/3] test: group wide-char regression into test/width.test.ts --- test/{wide-char-overlay.test.ts => width.test.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename test/{wide-char-overlay.test.ts => width.test.ts} (85%) diff --git a/test/wide-char-overlay.test.ts b/test/width.test.ts similarity index 85% rename from test/wide-char-overlay.test.ts rename to test/width.test.ts index 109db0f..22991d2 100644 --- a/test/wide-char-overlay.test.ts +++ b/test/width.test.ts @@ -5,7 +5,7 @@ import { print } from "./print.ts"; const decode = (b: Uint8Array) => new TextDecoder().decode(b); -describe("wide-char trailing overlay", () => { +describe("wide characters", () => { let term: Term; beforeEach(async () => { term = await createTerm({ width: 12, height: 1 }); @@ -30,7 +30,7 @@ describe("wide-char trailing overlay", () => { 12, 1, ); - // pins: half-overwritten 你 collapses to a space, X lands at col 1, 好 untouched at 2-3. + // half-overwritten 你 collapses to a space, X lands at col 1, 好 untouched at 2-3. expect(out).toBe(" X好 "); }); @@ -48,7 +48,7 @@ describe("wide-char trailing overlay", () => { close(), ]).output, ); - // pins: the overlay glyph reaches the byte stream rather than being swallowed by the column skip. + // the overlay glyph reaches the byte stream rather than being swallowed by the column skip. expect(ansi).toContain("X"); }); });