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