skip to content
==> src/width.c <==
1/* unicode-width 0.2.2's str_width and lookup_width, ported: the width of each character depends on
2 * the one after it (WidthInfo), so the string is walked backwards, as the crate's rfold does. */
3#include "tui/unicode_width.h"
4
5#include "uw_tables.h"
6#include "tui/utf8.h"
7
8#include <stdbool.h>
9#include <stdlib.h>
10
11typedef uint16_t info;
12
13enum {
14    LIGATURE_TRANSPARENT_MASK = 0x2000,
15};
16
17#define I_DEFAULT 0x0000
18#define I_LINE_FEED 0x0001
19#define I_EMOJI_MODIFIER 0x0002
20#define I_REGIONAL_INDICATOR 0x0003
21#define I_SEVERAL_REGIONAL_INDICATOR 0x0004
22#define I_EMOJI_PRESENTATION 0x0005
23#define I_ZWJ_EMOJI_PRESENTATION 0x1006
24#define I_VS16_ZWJ_EMOJI_PRESENTATION 0x9006
25#define I_KEYCAP_ZWJ_EMOJI_PRESENTATION 0x1007
26#define I_VS16_KEYCAP_ZWJ_EMOJI_PRESENTATION 0x9007
27#define I_REGIONAL_INDICATOR_ZWJ_PRESENTATION 0x0009
28#define I_EVEN_REGIONAL_INDICATOR_ZWJ_PRESENTATION 0x000A
29#define I_ODD_REGIONAL_INDICATOR_ZWJ_PRESENTATION 0x000B
30#define I_TAG_END_ZWJ_EMOJI_PRESENTATION 0x0010
31#define I_TAG_D1_END_ZWJ_EMOJI_PRESENTATION 0x0011
32#define I_TAG_D2_END_ZWJ_EMOJI_PRESENTATION 0x0012
33#define I_TAG_D3_END_ZWJ_EMOJI_PRESENTATION 0x0013
34#define I_TAG_A1_END_ZWJ_EMOJI_PRESENTATION 0x0019
35#define I_TAG_A2_END_ZWJ_EMOJI_PRESENTATION 0x001A
36#define I_TAG_A3_END_ZWJ_EMOJI_PRESENTATION 0x001B
37#define I_TAG_A4_END_ZWJ_EMOJI_PRESENTATION 0x001C
38#define I_TAG_A5_END_ZWJ_EMOJI_PRESENTATION 0x001D
39#define I_TAG_A6_END_ZWJ_EMOJI_PRESENTATION 0x001E
40#define I_KIRAT_RAI_VOWEL_SIGN_E 0x0020
41#define I_KIRAT_RAI_VOWEL_SIGN_AI 0x0021
42#define I_VARIATION_SELECTOR_1_2_OR_3 0x0200
43#define I_VARIATION_SELECTOR_15 0x4000
44#define I_VARIATION_SELECTOR_16 0x8000
45#define I_JOINING_GROUP_ALEF 0x30FF
46#define I_HEBREW_LETTER_LAMED 0x3800
47#define I_ZWJ_HEBREW_LETTER_LAMED 0x3C00
48#define I_BUGINESE_LETTER_YA 0x3801
49#define I_ZWJ_BUGINESE_LETTER_YA 0x3C01
50#define I_BUGINESE_VOWEL_SIGN_I_ZWJ_LETTER_YA 0x3C02
51#define I_TIFINAGH_CONSONANT 0x3803
52#define I_ZWJ_TIFINAGH_CONSONANT 0x3C03
53#define I_TIFINAGH_JOINER_CONSONANT 0x3C04
54#define I_LISU_TONE_LETTER_MYA_NA_JEU 0x3C05
55#define I_OLD_TURKIC_LETTER_ORKHON_I 0x3806
56#define I_ZWJ_OLD_TURKIC_LETTER_ORKHON_I 0x3C06
57#define I_KHMER_COENG_ELIGIBLE_LETTER 0x3C07
58
59static bool is_ligature_transparent_info(info i) { return (i & 0x0800) == 0x0800; }
60static info set_zwj_bit(info i) { return (info)(i | 0x0400); }
61static bool is_emoji_presentation(info i) { return (i & I_VARIATION_SELECTOR_16) == I_VARIATION_SELECTOR_16; }
62static bool is_zwj_emoji_presentation(info i) { return (i & 0xB000) == 0x9000; }
63static info set_emoji_presentation(info i) {
64    if ((i & LIGATURE_TRANSPARENT_MASK) == LIGATURE_TRANSPARENT_MASK || (i & 0x9000) == 0x1000)
65        return (info)(i | (I_VARIATION_SELECTOR_16 & ~I_VARIATION_SELECTOR_15 & ~I_VARIATION_SELECTOR_1_2_OR_3));
66    return I_VARIATION_SELECTOR_16;
67}
68static info unset_emoji_presentation(info i) {
69    if ((i & LIGATURE_TRANSPARENT_MASK) == LIGATURE_TRANSPARENT_MASK) return (info)(i & ~I_VARIATION_SELECTOR_16);
70    return I_DEFAULT;
71}
72static bool is_text_presentation(info i) { return (i & I_VARIATION_SELECTOR_15) == I_VARIATION_SELECTOR_15; }
73static info set_text_presentation(info i) {
74    if ((i & LIGATURE_TRANSPARENT_MASK) == LIGATURE_TRANSPARENT_MASK)
75        return (info)(i | (I_VARIATION_SELECTOR_15 & ~I_VARIATION_SELECTOR_16 & ~I_VARIATION_SELECTOR_1_2_OR_3));
76    return I_VARIATION_SELECTOR_15;
77}
78static info unset_text_presentation(info i) { return (info)(i & ~I_VARIATION_SELECTOR_15); }
79static bool is_vs1_2_3(info i) { return (i & I_VARIATION_SELECTOR_1_2_OR_3) == I_VARIATION_SELECTOR_1_2_OR_3; }
80static info set_vs1_2_3(info i) {
81    if ((i & LIGATURE_TRANSPARENT_MASK) == LIGATURE_TRANSPARENT_MASK)
82        return (info)(i | (I_VARIATION_SELECTOR_1_2_OR_3 & ~I_VARIATION_SELECTOR_15 & ~I_VARIATION_SELECTOR_16));
83    return I_VARIATION_SELECTOR_1_2_OR_3;
84}
85static info unset_vs1_2_3(info i) { return (info)(i & ~I_VARIATION_SELECTOR_1_2_OR_3); }
86
87typedef struct {
88    uint8_t width;
89    info info;
90} lookup;
91
92static lookup lookup_width(uint32_t cp) {
93    uint8_t t1 = TUI_UW_WIDTH_ROOT[cp >> 13];
94    uint8_t t2 = TUI_UW_WIDTH_MIDDLE[t1][cp >> 7 & 0x3F];
95    uint8_t packed = TUI_UW_WIDTH_LEAVES[t2][cp >> 2 & 0x1F];
96    uint8_t width = packed >> (2 * (cp & 3)) & 3;
97    if (width < 3) return (lookup){width, I_DEFAULT};
98    if (cp == 0xA) return (lookup){1, I_LINE_FEED};
99    if (cp == 0x5DC) return (lookup){1, I_HEBREW_LETTER_LAMED};
100    if (cp >= 0x622 && cp <= 0x882) return (lookup){1, I_JOINING_GROUP_ALEF};
101    if (cp >= 0x1780 && cp <= 0x17AF) return (lookup){1, I_KHMER_COENG_ELIGIBLE_LETTER};
102    if (cp == 0x17D8) return (lookup){3, I_DEFAULT};
103    if (cp == 0x1A10) return (lookup){1, I_BUGINESE_LETTER_YA};
104    if (cp >= 0x2D31 && cp <= 0x2D6F) return (lookup){1, I_TIFINAGH_CONSONANT};
105    if (cp >= 0xA4FC && cp <= 0xA4FD) return (lookup){1, I_LISU_TONE_LETTER_MYA_NA_JEU};
106    if (cp == 0xFE01) return (lookup){0, I_VARIATION_SELECTOR_1_2_OR_3};
107    if (cp == 0xFE0E) return (lookup){0, I_VARIATION_SELECTOR_15};
108    if (cp == 0xFE0F) return (lookup){0, I_VARIATION_SELECTOR_16};
109    if (cp == 0x10C03) return (lookup){1, I_OLD_TURKIC_LETTER_ORKHON_I};
110    if (cp == 0x16D67) return (lookup){1, I_KIRAT_RAI_VOWEL_SIGN_E};
111    if (cp == 0x16D68) return (lookup){1, I_KIRAT_RAI_VOWEL_SIGN_AI};
112    if (cp >= 0x1F1E6 && cp <= 0x1F1FF) return (lookup){1, I_REGIONAL_INDICATOR};
113    if (cp >= 0x1F3FB && cp <= 0x1F3FF) return (lookup){2, I_EMOJI_MODIFIER};
114    return (lookup){2, I_EMOJI_PRESENTATION};
115}
116
117int tui_char_width(uint32_t cp) {
118    if (cp < 0x7F) return cp >= 0x20 ? 1 : -1;
119    if (cp >= 0xA0) return lookup_width(cp).width;
120    return -1;
121}
122
123static bool is_transparent_zero_width(uint32_t cp) {
124    if (lookup_width(cp).width != 0) return false;
125    size_t lo = 0, hi = sizeof TUI_UW_NON_TRANSPARENT_ZERO_WIDTHS / sizeof TUI_UW_NON_TRANSPARENT_ZERO_WIDTHS[0];
126    while (lo < hi) {
127        size_t mid = (lo + hi) / 2;
128        if (cp < TUI_UW_NON_TRANSPARENT_ZERO_WIDTHS[mid][0]) hi = mid;
129        else if (cp > TUI_UW_NON_TRANSPARENT_ZERO_WIDTHS[mid][1]) lo = mid + 1;
130        else return false; /* found: not transparent */
131    }
132    return true;
133}
134
135static bool is_ligature_transparent(uint32_t c) {
136    return c == 0x34F || (c >= 0x17B4 && c <= 0x17B5) || (c >= 0x180B && c <= 0x180D) || c == 0x180F ||
137           c == 0x200D || (c >= 0xFE00 && c <= 0xFE0F) || (c >= 0xE0100 && c <= 0xE01EF);
138}
139
140static bool starts_emoji_presentation_seq(uint32_t cp) {
141    int leaf;
142    switch (cp >> 10) {
143    case 0x0: leaf = 0; break;
144    case 0x8: leaf = 1; break;
145    case 0x9: leaf = 2; break;
146    case 0xA: leaf = 3; break;
147    case 0xC: leaf = 4; break;
148    case 0x7C: leaf = 5; break;
149    case 0x7D: leaf = 6; break;
150    default: return false;
151    }
152    uint8_t byte = TUI_UW_EMOJI_PRESENTATION_LEAVES[leaf][(cp >> 3) & 0x7F];
153    return ((byte >> (cp & 7)) & 1) == 1;
154}
155
156static bool in_pairs(const uint8_t (*leaf)[2], size_t n, uint8_t b) {
157    size_t lo = 0, hi = n;
158    while (lo < hi) {
159        size_t mid = (lo + hi) / 2;
160        if (b < leaf[mid][0]) hi = mid;
161        else if (b > leaf[mid][1]) lo = mid + 1;
162        else return true;
163    }
164    return false;
165}
166
167#define LEAF(t) (t), (sizeof(t) / sizeof((t)[0]))
168
169static bool starts_non_ideographic_text_presentation_seq(uint32_t cp) {
170    uint8_t b = (uint8_t)(cp & 0xFF);
171    switch (cp >> 8) {
172    case 0x23: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_0), b);
173    case 0x25: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_1), b);
174    case 0x26: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_2), b);
175    case 0x27: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_3), b);
176    case 0x2B: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_4), b);
177    case 0x1F0: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_5), b);
178    case 0x1F3: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_6), b);
179    case 0x1F4: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_7), b);
180    case 0x1F5: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_8), b);
181    case 0x1F6: return in_pairs(LEAF(TUI_UW_TEXT_PRESENTATION_LEAF_9), b);
182    default: return false;
183    }
184}
185
186static bool is_emoji_modifier_base(uint32_t cp) {
187    uint8_t b = (uint8_t)(cp & 0xFF);
188    switch (cp >> 8) {
189    case 0x26: return in_pairs(LEAF(TUI_UW_EMOJI_MODIFIER_LEAF_0), b);
190    case 0x27: return in_pairs(LEAF(TUI_UW_EMOJI_MODIFIER_LEAF_1), b);
191    case 0x1F3: return in_pairs(LEAF(TUI_UW_EMOJI_MODIFIER_LEAF_2), b);
192    case 0x1F4: return in_pairs(LEAF(TUI_UW_EMOJI_MODIFIER_LEAF_3), b);
193    case 0x1F5: return in_pairs(LEAF(TUI_UW_EMOJI_MODIFIER_LEAF_4), b);
194    case 0x1F6: return in_pairs(LEAF(TUI_UW_EMOJI_MODIFIER_LEAF_5), b);
195    case 0x1F9: return in_pairs(LEAF(TUI_UW_EMOJI_MODIFIER_LEAF_6), b);
196    case 0x1FA: return in_pairs(LEAF(TUI_UW_EMOJI_MODIFIER_LEAF_7), b);
197    default: return false;
198    }
199}
200
201typedef struct {
202    int width;
203    info info;
204} step;
205
206static bool ri(uint32_t c) { return c >= 0x1F1E6 && c <= 0x1F1FF; }
207static bool tag_letter(uint32_t c) { return c >= 0xE0061 && c <= 0xE007A; }
208static bool tag_digit(uint32_t c) { return c >= 0xE0030 && c <= 0xE0039; }
209
210static step width_in_str(uint32_t c, info next) {
211    if (is_emoji_presentation(next)) {
212        if (starts_emoji_presentation_seq(c)) return (step){is_zwj_emoji_presentation(next) ? 0 : 2, I_EMOJI_PRESENTATION};
213        next = unset_emoji_presentation(next);
214    }
215    if (c <= 0xA0) {
216        if (c == '\n') return (step){1, I_LINE_FEED};
217        if (c == '\r' && next == I_LINE_FEED) return (step){0, I_DEFAULT};
218        return (step){1, I_DEFAULT};
219    }
220    if (next != I_DEFAULT) {
221        if (c == 0xFE0F) return (step){0, set_emoji_presentation(next)};
222        if (c == 0xFE01) return (step){0, set_vs1_2_3(next)};
223        if (c == 0xFE0E) return (step){0, set_text_presentation(next)};
224        if (is_text_presentation(next)) {
225            if (starts_non_ideographic_text_presentation_seq(c)) return (step){1, I_DEFAULT};
226            next = unset_text_presentation(next);
227        } else if (is_vs1_2_3(next)) {
228            if (c == 0x2018 || c == 0x2019 || c == 0x201C || c == 0x201D) return (step){2, I_DEFAULT};
229            next = unset_vs1_2_3(next);
230        }
231        if (is_ligature_transparent_info(next)) {
232            if (c == 0x200D) return (step){0, set_zwj_bit(next)};
233            if (is_ligature_transparent(c)) return (step){0, next};
234        }
235        switch (next) {
236        case I_JOINING_GROUP_ALEF:
237            if (c == 0x644 || (c >= 0x6B5 && c <= 0x6B8) || c == 0x76A || c == 0x8A6 || c == 0x8C7)
238                return (step){0, I_DEFAULT};
239            if (is_transparent_zero_width(c)) return (step){0, I_JOINING_GROUP_ALEF};
240            break;
241        case I_ZWJ_HEBREW_LETTER_LAMED:
242            if (c == 0x05D0) return (step){0, I_DEFAULT};
243            break;
244        case I_KHMER_COENG_ELIGIBLE_LETTER:
245            if (c == 0x17D2) return (step){-1, I_DEFAULT};
246            break;
247        case I_ZWJ_BUGINESE_LETTER_YA:
248            if (c == 0x1A17) return (step){0, I_BUGINESE_VOWEL_SIGN_I_ZWJ_LETTER_YA};
249            break;
250        case I_BUGINESE_VOWEL_SIGN_I_ZWJ_LETTER_YA:
251            if (c == 0x1A15) return (step){0, I_DEFAULT};
252            break;
253        default: break;
254        }
255        if ((next == I_TIFINAGH_CONSONANT || next == I_ZWJ_TIFINAGH_CONSONANT) && c == 0x2D7F)
256            return (step){1, I_TIFINAGH_JOINER_CONSONANT};
257        if (next == I_ZWJ_TIFINAGH_CONSONANT && ((c >= 0x2D31 && c <= 0x2D65) || c == 0x2D6F)) return (step){0, I_DEFAULT};
258        if (next == I_TIFINAGH_JOINER_CONSONANT && ((c >= 0x2D31 && c <= 0x2D65) || c == 0x2D6F))
259            return (step){-1, I_DEFAULT};
260        if (next == I_LISU_TONE_LETTER_MYA_NA_JEU && c >= 0xA4F8 && c <= 0xA4FB) return (step){0, I_DEFAULT};
261        if (next == I_ZWJ_OLD_TURKIC_LETTER_ORKHON_I && c == 0x10C32) return (step){0, I_DEFAULT};
262        if (next == I_EMOJI_MODIFIER && is_emoji_modifier_base(c)) return (step){0, I_EMOJI_PRESENTATION};
263        if ((next == I_REGIONAL_INDICATOR || next == I_SEVERAL_REGIONAL_INDICATOR) && ri(c))
264            return (step){1, I_SEVERAL_REGIONAL_INDICATOR};
265        if ((next == I_EMOJI_PRESENTATION || next == I_SEVERAL_REGIONAL_INDICATOR ||
266             next == I_EVEN_REGIONAL_INDICATOR_ZWJ_PRESENTATION || next == I_ODD_REGIONAL_INDICATOR_ZWJ_PRESENTATION ||
267             next == I_EMOJI_MODIFIER) &&
268            c == 0x200D)
269            return (step){0, I_ZWJ_EMOJI_PRESENTATION};
270        if (next == I_ZWJ_EMOJI_PRESENTATION && c == 0x20E3) return (step){0, I_KEYCAP_ZWJ_EMOJI_PRESENTATION};
271        if (next == I_VS16_ZWJ_EMOJI_PRESENTATION && starts_emoji_presentation_seq(c)) return (step){0, I_EMOJI_PRESENTATION};
272        if (next == I_VS16_KEYCAP_ZWJ_EMOJI_PRESENTATION && ((c >= '0' && c <= '9') || c == '#' || c == '*'))
273            return (step){0, I_EMOJI_PRESENTATION};
274        if (next == I_ZWJ_EMOJI_PRESENTATION && ri(c)) return (step){1, I_REGIONAL_INDICATOR_ZWJ_PRESENTATION};
275        if ((next == I_REGIONAL_INDICATOR_ZWJ_PRESENTATION || next == I_ODD_REGIONAL_INDICATOR_ZWJ_PRESENTATION) && ri(c))
276            return (step){-1, I_EVEN_REGIONAL_INDICATOR_ZWJ_PRESENTATION};
277        if (next == I_EVEN_REGIONAL_INDICATOR_ZWJ_PRESENTATION && ri(c))
278            return (step){3, I_ODD_REGIONAL_INDICATOR_ZWJ_PRESENTATION};
279        if (next == I_ZWJ_EMOJI_PRESENTATION && c >= 0x1F3FB && c <= 0x1F3FF) return (step){0, I_EMOJI_MODIFIER};
280        if (next == I_ZWJ_EMOJI_PRESENTATION && c == 0xE007F) return (step){0, I_TAG_END_ZWJ_EMOJI_PRESENTATION};
281        if (next == I_TAG_END_ZWJ_EMOJI_PRESENTATION && tag_letter(c)) return (step){0, I_TAG_A1_END_ZWJ_EMOJI_PRESENTATION};
282        if (next == I_TAG_A1_END_ZWJ_EMOJI_PRESENTATION && tag_letter(c)) return (step){0, I_TAG_A2_END_ZWJ_EMOJI_PRESENTATION};
283        if (next == I_TAG_A2_END_ZWJ_EMOJI_PRESENTATION && tag_letter(c)) return (step){0, I_TAG_A3_END_ZWJ_EMOJI_PRESENTATION};
284        if (next == I_TAG_A3_END_ZWJ_EMOJI_PRESENTATION && tag_letter(c)) return (step){0, I_TAG_A4_END_ZWJ_EMOJI_PRESENTATION};
285        if (next == I_TAG_A4_END_ZWJ_EMOJI_PRESENTATION && tag_letter(c)) return (step){0, I_TAG_A5_END_ZWJ_EMOJI_PRESENTATION};
286        if (next == I_TAG_A5_END_ZWJ_EMOJI_PRESENTATION && tag_letter(c)) return (step){0, I_TAG_A6_END_ZWJ_EMOJI_PRESENTATION};
287        if ((next == I_TAG_END_ZWJ_EMOJI_PRESENTATION || next == I_TAG_A1_END_ZWJ_EMOJI_PRESENTATION ||
288             next == I_TAG_A2_END_ZWJ_EMOJI_PRESENTATION || next == I_TAG_A3_END_ZWJ_EMOJI_PRESENTATION ||
289             next == I_TAG_A4_END_ZWJ_EMOJI_PRESENTATION) &&
290            tag_digit(c))
291            return (step){0, I_TAG_D1_END_ZWJ_EMOJI_PRESENTATION};
292        if (next == I_TAG_D1_END_ZWJ_EMOJI_PRESENTATION && tag_digit(c)) return (step){0, I_TAG_D2_END_ZWJ_EMOJI_PRESENTATION};
293        if (next == I_TAG_D2_END_ZWJ_EMOJI_PRESENTATION && tag_digit(c)) return (step){0, I_TAG_D3_END_ZWJ_EMOJI_PRESENTATION};
294        if ((next == I_TAG_A3_END_ZWJ_EMOJI_PRESENTATION || next == I_TAG_A4_END_ZWJ_EMOJI_PRESENTATION ||
295             next == I_TAG_A5_END_ZWJ_EMOJI_PRESENTATION || next == I_TAG_A6_END_ZWJ_EMOJI_PRESENTATION ||
296             next == I_TAG_D3_END_ZWJ_EMOJI_PRESENTATION) &&
297            c == 0x1F3F4)
298            return (step){0, I_EMOJI_PRESENTATION};
299        if (next == I_ZWJ_EMOJI_PRESENTATION && lookup_width(c).info == I_EMOJI_PRESENTATION)
300            return (step){0, I_EMOJI_PRESENTATION};
301        if (next == I_KIRAT_RAI_VOWEL_SIGN_E && c == 0x16D63) return (step){0, I_DEFAULT};
302        if (next == I_KIRAT_RAI_VOWEL_SIGN_E && c == 0x16D67) return (step){0, I_KIRAT_RAI_VOWEL_SIGN_AI};
303        if (next == I_KIRAT_RAI_VOWEL_SIGN_E && c == 0x16D68) return (step){1, I_KIRAT_RAI_VOWEL_SIGN_E};
304        if (next == I_KIRAT_RAI_VOWEL_SIGN_E && c == 0x16D69) return (step){0, I_DEFAULT};
305        if (next == I_KIRAT_RAI_VOWEL_SIGN_AI && c == 0x16D63) return (step){0, I_DEFAULT};
306    }
307    lookup l = lookup_width(c);
308    return (step){l.width, l.info};
309}
310
311size_t tui_str_width(const char *s, size_t len) {
312    /* Decode forwards, fold backwards. */
313    uint32_t small[256];
314    uint32_t *cps = small;
315    size_t n = 0, cap = 256;
316    for (size_t i = 0; i < len;) {
317        if (n == cap) {
318            cap *= 2;
319            uint32_t *bigger = malloc(cap * sizeof *bigger);
320            if (!bigger) abort();
321            for (size_t k = 0; k < n; k++) bigger[k] = cps[k];
322            if (cps != small) free(cps);
323            cps = bigger;
324        }
325        cps[n++] = tui_utf8_next(s, len, &i);
326    }
327    size_t sum = 0;
328    info next = I_DEFAULT;
329    for (size_t k = n; k-- > 0;) {
330        step st = width_in_str(cps[k], next);
331        sum += (size_t)(ptrdiff_t)st.width; /* wrapping_add_signed */
332        next = st.info;
333    }
334    if (cps != small) free(cps);
335    return sum;
336}
337
338size_t tui_cstr_width(const char *s) {
339    size_t len = 0;
340    while (s[len]) len++;
341    return tui_str_width(s, len);
342}