Skip to content

Commit e21e1e6

Browse files
committed
fix grapheme cluster width
as pointed out in #125, the width of a grapheme cluster is not the sum of the wcwidth of its codepoints. there's no standard way of determining the column width of a grapheme cluster, but the implementation in this commit is a decent real world approximation. basically, it returns the max wcwidth in the cluster, and tries to account for vs15, vs16, zero-width joiners, and regional indicators.
1 parent ed68bcc commit e21e1e6

1 file changed

Lines changed: 31 additions & 23 deletions

File tree

termbox2.h

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -406,18 +406,18 @@ typedef uint16_t uintattr_t;
406406
* `nech`, and `cech` via `tb_set_cell_ex`. `ech` is only valid when `nech>0`,
407407
* otherwise `ch` is used.
408408
*
409-
* For non-single-width codepoints, given `N=wcwidth(ch)/wcswidth(ech)`:
409+
* For non-single-width codepoints, given `W=wcwidth(ch || ech)`:
410410
*
411-
* when `N==0`: termbox forces a single-width cell. Callers should avoid this
411+
* when `W<=0`: termbox forces a single-width cell. Callers should avoid this
412412
* if aiming to render text accurately. Callers may use
413-
* `tb_set_cell_ex` or `tb_print*` to render `N==0` combining
413+
* `tb_set_cell_ex` or `tb_print*` to render `W==0` combining
414414
* characters.
415415
*
416-
* when `N>1`: termbox zeroes out the following `N-1` cells and skips sending
417-
* them to the tty. So, e.g., if the caller sets `x=0,y=0` to an
418-
* `N==2` codepoint, the caller's next set should be at `x=2,y=0`.
416+
* when `W>=2`: termbox zeroes out the following `W-1` cells and skips sending
417+
* them to the tty. So, e.g., if the caller sets `x=0,y=0` to a
418+
* `W==2` codepoint, the caller's next set should be at `x=2,y=0`.
419419
* Anything set at `x=1,y=0` will be ignored. If there are not
420-
* enough columns remaining on the line to render `N` width, spaces
420+
* enough columns remaining on the line to render `W` cells, spaces
421421
* are sent instead.
422422
*
423423
* See `tb_present` for implementation.
@@ -2329,7 +2329,7 @@ static int bytebuf_flush(struct bytebuf *b, int fd);
23292329
static int bytebuf_reserve(struct bytebuf *b, size_t sz);
23302330
static int bytebuf_free(struct bytebuf *b);
23312331
static int tb_iswprint_ex(uint32_t ch, int *width);
2332-
static int tb_wcswidth(uint32_t *ch, size_t nch);
2332+
static int tb_cluster_width(uint32_t *ch, size_t nch);
23332333

23342334
int tb_init(void) {
23352335
return tb_init_file("/dev/tty");
@@ -2424,12 +2424,12 @@ int tb_present(void) {
24242424
{
24252425
#ifdef TB_OPT_EGC
24262426
if (back->nech > 0)
2427-
w = tb_wcswidth(back->ech, back->nech);
2427+
w = tb_cluster_width(back->ech, back->nech);
24282428
else
24292429
#endif
24302430
w = tb_wcwidth((wchar_t)back->ch);
24312431
}
2432-
if (w < 1) w = 1; // wcwidth qreturns -1 for invalid codepoints
2432+
if (w < 1) w = 1; // wcwidth returns -1 for invalid codepoints
24332433

24342434
if (cell_cmp(back, front) != 0) {
24352435
cell_copy(front, back);
@@ -4237,27 +4237,35 @@ int tb_iswprint(uint32_t ch) {
42374237
}
42384238

42394239
int tb_wcwidth(uint32_t ch) {
4240+
int w;
42404241
#ifdef TB_OPT_LIBC_WCHAR
4241-
return wcwidth((wchar_t)ch);
4242+
w = wcwidth((wchar_t)ch);
42424243
#else
4243-
return tb_wcswidth(&ch, 1);
4244+
tb_iswprint_ex(ch, &w);
42444245
#endif
4246+
return w;
42454247
}
42464248

4247-
static int tb_wcswidth(uint32_t *ch, size_t nch) {
4248-
#ifdef TB_OPT_LIBC_WCHAR
4249-
return wcswidth((wchar_t *)ch, nch);
4250-
#else
4251-
int sw = 0;
4249+
static int tb_cluster_width(uint32_t *ch, size_t nch) {
4250+
int wmax = -1;
4251+
int vs15 = 0, vs16 = 0, ri = 0, zwj = 0;
42524252
size_t i = 0;
42534253
for (i = 0; i < nch; i++) {
4254-
int w;
4255-
tb_iswprint_ex(ch[i], &w);
4256-
if (w < 0) return -1;
4257-
sw += w;
4254+
uint32_t c = ch[i];
4255+
switch (c) {
4256+
case 0xfe0e: ++vs15; break;
4257+
case 0xfe0f: ++vs16; break;
4258+
case 0x200d: ++zwj; break;
4259+
default: if (c >= 0x1f1e6 && c <= 0x1f1ff) ++ri;
4260+
}
4261+
int w = tb_wcwidth(c);
4262+
if (w > wmax) wmax = w;
42584263
}
4259-
return sw;
4260-
#endif
4264+
if (wmax >= 1) {
4265+
if (vs15) return 1;
4266+
else if (vs16 || zwj || ri >= 2) return 2;
4267+
}
4268+
return wmax;
42614269
}
42624270

42634271
static int tb_iswprint_ex(uint32_t ch, int *w) {

0 commit comments

Comments
 (0)