skip to content
==> 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}