skip to content
==> src/paragraph.c <==
1/* ratatui-widgets 0.3.2's Paragraph with reflow.rs's WordWrapper and LineTruncator, ported. */
2#include "tui/paragraph.h"
3
4#include "tui/grapheme.h"
5#include "tui/utf8.h"
6
7#include <stdlib.h>
8#include <string.h>
9
10typedef struct {
11    tui_sgrapheme *g;
12    size_t n, cap;
13} gvec;
14
15static void gpush(tui_arena *a, gvec *v, tui_sgrapheme g) {
16    if (v->n == v->cap) {
17        size_t cap = v->cap ? v->cap * 2 : 32;
18        v->g = tui_arena_grow(a, v->g, v->cap * sizeof *v->g, cap * sizeof *v->g);
19        v->cap = cap;
20    }
21    v->g[v->n++] = g;
22}
23
24static uint16_t gw(const tui_sgrapheme *g) { return tui_symbol_width(g->s, g->len); }
25
26/* StyledGrapheme::is_whitespace: ZWSP, or all whitespace and not NBSP. */
27static bool is_ws_cp(uint32_t c) {
28    return (c >= 0x9 && c <= 0xD) || c == 0x20 || c == 0x85 || c == 0xA0 || c == 0x1680 || (c >= 0x2000 && c <= 0x200A) ||
29           c == 0x2028 || c == 0x2029 || c == 0x202F || c == 0x205F || c == 0x3000;
30}
31static bool is_whitespace(const tui_sgrapheme *g) {
32    if (g->len == 3 && memcmp(g->s, "\xE2\x80\x8B", 3) == 0) return true;
33    if (g->len == 2 && memcmp(g->s, "\xC2\xA0", 2) == 0) return false;
34    for (size_t i = 0; i < g->len;) {
35        uint32_t c = tui_utf8_next(g->s, g->len, &i);
36        if (!is_ws_cp(c)) return false;
37    }
38    return true;
39}
40
41typedef struct {
42    gvec *lines;
43    size_t n, cap;
44} lvec;
45
46static void lpush(tui_arena *a, lvec *v, gvec g) {
47    if (v->n == v->cap) {
48        size_t cap = v->cap ? v->cap * 2 : 16;
49        v->lines = tui_arena_grow(a, v->lines, v->cap * sizeof *v->lines, cap * sizeof *v->lines);
50        v->cap = cap;
51    }
52    v->lines[v->n++] = g;
53}
54
55/* WordWrapper::process_input for one input line: appends its wrapped lines to out. */
56static void wrap_line(tui_arena *a, const tui_sgrapheme *in, size_t nin, uint16_t max, bool trim, lvec *out) {
57    gvec pending = {0}, word = {0}, ws = {0}; /* ws is a deque: [ws_head, ws.n) */
58    size_t ws_head = 0;
59    uint16_t line_width = 0, word_width = 0, ws_width = 0;
60    bool non_ws_prev = false;
61    size_t before = out->n;
62    for (size_t i = 0; i < nin; i++) {
63        tui_sgrapheme g = in[i];
64        bool is_ws = is_whitespace(&g);
65        uint16_t sw = gw(&g);
66        if (sw > max) continue;
67        bool word_found = non_ws_prev && is_ws;
68        bool trimmed_overflow = pending.n == 0 && trim && word_width + sw > max;
69        bool ws_overflow = pending.n == 0 && trim && ws_width + sw > max;
70        bool untrimmed_overflow = pending.n == 0 && !trim && word_width + ws_width + sw > max;
71        if (word_found || trimmed_overflow || ws_overflow || untrimmed_overflow) {
72            if (pending.n != 0 || !trim) {
73                for (size_t k = ws_head; k < ws.n; k++) gpush(a, &pending, ws.g[k]);
74                line_width = (uint16_t)(line_width + ws_width);
75            }
76            for (size_t k = 0; k < word.n; k++) gpush(a, &pending, word.g[k]);
77            word.n = 0;
78            line_width = (uint16_t)(line_width + word_width);
79            ws.n = ws_head = 0;
80            ws_width = 0;
81            word_width = 0;
82        }
83        bool line_full = line_width >= max;
84        bool word_overflow = sw > 0 && line_width + ws_width + word_width >= max;
85        if (line_full || word_overflow) {
86            uint16_t remaining = tui_sat_sub(max, line_width);
87            lpush(a, out, pending);
88            pending = (gvec){0};
89            line_width = 0;
90            while (ws_head < ws.n) {
91                uint16_t w = gw(&ws.g[ws_head]);
92                if (w > remaining) break;
93                ws_width = (uint16_t)(ws_width - w);
94                remaining = (uint16_t)(remaining - w);
95                ws_head++;
96            }
97            if (is_ws && ws_head == ws.n) continue;
98        }
99        if (is_ws) {
100            ws_width = (uint16_t)(ws_width + sw);
101            gpush(a, &ws, g);
102        } else {
103            word_width = (uint16_t)(word_width + sw);
104            gpush(a, &word, g);
105        }
106        non_ws_prev = !is_ws;
107    }
108    if (pending.n == 0 && word.n == 0 && ws_head < ws.n && trim) lpush(a, out, (gvec){0});
109    if (pending.n != 0 || !trim)
110        for (size_t k = ws_head; k < ws.n; k++) gpush(a, &pending, ws.g[k]);
111    for (size_t k = 0; k < word.n; k++) gpush(a, &pending, word.g[k]);
112    if (pending.n != 0) lpush(a, out, pending);
113    if (out->n == before) lpush(a, out, (gvec){0});
114}
115
116static uint16_t line_offset(uint16_t lw, uint16_t aw, int8_t align) {
117    if (align == TUI_ALIGN_CENTER) return tui_sat_sub((uint16_t)(aw / 2), (uint16_t)(lw / 2));
118    if (align == TUI_ALIGN_RIGHT) return tui_sat_sub(aw, lw);
119    return 0;
120}
121
122static void render_line(const tui_sgrapheme *g, size_t n, uint16_t width, int8_t align, tui_rect area, tui_buffer *buf,
123                        uint16_t y) {
124    uint16_t x = line_offset(width, area.width, align);
125    for (size_t i = 0; i < n; i++) {
126        uint16_t w = gw(&g[i]);
127        if (w == 0) continue;
128        tui_cell *c = tui_buffer_cell(buf, (uint16_t)(area.x + x), (uint16_t)(area.y + y));
129        if (c) {
130            if (g[i].len == 0) tui_cell_set_str(c, " ");
131            else tui_cell_set_symbol(c, g[i].s, g[i].len);
132            tui_cell_set_style(c, g[i].style);
133        }
134        x = (uint16_t)(x + w);
135    }
136}
137
138void tui_paragraph_render(tui_arena *a, const tui_paragraph *p, tui_rect area, tui_buffer *buf) {
139    area = tui_rect_intersection(area, buf->area);
140    tui_buffer_set_style(buf, area, p->style);
141    if (p->block) tui_block_render(a, p->block, area, buf);
142    tui_rect ta = tui_block_inner_opt(p->block, area);
143    if (tui_rect_empty(ta)) return;
144    tui_buffer_set_style(buf, ta, p->style);
145    uint16_t y = 0;
146    if (p->wrap != TUI_WRAP_NONE) {
147        /* Every wrapped line, then skip scroll_y of them. */
148        uint16_t skip = p->scroll_y;
149        for (size_t li = 0; li < p->text.n; li++) {
150            const tui_line *l = &p->text.lines[li];
151            tui_sgrapheme *g;
152            size_t n = tui_line_graphemes(a, l, p->text.style, &g);
153            int8_t align = l->align != TUI_ALIGN_NONE ? l->align : p->alignment;
154            lvec wrapped = {0};
155            wrap_line(a, g, n, ta.width, p->wrap == TUI_WRAP_TRIM, &wrapped);
156            for (size_t k = 0; k < wrapped.n; k++) {
157                if (skip) {
158                    skip--;
159                    continue;
160                }
161                uint16_t w = 0;
162                for (size_t j = 0; j < wrapped.lines[k].n; j++) w = (uint16_t)(w + gw(&wrapped.lines[k].g[j]));
163                render_line(wrapped.lines[k].g, wrapped.lines[k].n, w, align, ta, buf, y);
164                y++;
165                if (y >= ta.height) return;
166            }
167        }
168        return;
169    }
170    for (size_t li = p->scroll_y; li < p->text.n; li++) {
171        const tui_line *l = &p->text.lines[li];
172        tui_sgrapheme *g;
173        size_t n = tui_line_graphemes(a, l, p->text.style, &g);
174        int8_t align = l->align != TUI_ALIGN_NONE ? l->align : p->alignment;
175        gvec cur = {0};
176        uint16_t width = 0, hoff = p->scroll_x;
177        for (size_t i = 0; i < n; i++) {
178            uint16_t w = gw(&g[i]);
179            if (w > ta.width) continue;
180            if (width + w > ta.width) break;
181            tui_sgrapheme s = g[i];
182            if (hoff != 0 && align == TUI_ALIGN_LEFT) {
183                if (w > hoff) {
184                    /* trim_offset: drop leading graphemes of this symbol worth hoff columns. */
185                    size_t start = 0;
186                    uint16_t off = hoff;
187                    for (size_t k = 0; k < s.len;) {
188                        size_t e = tui_grapheme_next(s.s, s.len, k);
189                        uint16_t cw = tui_symbol_width(s.s + k, e - k);
190                        if (cw <= off) {
191                            off = (uint16_t)(off - cw);
192                            start = e;
193                            k = e;
194                        } else break;
195                    }
196                    s.s += start;
197                    s.len -= start;
198                    hoff = 0;
199                } else {
200                    hoff = (uint16_t)(hoff - w);
201                    s.len = 0;
202                }
203            }
204            width = (uint16_t)(width + tui_symbol_width(s.s, s.len));
205            gpush(a, &cur, s);
206        }
207        render_line(cur.g, cur.n, width, align, ta, buf, y);
208        y++;
209        if (y >= ta.height) return;
210    }
211}