==> 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}