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); diff --git a/test/width.test.ts b/test/width.test.ts new file mode 100644 index 0000000..22991d2 --- /dev/null +++ b/test/width.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 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"); + }); +});