==> src/canvas.c <==1/* ratatui-widgets 0.3.2's canvas.rs (PatternGrid 2x4 for braille, CharGrid, HalfBlockGrid, Painter,
2 * Context, Canvas) and canvas/line.rs, points.rs, with line-clipping 0.3's Cohen-Sutherland. */
3#include "tui/canvas.h"
4
5#include "tui/utf8.h"
6
7#include <math.h>
8#include <stdlib.h>
9#include <string.h>
10
11typedef struct {
12 bool has_symbol, has_fg, has_bg;
13 uint32_t symbol;
14 tui_color fg, bg;
15} layer_cell;
16
17typedef struct layer {
18 layer_cell *cells;
19 struct layer *next;
20} layer;
21
22typedef struct {
23 tui_marker marker;
24 uint16_t width, height;
25 uint8_t *pattern; /* braille: one byte per cell */
26 bool *has_color; /* braille and char grids: per cell */
27 tui_color *color; /* per cell (half blocks: per pixel, 2 rows a cell) */
28 bool *has_pixel; /* half blocks: per pixel */
29} grid;
30
31struct tui_canvas {
32 tui_arena *a;
33 uint16_t width, height;
34 double xb[2], yb[2];
35 grid g;
36 bool dirty;
37 layer *first, *last;
38 struct label {
39 double x, y;
40 tui_line line;
41 } *labels;
42 size_t nlabels, caplabels;
43};
44
45static void grid_init(tui_arena *a, grid *g, uint16_t w, uint16_t h, tui_marker m) {
46 size_t n = (size_t)w * h;
47 *g = (grid){.marker = m, .width = w, .height = h};
48 if (m == TUI_MARKER_HALF_BLOCK) {
49 g->color = tui_arena_alloc(a, (n ? 2 * n : 1) * sizeof *g->color);
50 g->has_pixel = tui_arena_alloc(a, n ? 2 * n : 1);
51 } else {
52 g->color = tui_arena_alloc(a, (n ? n : 1) * sizeof *g->color);
53 g->has_color = tui_arena_alloc(a, n ? n : 1);
54 if (m == TUI_MARKER_BRAILLE) g->pattern = tui_arena_alloc(a, n ? n : 1);
55 }
56}
57
58static void grid_resolution(const grid *g, double *rx, double *ry) {
59 if (g->marker == TUI_MARKER_BRAILLE) {
60 *rx = (double)g->width * 2.0;
61 *ry = (double)g->height * 4.0;
62 } else if (g->marker == TUI_MARKER_HALF_BLOCK) {
63 *rx = (double)g->width;
64 *ry = (double)g->height * 2.0;
65 } else {
66 *rx = (double)g->width;
67 *ry = (double)g->height;
68 }
69}
70
71static void grid_paint(grid *g, size_t x, size_t y, tui_color c) {
72 size_t n = (size_t)g->width * g->height;
73 if (g->marker == TUI_MARKER_BRAILLE) {
74 size_t i = (y / 4) * g->width + x / 2; /* x is not checked against the width, as ratatui's */
75 if (i < n) {
76 g->pattern[i] |= (uint8_t)(1u << ((x % 2) + 2 * (y % 4)));
77 g->has_color[i] = true;
78 g->color[i] = c;
79 }
80 } else if (g->marker == TUI_MARKER_HALF_BLOCK) {
81 if (y < 2u * g->height && x < g->width) {
82 g->has_pixel[y * g->width + x] = true;
83 g->color[y * g->width + x] = c;
84 }
85 } else {
86 size_t i = y * g->width + x;
87 if (i < n) {
88 g->has_color[i] = true;
89 g->color[i] = c;
90 }
91 }
92}
93
94/* The braille character for a row-major dot pattern (bit (x % 2) + 2 (y % 4)). */
95static uint32_t braille(uint8_t p) {
96 static const uint8_t dot[8] = {0x01, 0x08, 0x02, 0x10, 0x04, 0x20, 0x40, 0x80};
97 uint32_t cp = 0x2800;
98 for (int b = 0; b < 8; b++)
99 if (p & (1u << b)) cp |= dot[b];
100 return cp;
101}
102
103static layer *grid_save(tui_arena *a, const grid *g) {
104 size_t n = (size_t)g->width * g->height;
105 layer *l = tui_arena_alloc(a, sizeof *l);
106 l->cells = tui_arena_alloc(a, (n ? n : 1) * sizeof *l->cells);
107 for (size_t i = 0; i < n; i++) {
108 layer_cell *c = &l->cells[i];
109 switch (g->marker) {
110 case TUI_MARKER_BRAILLE:
111 if (g->pattern[i]) {
112 c->has_symbol = true;
113 c->symbol = braille(g->pattern[i]);
114 }
115 if (g->has_color[i]) {
116 c->has_fg = true;
117 c->fg = g->color[i];
118 }
119 break;
120 case TUI_MARKER_HALF_BLOCK: {
121 size_t x = i % g->width, row = i / g->width;
122 size_t up = 2 * row * g->width + x, lo = (2 * row + 1) * g->width + x;
123 bool hu = g->has_pixel[up], hl = g->has_pixel[lo];
124 tui_color u = g->color[up], d = g->color[lo];
125 if (!hu && hl) *c = (layer_cell){true, true, false, 0x2584, d, {0}};
126 else if (hu && !hl) *c = (layer_cell){true, true, false, 0x2580, u, {0}};
127 else if (hu && hl && tui_color_eq(u, d)) *c = (layer_cell){true, true, true, 0x2588, u, d};
128 else if (hu && hl) *c = (layer_cell){true, true, true, 0x2580, u, d};
129 break;
130 }
131 default:
132 if (g->has_color[i]) {
133 c->has_symbol = true;
134 c->symbol = g->marker == TUI_MARKER_BLOCK ? 0x2588 : g->marker == TUI_MARKER_BAR ? 0x2584 : 0x2022;
135 c->has_fg = true;
136 c->fg = g->color[i];
137 if (g->marker == TUI_MARKER_BLOCK) {
138 c->has_bg = true;
139 c->bg = g->color[i];
140 }
141 }
142 break;
143 }
144 }
145 return l;
146}
147
148tui_canvas *tui_canvas_new(tui_arena *a, uint16_t width, uint16_t height, const double x_bounds[2],
149 const double y_bounds[2], tui_marker marker) {
150 tui_canvas *c = tui_arena_alloc(a, sizeof *c);
151 c->a = a;
152 c->width = width;
153 c->height = height;
154 memcpy(c->xb, x_bounds, sizeof c->xb);
155 memcpy(c->yb, y_bounds, sizeof c->yb);
156 grid_init(a, &c->g, width, height, marker);
157 return c;
158}
159
160static void finish(tui_canvas *c) {
161 if (!c->dirty) return;
162 layer *l = grid_save(c->a, &c->g);
163 if (c->last) c->last->next = l;
164 else c->first = l;
165 c->last = l;
166 grid_init(c->a, &c->g, c->width, c->height, c->g.marker);
167 c->dirty = false;
168}
169
170void tui_canvas_marker(tui_canvas *c, tui_marker m) {
171 finish(c);
172 grid_init(c->a, &c->g, c->width, c->height, m);
173}
174
175/* Painter::get_point. */
176static bool get_point(const tui_canvas *c, double x, double y, size_t *px, size_t *py) {
177 double left = c->xb[0], right = c->xb[1], bottom = c->yb[0], top = c->yb[1];
178 if (x < left || x > right || y < bottom || y > top) return false;
179 double width = right - left, height = top - bottom;
180 if (width <= 0.0 || height <= 0.0) return false;
181 double rx, ry;
182 grid_resolution(&c->g, &rx, &ry);
183 double fx = round((x - left) * (rx - 1.0) / width), fy = round((top - y) * (ry - 1.0) / height);
184 *px = fx <= 0 || fx != fx ? 0 : (size_t)fx;
185 *py = fy <= 0 || fy != fy ? 0 : (size_t)fy;
186 return true;
187}
188
189void tui_canvas_points(tui_canvas *c, const tui_point *pts, size_t n, tui_color color) {
190 c->dirty = true;
191 for (size_t i = 0; i < n; i++) {
192 size_t x, y;
193 if (get_point(c, pts[i].x, pts[i].y, &x, &y)) grid_paint(&c->g, x, y, color);
194 }
195}
196
197/* line-clipping 0.3's Cohen-Sutherland. */
198enum { R_LEFT = 1, R_RIGHT = 2, R_BOTTOM = 4, R_TOP = 8 };
199
200static int region(double x, double y, const double xb[2], const double yb[2]) {
201 int r = 0;
202 if (x < xb[0]) r |= R_LEFT;
203 else if (x > xb[1]) r |= R_RIGHT;
204 if (y < yb[0]) r |= R_BOTTOM;
205 else if (y > yb[1]) r |= R_TOP;
206 return r;
207}
208
209static void intersect(double *x1, double *y1, double x2, double y2, int r, const double xb[2], const double yb[2]) {
210 double dx = x2 - *x1, dy = y2 - *y1;
211 if (r & R_LEFT) {
212 *y1 = *y1 + (xb[0] - *x1) * dy / dx;
213 *x1 = xb[0];
214 } else if (r & R_RIGHT) {
215 *y1 = *y1 + (xb[1] - *x1) * dy / dx;
216 *x1 = xb[1];
217 } else if (r & R_BOTTOM) {
218 *x1 = *x1 + (yb[0] - *y1) * dx / dy;
219 *y1 = yb[0];
220 } else {
221 *x1 = *x1 + (yb[1] - *y1) * dx / dy;
222 *y1 = yb[1];
223 }
224}
225
226static bool clip(const double xb[2], const double yb[2], double *x1, double *y1, double *x2, double *y2) {
227 int r1 = region(*x1, *y1, xb, yb), r2 = region(*x2, *y2, xb, yb);
228 for (;;) {
229 if (r1 & r2) return false;
230 if (r1) {
231 intersect(x1, y1, *x2, *y2, r1, xb, yb);
232 r1 = region(*x1, *y1, xb, yb);
233 } else if (r2) {
234 intersect(x2, y2, *x1, *y1, r2, xb, yb);
235 r2 = region(*x2, *y2, xb, yb);
236 } else {
237 return true;
238 }
239 }
240}
241
242typedef void (*plot_fn)(tui_canvas *c, size_t x, size_t y, void *arg);
243
244static void line_low(tui_canvas *c, size_t x1, size_t y1, size_t x2, size_t y2, plot_fn f, void *arg) {
245 long dx = (long)(x2 - x1), dy = labs((long)y2 - (long)y1);
246 long d = 2 * dy - dx;
247 size_t y = y1;
248 for (size_t x = x1; x <= x2; x++) {
249 f(c, x, y, arg);
250 if (d > 0) {
251 y = y1 > y2 ? (y ? y - 1 : 0) : y + 1;
252 d -= 2 * dx;
253 }
254 d += 2 * dy;
255 }
256}
257
258static void line_high(tui_canvas *c, size_t x1, size_t y1, size_t x2, size_t y2, plot_fn f, void *arg) {
259 long dx = labs((long)x2 - (long)x1), dy = (long)(y2 - y1);
260 long d = 2 * dx - dy;
261 size_t x = x1;
262 for (size_t y = y1; y <= y2; y++) {
263 f(c, x, y, arg);
264 if (d > 0) {
265 x = x1 > x2 ? (x ? x - 1 : 0) : x + 1;
266 d -= 2 * dy;
267 }
268 d += 2 * dx;
269 }
270}
271
272static void each_line_point(tui_canvas *c, size_t x1, size_t y1, size_t x2, size_t y2, plot_fn f, void *arg) {
273 size_t dx = x1 > x2 ? x1 - x2 : x2 - x1, dy = y1 > y2 ? y1 - y2 : y2 - y1;
274 if (dx == 0) {
275 for (size_t y = y1 < y2 ? y1 : y2; y <= (y1 > y2 ? y1 : y2); y++) f(c, x1, y, arg);
276 } else if (dy == 0) {
277 for (size_t x = x1 < x2 ? x1 : x2; x <= (x1 > x2 ? x1 : x2); x++) f(c, x, y1, arg);
278 } else if (dy < dx) {
279 if (x1 > x2) line_low(c, x2, y2, x1, y1, f, arg);
280 else line_low(c, x1, y1, x2, y2, f, arg);
281 } else if (y1 > y2) {
282 line_high(c, x2, y2, x1, y1, f, arg);
283 } else {
284 line_high(c, x1, y1, x2, y2, f, arg);
285 }
286}
287
288static void plot(tui_canvas *c, size_t x, size_t y, void *arg) { grid_paint(&c->g, x, y, *(tui_color *)arg); }
289
290void tui_canvas_line(tui_canvas *c, double x1, double y1, double x2, double y2, tui_color color) {
291 c->dirty = true;
292 if (!clip(c->xb, c->yb, &x1, &y1, &x2, &y2)) return;
293 size_t px1, py1, px2, py2;
294 if (!get_point(c, x1, y1, &px1, &py1) || !get_point(c, x2, y2, &px2, &py2)) return;
295 each_line_point(c, px1, py1, px2, py2, plot, &color);
296}
297
298typedef struct {
299 size_t fill;
300 tui_color color;
301} fill_arg;
302
303static void plot_fill(tui_canvas *c, size_t x, size_t y, void *arg) {
304 fill_arg *f = arg;
305 size_t s = y < f->fill ? y : f->fill, e = y > f->fill ? y : f->fill;
306 for (size_t k = s; k <= e; k++) grid_paint(&c->g, x, k, f->color);
307}
308
309void tui_canvas_filled_line(tui_canvas *c, double x1, double y1, double x2, double y2, double fill_to_y, tui_color color) {
310 c->dirty = true;
311 double wx1 = x1, wy1 = y1, wx2 = x2, wy2 = y2;
312 if (!clip(c->xb, c->yb, &wx1, &wy1, &wx2, &wy2)) return;
313 size_t px1, py1, px2, py2, fx, fy;
314 if (!get_point(c, wx1, wy1, &px1, &py1) || !get_point(c, wx2, wy2, &px2, &py2)) return;
315 double yf = fill_to_y < c->yb[0] ? c->yb[0] : fill_to_y > c->yb[1] ? c->yb[1] : fill_to_y;
316 if (!get_point(c, wx1, yf, &fx, &fy)) return;
317 fill_arg f = {fy, color};
318 each_line_point(c, px1, py1, px2, py2, plot_fill, &f);
319}
320
321void tui_canvas_bar(tui_canvas *c, double x, double y, tui_color color) { tui_canvas_line(c, x, 0.0, x, y, color); }
322
323void tui_canvas_print(tui_canvas *c, double x, double y, tui_line line) {
324 if (c->nlabels == c->caplabels) {
325 size_t cap = c->caplabels ? c->caplabels * 2 : 4;
326 c->labels = tui_arena_grow(c->a, c->labels, c->caplabels * sizeof *c->labels, cap * sizeof *c->labels);
327 c->caplabels = cap;
328 }
329 c->labels[c->nlabels++] = (struct label){x, y, line};
330}
331
332void tui_canvas_render(tui_arena *a, tui_canvas *c, const tui_block *block, tui_color background, tui_rect area,
333 tui_buffer *buf) {
334 if (block) tui_block_render(a, block, area, buf);
335 tui_rect ca = tui_block_inner_opt(block, area);
336 if (tui_rect_empty(ca)) return;
337 tui_buffer_set_style(buf, ca, tui_style_bg(tui_style_new(), background));
338 finish(c);
339 size_t width = ca.width;
340 for (layer *l = c->first; l; l = l->next) {
341 size_t n = (size_t)c->width * c->height;
342 for (size_t i = 0; i < n; i++) {
343 tui_cell *cell = tui_buffer_cell(buf, (uint16_t)(i % width + ca.x), (uint16_t)(i / width + ca.y));
344 if (!cell) continue;
345 const layer_cell *lc = &l->cells[i];
346 if (lc->has_symbol) {
347 char s[4];
348 size_t k = tui_utf8_encode(lc->symbol, s);
349 tui_cell_set_symbol(cell, s, k);
350 }
351 if (lc->has_fg) cell->fg = lc->fg;
352 if (lc->has_bg) cell->bg = lc->bg;
353 }
354 }
355 double left = c->xb[0], right = c->xb[1], top = c->yb[1], bottom = c->yb[0];
356 double w = fabs(c->xb[1] - c->xb[0]), h = fabs(c->yb[1] - c->yb[0]);
357 double rx = (double)(ca.width - 1), ry = (double)(ca.height - 1);
358 for (size_t i = 0; i < c->nlabels; i++) {
359 const struct label *lb = &c->labels[i];
360 if (!(lb->x >= left && lb->x <= right && lb->y <= top && lb->y >= bottom)) continue;
361 uint16_t x = (uint16_t)((lb->x - left) * rx / w) + ca.x;
362 uint16_t y = (uint16_t)((top - lb->y) * ry / h) + ca.y;
363 tui_buffer_set_line(buf, x, y, &lb->line, (uint16_t)(tui_rect_right(ca) - x));
364 }
365}
366
367void tui_canvas_widget(tui_arena *a, const tui_block *block, tui_color background, const double x_bounds[2],
368 const double y_bounds[2], tui_marker marker, void (*paint)(tui_canvas *c, void *arg), void *arg,
369 tui_rect area, tui_buffer *buf) {
370 tui_rect ca = tui_block_inner_opt(block, area);
371 tui_canvas *c = tui_canvas_new(a, ca.width, ca.height, x_bounds, y_bounds, marker);
372 if (paint && !tui_rect_empty(ca)) paint(c, arg);
373 tui_canvas_render(a, c, block, background, area, buf);
374}