skip to content
==> src/text.c <==
1/* Spans, lines and text: building, measuring, rendering. */
2#include "tui/text.h"
3
4#include "tui/grapheme.h"
5#include "tui/unicode_width.h"
6
7#include <string.h>
8
9void tui_line_push(tui_arena *a, tui_line *l, tui_span s) {
10    if (l->n == l->cap) {
11        size_t cap = l->cap ? l->cap * 2 : 4;
12        l->spans = tui_arena_grow(a, l->spans, l->cap * sizeof *l->spans, cap * sizeof *l->spans);
13        l->cap = cap;
14    }
15    l->spans[l->n++] = s;
16}
17
18tui_line tui_line_of(tui_arena *a, size_t n, const tui_span *spans) {
19    tui_line l = tui_line_empty();
20    for (size_t i = 0; i < n; i++) tui_line_push(a, &l, spans[i]);
21    return l;
22}
23
24void tui_str_lines(const char *s, void (*f)(void *ctx, const char *line, size_t len), void *ctx) {
25    size_t len = strlen(s), start = 0;
26    while (start < len) {
27        const char *nl = memchr(s + start, '\n', len - start);
28        size_t end = nl ? (size_t)(nl - s) : len;
29        size_t e = end;
30        if (nl && e > start && s[e - 1] == '\r') e--;
31        f(ctx, s + start, e - start);
32        start = nl ? end + 1 : len;
33    }
34}
35
36typedef struct {
37    tui_arena *a;
38    tui_line *line;
39    tui_text *text;
40} lines_ctx;
41
42static void add_span(void *ctx, const char *s, size_t len) {
43    lines_ctx *c = ctx;
44    tui_line_push(c->a, c->line, tui_span_raw(tui_arena_strndup(c->a, s, len)));
45}
46
47tui_line tui_line_from_str(tui_arena *a, const char *s) {
48    tui_line l = tui_line_empty();
49    lines_ctx c = {a, &l, NULL};
50    tui_str_lines(s, add_span, &c);
51    return l;
52}
53
54void tui_text_push(tui_arena *a, tui_text *t, tui_line l) {
55    if (t->n == t->cap) {
56        size_t cap = t->cap ? t->cap * 2 : 8;
57        t->lines = tui_arena_grow(a, t->lines, t->cap * sizeof *t->lines, cap * sizeof *t->lines);
58        t->cap = cap;
59    }
60    t->lines[t->n++] = l;
61}
62
63static void add_line(void *ctx, const char *s, size_t len) {
64    lines_ctx *c = ctx;
65    tui_line l = tui_line_empty();
66    tui_line_push(c->a, &l, tui_span_raw(tui_arena_strndup(c->a, s, len)));
67    tui_text_push(c->a, c->text, l);
68}
69
70tui_text tui_text_from_str(tui_arena *a, const char *s) {
71    tui_text t = tui_text_empty();
72    lines_ctx c = {a, NULL, &t};
73    tui_str_lines(s, add_line, &c);
74    return t;
75}
76
77tui_text tui_text_of(tui_arena *a, size_t n, const tui_line *lines) {
78    tui_text t = tui_text_empty();
79    for (size_t i = 0; i < n; i++) tui_text_push(a, &t, lines[i]);
80    return t;
81}
82
83size_t tui_span_width(const tui_span *s) { return tui_cstr_width(s->s); }
84
85size_t tui_line_width(const tui_line *l) {
86    size_t w = 0;
87    for (size_t i = 0; i < l->n; i++) w += tui_span_width(&l->spans[i]);
88    return w;
89}
90
91size_t tui_text_width(const tui_text *t) {
92    size_t w = 0;
93    for (size_t i = 0; i < t->n; i++) {
94        size_t lw = tui_line_width(&t->lines[i]);
95        if (lw > w) w = lw;
96    }
97    return w;
98}
99
100size_t tui_line_graphemes(tui_arena *a, const tui_line *l, tui_style base, tui_sgrapheme **out) {
101    tui_style ls = tui_style_patch(base, l->style);
102    size_t n = 0, cap = 0;
103    tui_sgrapheme *g = NULL;
104    for (size_t k = 0; k < l->n; k++) {
105        const tui_span *sp = &l->spans[k];
106        tui_style st = tui_style_patch(ls, sp->style);
107        size_t len = strlen(sp->s);
108        for (size_t i = 0; i < len;) {
109            size_t end = tui_grapheme_next(sp->s, len, i);
110            if (!tui_symbol_has_control(sp->s + i, end - i)) {
111                if (n == cap) {
112                    size_t nc = cap ? cap * 2 : 64;
113                    g = tui_arena_grow(a, g, cap * sizeof *g, nc * sizeof *g);
114                    cap = nc;
115                }
116                g[n++] = (tui_sgrapheme){sp->s + i, end - i, st};
117            }
118            i = end;
119        }
120    }
121    *out = g;
122    return n;
123}
124
125void tui_span_render(const tui_span *sp, tui_rect area, tui_buffer *b) {
126    area = tui_rect_intersection(area, b->area);
127    if (tui_rect_empty(area)) return;
128    uint16_t x = area.x, y = area.y;
129    size_t len = strlen(sp->s), i = 0;
130    int idx = 0;
131    while (i < len) {
132        size_t end = tui_grapheme_next(sp->s, len, i);
133        const char *g = sp->s + i;
134        size_t glen = end - i;
135        i = end;
136        if (tui_symbol_has_control(g, glen)) continue;
137        uint16_t w = tui_symbol_width(g, glen);
138        uint16_t next = tui_sat_add(x, w);
139        if (next > tui_rect_right(area)) break;
140        tui_style st = sp->style;
141        if (idx == 0) {
142            tui_cell *c = tui_buffer_cell(b, x, y);
143            tui_cell_set_symbol(c, g, glen);
144            tui_cell_set_style(c, st);
145        } else if (x == area.x) {
146            tui_cell *c = tui_buffer_cell(b, x, y);
147            tui_cell_append_symbol(c, g, glen);
148            tui_cell_set_style(c, st);
149        } else if (w == 0) {
150            tui_cell *c = tui_buffer_cell(b, (uint16_t)(x - 1), y);
151            tui_cell_append_symbol(c, g, glen);
152            tui_cell_set_style(c, st);
153        } else {
154            tui_cell *c = tui_buffer_cell(b, x, y);
155            tui_cell_set_symbol(c, g, glen);
156            tui_cell_set_style(c, st);
157        }
158        for (uint16_t h = (uint16_t)(x + 1); h < next; h++) tui_cell_reset(tui_buffer_cell(b, h, y));
159        x = next;
160        idx++;
161    }
162}
163
164/* unicode_truncate_start: the longest suffix of s (whole graphemes) at most max_width wide. */
165static const char *truncate_start(const char *s, size_t max_width, size_t *width) {
166    size_t len = strlen(s);
167    /* Grapheme starts, walked from the end. */
168    size_t starts[512], n = 0;
169    size_t *st = starts;
170    size_t cap = 512;
171    tui_arena tmp = {0};
172    for (size_t i = 0; i < len; i = tui_grapheme_next(s, len, i)) {
173        if (n == cap) {
174            size_t *bigger = tui_arena_alloc(&tmp, cap * 2 * sizeof *bigger);
175            memcpy(bigger, st, n * sizeof *bigger);
176            st = bigger;
177            cap *= 2;
178        }
179        st[n++] = i;
180    }
181    size_t at = len, sum = 0;
182    for (size_t k = n; k-- > 0;) {
183        size_t end = k + 1 < n ? st[k + 1] : len;
184        size_t w = tui_str_width(s + st[k], end - st[k]);
185        if (sum + w > max_width) break;
186        sum += w;
187        at = st[k];
188    }
189    tui_arena_free(&tmp);
190    *width = at == len ? 0 : sum;
191    return s + at;
192}
193
194static void render_spans(tui_arena *a, const tui_line *l, tui_rect area, tui_buffer *b, size_t skip) {
195    for (size_t k = 0; k < l->n; k++) {
196        const tui_span *sp = &l->spans[k];
197        size_t sw = tui_span_width(sp);
198        if (skip >= sw) {
199            skip -= sw;
200            continue;
201        }
202        size_t avail = sw - skip;
203        skip = 0;
204        tui_span shown = *sp;
205        size_t shown_w = sw;
206        uint16_t offset = 0;
207        if (sw > avail) {
208            size_t actual;
209            shown.s = truncate_start(sp->s, avail, &actual);
210            shown_w = actual;
211            offset = (uint16_t)(avail - actual);
212        }
213        area = tui_rect_indent_x(area, offset);
214        if (tui_rect_empty(area)) break;
215        tui_span_render(&shown, area, b);
216        area = tui_rect_indent_x(area, (uint16_t)(shown_w > 0xFFFF ? 0xFFFF : shown_w));
217    }
218    (void)a;
219}
220
221void tui_line_render(tui_arena *a, const tui_line *l, tui_rect area, tui_buffer *b, int8_t parent) {
222    area = tui_rect_intersection(area, b->area);
223    if (tui_rect_empty(area)) return;
224    area.height = 1;
225    size_t lw = tui_line_width(l);
226    if (lw == 0) return;
227    tui_buffer_set_style(b, area, l->style);
228    int8_t align = l->align != TUI_ALIGN_NONE ? l->align : parent;
229    size_t aw = area.width;
230    if (lw <= aw) {
231        size_t indent = align == TUI_ALIGN_CENTER ? (aw - lw) / 2 : align == TUI_ALIGN_RIGHT ? aw - lw : 0;
232        render_spans(a, l, tui_rect_indent_x(area, (uint16_t)indent), b, 0);
233    } else {
234        size_t skip = align == TUI_ALIGN_CENTER ? (lw - aw) / 2 : align == TUI_ALIGN_RIGHT ? lw - aw : 0;
235        render_spans(a, l, area, b, skip);
236    }
237}
238
239void tui_text_render(tui_arena *a, const tui_text *t, tui_rect area, tui_buffer *b) {
240    area = tui_rect_intersection(area, b->area);
241    tui_buffer_set_style(b, area, t->style);
242    for (size_t i = 0; i < t->n && i < area.height; i++)
243        tui_line_render(a, &t->lines[i], tui_rect_row(area, (uint16_t)i), b, t->align);
244}
245
246uint16_t tui_buffer_set_line(tui_buffer *b, uint16_t x, uint16_t y, const tui_line *l, uint16_t max_width) {
247    uint16_t remaining = max_width;
248    for (size_t k = 0; k < l->n; k++) {
249        if (remaining == 0) break;
250        const tui_span *sp = &l->spans[k];
251        uint16_t nx = tui_buffer_set_stringn(b, x, y, sp->s, strlen(sp->s), remaining, tui_style_patch(l->style, sp->style));
252        uint16_t w = tui_sat_sub(nx, x);
253        x = nx;
254        remaining = tui_sat_sub(remaining, w);
255    }
256    return x;
257}