diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/draw.c | 456 | ||||
| -rw-r--r-- | src/main.c | 604 | ||||
| -rw-r--r-- | src/screen.c | 229 | ||||
| -rw-r--r-- | src/symbols-gl.c | 260 | ||||
| -rw-r--r-- | src/symbols-gl.h | 24 | ||||
| -rw-r--r-- | src/symbols-tool.c | 881 | ||||
| -rw-r--r-- | src/symbols.c | 185 | ||||
| -rw-r--r-- | src/symbols.h | 54 | ||||
| -rw-r--r-- | src/vega.h | 124 | ||||
| -rw-r--r-- | src/web.c | 453 | ||||
| -rw-r--r-- | src/width.c | 70 |
11 files changed, 0 insertions, 3340 deletions
diff --git a/src/draw.c b/src/draw.c deleted file mode 100644 index 31188c7..0000000 --- a/src/draw.c +++ /dev/null @@ -1,456 +0,0 @@ -#include "vega.h" -#include <stdlib.h> -#include <string.h> - -static const char *const tone_names[VEGA_TONES] = {"normal", "muted", "accent", "good", "warning", "danger"}; - -/* ---- CLI ------------------------------------------------------------ */ - -/* Text for a pipe: control characters become their pictures so a value never - * breaks a line. */ -static void put_text(FILE *out, const char *text, size_t length) { - for (size_t at = 0; at < length;) { - uint32_t codepoint; - size_t size = vega_decode(text, length, at, &codepoint); - if (vega_visible(codepoint) == codepoint) fwrite(text + at, 1, size, out); - else { - uint32_t picture = vega_visible(codepoint); - fputc(0xE0 | (int)(picture >> 12), out); - fputc(0x80 | (int)(picture >> 6 & 0x3F), out); - fputc(0x80 | (int)(picture & 0x3F), out); - } - at += size; - } -} - -static void pad(FILE *out, unsigned count) { - while (count--) fputc(' ', out); -} - -static const char *const colors[VEGA_TONES] = {"", "\033[2m", "\033[1;36m", "\033[32m", "\033[33m", "\033[31m"}; - -static void tone_start(FILE *out, VegaTone tone, bool color) { - if (color && tone) fputs(colors[tone], out); -} - -static void tone_end(FILE *out, VegaTone tone, bool color) { - if (color && tone) fputs("\033[0m", out); - else if (!color && tone) fprintf(out, " (%s)", tone_names[tone]); -} - -static void print_list(FILE *out, PureApp *app, const VegaPart *part, bool color) { - char text[VEGA_TEXT]; - size_t length, rows = vega_rows(app, part), focus = vega_focus(app, part); - for (size_t row = 0; row < rows; ++row) { - VegaTone tone = vega_tone(app, part, row); - fputs(row == focus ? "> " : " ", out); - tone_start(out, tone, color); - if (vega_text(app, (uint32_t)part->label, row, text, &length)) put_text(out, text, length); - if (part->badge >= 0 && vega_text(app, (uint32_t)part->badge, row, text, &length) && length) { - fputs(" [", out); - put_text(out, text, length); - fputc(']', out); - } - if (part->detail >= 0 && vega_text(app, (uint32_t)part->detail, row, text, &length) && length) { - fputs(" - ", out); - put_text(out, text, length); - } - tone_end(out, tone, color); - fputc('\n', out); - } -} - -static void print_table(FILE *out, PureApp *app, const VegaPart *part, bool color) { - const PureModule *m = app->module; - size_t rows = vega_rows(app, part), focus = vega_focus(app, part); - unsigned widths[PURE_MODULE_VIEWS]; - char text[VEGA_TEXT]; - size_t length; - for (unsigned c = 0; c < part->column_count; ++c) { - const char *name = m->view_names[part->columns[c]]; - widths[c] = vega_text_width(name, strlen(name)); - for (size_t row = 0; row < rows; ++row) - if (vega_text(app, part->columns[c], row, text, &length)) { - unsigned width = vega_text_width(text, length); - if (width > widths[c]) widths[c] = width; - } - } - fputs(" ", out); - for (unsigned c = 0; c < part->column_count; ++c) { - const char *name = m->view_names[part->columns[c]]; - fputs(name, out); - if (c + 1 < part->column_count) pad(out, widths[c] - (unsigned)strlen(name) + 2); - } - fputc('\n', out); - for (size_t row = 0; row < rows; ++row) { - VegaTone tone = vega_tone(app, part, row); - fputs(row == focus ? "> " : " ", out); - tone_start(out, tone, color); - for (unsigned c = 0; c < part->column_count; ++c) { - bool ok = vega_text(app, part->columns[c], row, text, &length); - if (!ok) { memcpy(text, "-", 2); length = 1; } - put_text(out, text, length); - if (c + 1 < part->column_count) pad(out, widths[c] - vega_text_width(text, length) + 2); - } - tone_end(out, tone, color); - fputc('\n', out); - } -} - -void vega_print(FILE *out, PureApp *app, const VegaScreen *screen, bool color) { - const PureModule *m = app->module; - char text[VEGA_TEXT]; - size_t length; - if (screen->title >= 0 && vega_text(app, (uint32_t)screen->title, 0, text, &length)) { - if (color) fputs("\033[1m", out); - put_text(out, text, length); - if (color) fputs("\033[0m", out); - fputc('\n', out); - } - for (unsigned i = 0; i < screen->count; ++i) { - const VegaPart *part = &screen->parts[i]; - const char *name = m->view_names[part->view]; - if (part->kind == VEGA_FIELD) { - fprintf(out, "%s: ", name); - if (vega_text(app, part->view, 0, text, &length)) put_text(out, text, length); - else fputs("-", out); - fputc('\n', out); - continue; - } - fprintf(out, "%s (%zu)\n", name, vega_rows(app, part)); - if (part->kind == VEGA_LIST) print_list(out, app, part, color); - else print_table(out, app, part, color); - } -} - -/* ---- NUI ------------------------------------------------------------ */ - -/* Spoken text: control characters become spaces, and trailing ones go. */ -static void speak(FILE *out, const char *text, size_t length) { - while (length && ((unsigned char)text[length - 1] <= 0x20)) --length; - for (size_t i = 0; i < length; ++i) fputc((unsigned char)text[i] < 0x20 ? ' ' : text[i], out); -} - -/* "Item 2 of 3: milk, done." Tones other than normal and muted are said. */ -static void say_focus(FILE *out, PureApp *app, const VegaPart *part) { - const PureModule *m = app->module; - char text[VEGA_TEXT]; - size_t length, rows = vega_rows(app, part); - if (!rows) { fprintf(out, "%s is empty.\n", m->view_names[part->view]); return; } - size_t focus = vega_focus(app, part); - if (focus >= rows) { fprintf(out, "%s: none of %zu focused.\n", m->view_names[part->view], rows); return; } - fprintf(out, "Item %zu of %zu: ", focus + 1, rows); - VegaTone tone = vega_tone(app, part, focus); - if (tone >= VEGA_ACCENT) fprintf(out, "%s, ", tone_names[tone]); - if (part->kind == VEGA_LIST) { - if (vega_text(app, (uint32_t)part->label, focus, text, &length)) speak(out, text, length); - int more[] = {part->badge, part->detail}; - for (unsigned k = 0; k < 2; ++k) - if (more[k] >= 0 && vega_text(app, (uint32_t)more[k], focus, text, &length) && length) { - fputs(", ", out); - speak(out, text, length); - } - } else { - for (unsigned c = 0; c < part->column_count; ++c) { - fprintf(out, "%s%s ", c ? ", " : "", m->view_names[part->columns[c]]); - if (vega_text(app, part->columns[c], focus, text, &length)) speak(out, text, length); - else fputs("none", out); - } - } - fputs(".\n", out); -} - -void vega_say(FILE *out, PureApp *app, const VegaScreen *screen, bool all) { - const PureModule *m = app->module; - char text[VEGA_TEXT]; - size_t length; - if (!all) { - /* The focused item where a list shows it, else the first list. */ - const VegaPart *first = NULL; - for (unsigned i = 0; i < screen->count; ++i) { - const VegaPart *part = &screen->parts[i]; - if (part->kind == VEGA_FIELD) continue; - if (!first) first = part; - if (vega_focus(app, part) < vega_rows(app, part)) { say_focus(out, app, part); return; } - } - if (first) say_focus(out, app, first); - return; - } - if (screen->title >= 0 && vega_text(app, (uint32_t)screen->title, 0, text, &length)) { - speak(out, text, length); - fputs(".\n", out); - } - for (unsigned i = 0; i < screen->count; ++i) { - const VegaPart *part = &screen->parts[i]; - const char *name = m->view_names[part->view]; - if (part->kind == VEGA_FIELD) { - fprintf(out, "%s: ", name); - if (vega_text(app, part->view, 0, text, &length)) speak(out, text, length); - else fputs("none", out); - fputs(".\n", out); - continue; - } - size_t rows = vega_rows(app, part); - fprintf(out, "%s, %zu item%s.\n", name, rows, rows == 1 ? "" : "s"); - if (rows) say_focus(out, app, part); - } -} - -/* ---- TUI ------------------------------------------------------------ */ - -bool vega_grid(VegaGrid *grid, unsigned width, unsigned height) { - size_t count = (size_t)width * height; - VegaCell *cells = calloc(count ? count : 1, sizeof(*cells)), *shown = calloc(count ? count : 1, sizeof(*shown)); - struct VegaRow *rows = calloc(height ? height : 1, sizeof(*rows)); - if (!cells || !shown || !rows) { free(cells); free(shown); free(rows); return false; } - for (unsigned r = 0; r < height; ++r) rows[r].part = -1; - free(grid->cells); - free(grid->shown); - free(grid->rows); - grid->cells = cells; - grid->shown = shown; - grid->rows = rows; - grid->painted = false; - grid->width = width; - grid->height = height; - return true; -} - -void vega_grid_free(VegaGrid *grid) { - free(grid->cells); - free(grid->shown); - free(grid->rows); - *grid = (VegaGrid){0}; -} - -typedef struct { VegaGrid *grid; unsigned row, column, end; uint8_t tone; bool focus, strong; } Pen; - -/* Write text from the pen's column up to its end; a wide character that - * does not fit is left out, and a mark joins the character before it. */ -static void write_text(Pen *pen, const char *text, size_t length) { - VegaCell *line = pen->grid->cells + (size_t)pen->row * pen->grid->width; - for (size_t at = 0; at < length;) { - uint32_t codepoint; - at += vega_decode(text, length, at, &codepoint); - codepoint = vega_visible(codepoint); - unsigned width = vega_width(codepoint); - if (!width) { - if (pen->column && !line[pen->column - 1].mark) { - unsigned base = pen->column - 1; - while (base && !line[base].ch) --base; - line[base].mark = codepoint; - } - continue; - } - if (pen->column + width > pen->end) { pen->column = pen->end; return; } - line[pen->column] = (VegaCell){codepoint, 0, pen->tone, pen->focus, pen->strong}; - if (width == 2) line[pen->column + 1] = (VegaCell){0, 0, pen->tone, pen->focus, pen->strong}; - pen->column += width; - } -} - -static void write_cstr(Pen *pen, const char *text) { write_text(pen, text, strlen(text)); } - -/* Fill the rest of the pen's span with spaces in its style (a focused row - * is highlighted across). */ -static void fill(Pen *pen) { - VegaCell *line = pen->grid->cells + (size_t)pen->row * pen->grid->width; - while (pen->column < pen->end) line[pen->column++] = (VegaCell){' ', 0, pen->tone, pen->focus, pen->strong}; -} - -/* Text cut to width with an ellipsis. */ -static void write_fit(Pen *pen, const char *text, size_t length, unsigned width) { - unsigned end = pen->end; - if (pen->column + width < end) pen->end = pen->column + width; - if (vega_text_width(text, length) > pen->end - pen->column && pen->end > pen->column) { - --pen->end; - write_text(pen, text, length); - pen->end = end; - write_cstr(pen, "\xe2\x80\xa6"); - } else { - write_text(pen, text, length); - pen->end = end; - } -} - -static const char *key_symbol(const char *key) { - static const struct { const char *key, *symbol; } symbols[] = { - {"ArrowUp", "\xe2\x86\x91"}, {"ArrowDown", "\xe2\x86\x93"}, {"ArrowLeft", "\xe2\x86\x90"}, - {"ArrowRight", "\xe2\x86\x92"}, {"Space", "Space"}, {"Enter", "Enter"}, {"Backspace", "Bksp"}, - {"Delete", "Del"}, {"Escape", "Esc"}, {"PageUp", "PgUp"}, {"PageDown", "PgDn"}}; - for (size_t i = 0; i < sizeof(symbols) / sizeof(*symbols); ++i) - if (!strcmp(symbols[i].key, key)) return symbols[i].symbol; - if (!strncmp(key, "Key", 3)) return key + 3; - if (!strncmp(key, "Digit", 5)) return key + 5; - return key; -} - -static void draw_list(VegaGrid *grid, PureApp *app, const VegaPart *part, int index, size_t first, unsigned top, - unsigned height) { - char text[VEGA_TEXT]; - size_t length, rows = vega_rows(app, part), focus = vega_focus(app, part); - for (unsigned r = 0; r < height && first + r < rows; ++r) { - size_t row = first + r; - grid->rows[top + r] = (struct VegaRow){index, row}; - bool focused = row == focus; - VegaTone tone = vega_tone(app, part, row); - Pen pen = {grid, top + r, 0, grid->width, (uint8_t)tone, focused, false}; - write_cstr(&pen, focused ? "> " : " "); - char badge[VEGA_TEXT]; - size_t badge_length = 0; - if (part->badge >= 0 && !vega_text(app, (uint32_t)part->badge, row, badge, &badge_length)) badge_length = 0; - unsigned badge_width = badge_length ? vega_text_width(badge, badge_length) + 2 : 0; - unsigned room = grid->width > pen.column + badge_width ? grid->width - pen.column - badge_width : 0; - if (vega_text(app, (uint32_t)part->label, row, text, &length)) write_fit(&pen, text, length, room); - if (part->detail >= 0 && vega_text(app, (uint32_t)part->detail, row, text, &length) && length) { - unsigned used = pen.column; - uint8_t saved = pen.tone; - pen.tone = pen.tone ? pen.tone : VEGA_MUTED; - write_cstr(&pen, " "); - unsigned left = grid->width > badge_width + pen.column ? grid->width - badge_width - pen.column : 0; - if (pen.column - used == 2) write_fit(&pen, text, length, left); - pen.tone = saved; - } - if (badge_length && grid->width >= badge_width) { - /* The badge ends at the last column, two cells after the label at least. */ - unsigned start = grid->width - (badge_width - 2); - if (pen.column < start) fill(&(Pen){grid, pen.row, pen.column, start, pen.tone, focused, false}); - pen.column = start > pen.column ? start : pen.column; - write_fit(&pen, badge, badge_length, badge_width - 2); - } - fill(&pen); - } -} - -static void draw_table(VegaGrid *grid, PureApp *app, const VegaPart *part, int index, size_t first, unsigned top, - unsigned height) { - const PureModule *m = app->module; - char text[VEGA_TEXT]; - size_t length, rows = vega_rows(app, part), focus = vega_focus(app, part); - unsigned visible = height ? height - 1 : 0, widths[PURE_MODULE_VIEWS], total = 2; - /* Columns as wide as their header and the rows on screen, shared out when too wide. */ - for (unsigned c = 0; c < part->column_count; ++c) { - const char *name = m->view_names[part->columns[c]]; - widths[c] = vega_text_width(name, strlen(name)); - for (unsigned r = 0; r < visible && first + r < rows; ++r) - if (vega_text(app, part->columns[c], first + r, text, &length)) { - unsigned width = vega_text_width(text, length); - if (width > widths[c]) widths[c] = width; - } - total += widths[c] + 2; - } - if (part->column_count && total > grid->width) { - unsigned share = grid->width > 2 + 2 * part->column_count ? (grid->width - 2) / part->column_count - 2 : 1; - for (unsigned c = 0; c < part->column_count; ++c) if (widths[c] > share) widths[c] = share; - } - if (!height) return; - Pen head = {grid, top, 2, grid->width, VEGA_MUTED, false, true}; - for (unsigned c = 0; c < part->column_count; ++c) { - const char *name = m->view_names[part->columns[c]]; - unsigned start = head.column; - write_fit(&head, name, strlen(name), widths[c]); - head.column = start + widths[c] + 2 < grid->width ? start + widths[c] + 2 : grid->width; - } - for (unsigned r = 0; r < visible && first + r < rows; ++r) { - size_t row = first + r; - bool focused = row == focus; - grid->rows[top + 1 + r] = (struct VegaRow){index, row}; - Pen pen = {grid, top + 1 + r, 0, grid->width, (uint8_t)vega_tone(app, part, row), focused, false}; - write_cstr(&pen, focused ? "> " : " "); - for (unsigned c = 0; c < part->column_count; ++c) { - unsigned start = pen.column, end = start + widths[c] + 2 < grid->width ? start + widths[c] + 2 : grid->width; - if (vega_text(app, part->columns[c], row, text, &length)) write_fit(&pen, text, length, widths[c]); - else write_cstr(&pen, "-"); - if (pen.column < end) fill(&(Pen){grid, pen.row, pen.column, end, pen.tone, focused, false}); - pen.column = end; - } - fill(&pen); - } -} - -bool vega_hit(const VegaGrid *grid, unsigned line, unsigned *part, size_t *item) { - if (line >= grid->height || grid->rows[line].part < 0) return false; - *part = (unsigned)grid->rows[line].part; - *item = grid->rows[line].item; - return true; -} - -void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const char *prompt, size_t prompt_length, - bool typing) { - const PureModule *m = app->module; - for (size_t i = 0; i < (size_t)grid->width * grid->height; ++i) grid->cells[i] = (VegaCell){' ', 0, 0, false, false}; - for (unsigned r = 0; r < grid->height; ++r) grid->rows[r].part = -1; - grid->cursor_row = grid->cursor_column = -1; - if (!grid->width || !grid->height) return; - char text[VEGA_TEXT]; - size_t length; - unsigned row = 0, bottom = grid->height - 1 - (screen->prompt >= 0 && grid->height > 1); - if (screen->title >= 0 && row < bottom && vega_text(app, (uint32_t)screen->title, 0, text, &length)) { - Pen pen = {grid, row++, 0, grid->width, VEGA_NORMAL, false, true}; - write_fit(&pen, text, length, grid->width); - } - unsigned groups = 0; - for (unsigned i = 0; i < screen->count; ++i) { - const VegaPart *part = &screen->parts[i]; - if (part->kind != VEGA_FIELD) { ++groups; continue; } - if (row >= bottom) continue; - Pen pen = {grid, row++, 0, grid->width, VEGA_MUTED, false, false}; - write_cstr(&pen, m->view_names[part->view]); - write_cstr(&pen, " "); - pen.tone = VEGA_NORMAL; - if (vega_text(app, part->view, 0, text, &length)) write_fit(&pen, text, length, grid->width); - else { pen.tone = VEGA_MUTED; write_cstr(&pen, "-"); } - } - /* Lists and tables share the rows left; each keeps its focused row in view - * and only its visible rows are evaluated. */ - unsigned left = bottom > row ? bottom - row : 0; - for (unsigned i = 0, group = 0; i < screen->count; ++i) { - const VegaPart *part = &screen->parts[i]; - if (part->kind == VEGA_FIELD) continue; - unsigned share = left / groups + (group < left % groups); - ++group; - if (!share) continue; - size_t rows = vega_rows(app, part), focus = vega_focus(app, part); - Pen head = {grid, row, 0, grid->width, VEGA_NORMAL, false, true}; - write_cstr(&head, m->view_names[part->view]); - char count[32]; - snprintf(count, sizeof(count), " (%zu)", rows); - head.strong = false; - head.tone = VEGA_MUTED; - write_cstr(&head, count); - unsigned area = share - 1, visible = part->kind == VEGA_TABLE && area ? area - 1 : area; - size_t *first = &grid->first[i]; - *first = vega_scroll(*first, focus, visible, rows); - if (part->kind == VEGA_LIST) draw_list(grid, app, part, (int)i, *first, row + 1, area); - else draw_table(grid, app, part, (int)i, *first, row + 1, area); - row += share; - } - /* Keys the app answers to, then the text being typed. */ - Pen help = {grid, bottom, 0, grid->width, VEGA_MUTED, false, false}; - if (screen->prompt >= 0) write_cstr(&help, typing ? "Tab keys " : "Tab type "); - for (uint32_t i = 0; i < m->inputs; ++i) { - if (m->input[i].kind != PURE_KIND_KEY) continue; - bool bound = false; - for (uint32_t h = 0; h < m->handlers && !bound; ++h) bound = m->handler[h].event == 1 + 2 * i; - if (!bound) continue; - write_cstr(&help, key_symbol(m->input_keys[i])); - write_cstr(&help, " "); - write_cstr(&help, m->input_names[i]); - write_cstr(&help, " "); - } - if (screen->prompt >= 0 && grid->height > 1) { - Pen pen = {grid, grid->height - 1, 0, grid->width, typing ? VEGA_ACCENT : VEGA_MUTED, false, false}; - write_cstr(&pen, m->input_names[screen->prompt]); - write_cstr(&pen, "> "); - pen.tone = typing ? VEGA_NORMAL : VEGA_MUTED; - /* Show the end of a long entry. */ - size_t start = 0; - unsigned room = grid->width > pen.column + 1 ? grid->width - pen.column - 1 : 0; - while (start < prompt_length && vega_text_width(prompt + start, prompt_length - start) > room) { - uint32_t skip; - start += vega_decode(prompt, prompt_length, start, &skip); - } - write_text(&pen, prompt + start, prompt_length - start); - if (typing) { grid->cursor_row = (int)pen.row; grid->cursor_column = (int)pen.column; } - } -} diff --git a/src/main.c b/src/main.c deleted file mode 100644 index 82131ab..0000000 --- a/src/main.c +++ /dev/null @@ -1,604 +0,0 @@ -#define _DEFAULT_SOURCE -#include "vega.h" -#include "module-store.h" -#include "store.h" -#include <poll.h> -#include <signal.h> -#include <stdlib.h> -#include <string.h> -#include <sys/ioctl.h> -#include <termios.h> -#include <time.h> -#include <unistd.h> - -/* vega SURFACE MODULE [--store PATH]: one module, one screen, several surfaces. - * check what the module seems to mean but does not meet (exit 1) - * tui interactive terminal - * tui --dump WxH the terminal's frames as text, driven by a script - * tui --paint WxH the bytes each frame sends to the terminal, driven by a script - * cli plain lines for pipes, driven by a script - * say sentences (the first natural-language surface), driven by a script - * web [--listen ADDRESS] [--port N] - * a web page (a GUI), on 127.0.0.1 unless another address is given - * With --store the app's states live in that tree store: the app continues - * where it was, whichever surface runs it. With --watch (tui, web) a module - * written again replaces the running one and states keep their values by - * name: aup watch app.aup -o app.pmod beside it makes a saved edit show at once. - * Script commands, one per line: key KEYNAME | text HEX|- | type HEX | enter | - * tab | frame [N] | point VIEW ROW | click LINE COLUMN | time MS | tick TIMER | swap MODULE | show. key presses and - * releases the input bound to that key; text commits text to the screen's text - * input; type, enter and tab act on the TUI's input line; point focuses a row - * (0 first) of a list or table; click is a mouse click on the last shown TUI - * frame (1 first, as terminals report it); time sets the time the next - * events see (live surfaces use the clock) and tick ticks a timer (live - * surfaces tick them on time); swap replaces the module as --watch does. */ - -static PureModule modules[2]; /* the running one (app.module) and the next */ -static unsigned current; -static PureApp app; -static VegaScreen screen; -static VegaGrid *drawn; /* the TUI's last frame, which a click lands on */ -static char entry[VEGA_TEXT]; -static size_t entry_length; -static bool typing; - -/* The module file last read, and a digest of its bytes. A loaded module - * keeps nothing of them, so one buffer, grown to the largest, serves every load. */ -static unsigned char *module_bytes; -static size_t module_capacity; -static uint64_t module_digest; - -/* Read a module file whole into module_bytes: its size, or -1. */ -static long read_module(const char *path) { - FILE *file = fopen(path, "rb"); - size_t size = 0; - bool ok = file != NULL; - while (ok && !feof(file)) { - if (size == module_capacity) { - size_t capacity = module_capacity ? module_capacity * 2 : 1 << 16; - unsigned char *grown = capacity <= (8u << 20) ? realloc(module_bytes, capacity) : NULL; - if (!grown) { ok = false; break; } - module_bytes = grown; - module_capacity = capacity; - } - size += fread(module_bytes + size, 1, module_capacity - size, file); - ok = !ferror(file); - } - if (file) fclose(file); - if (!ok) return -1; - uint64_t h = UINT64_C(0xcbf29ce484222325) ^ size; - for (size_t i = 0; i < size; ++i) h = (h ^ module_bytes[i]) * UINT64_C(0x100000001b3); - module_digest = h; - return (long)size; -} - -/* Decode the module that read_module read from path. */ -static bool decode(const char *path, long size, PureModule *module) { - if (size < 0) { fprintf(stderr, "vega: cannot read %s\n", path); return false; } - char error[128]; - if (pure_module_load(module_bytes, (size_t)size, module, error, sizeof(error)) != PURE_OK) { - fprintf(stderr, "vega: %s: %s\n", path, error); - return false; - } - return true; -} - -static bool load_module(const char *path, PureModule *module) { return decode(path, read_module(path), module); } - -static void print_finding(void *path, const char *name, const char *message) { - printf("%s: %s: %s\n", (const char *)path, name, message); -} - -static bool load(const char *path, const char *store) { - if (!load_module(path, &modules[current])) return false; - if (store && pure_store_init(store) != PURE_OK) { fprintf(stderr, "vega: cannot open store %s\n", store); return false; } - PureResult started = pure_app_start(&app, &modules[current], store ? pure_tree_data(store) : (PureData){0}); - if (started != PURE_OK) { fprintf(stderr, "vega: cannot start (%d)\n", (int)started); return false; } - vega_screen(&screen, &modules[current]); - typing = screen.prompt >= 0; - return true; -} - -/* Replace the running app's module with the one read from path, states - * keeping their values by name; a module that does not load or replace - * leaves the app as it was. */ -static bool swap(const char *path, long size) { - PureModule *next = &modules[!current]; - if (!decode(path, size, next)) return false; - PureResult replaced = pure_app_replace(&app, next); - if (replaced != PURE_OK) { - fprintf(stderr, "vega: keeping the running module: cannot replace it (%d)\n", (int)replaced); - return false; - } - bool had_prompt = screen.prompt >= 0; - current = !current; - vega_screen(&screen, next); - typing = screen.prompt >= 0 && (typing || !had_prompt); - return true; -} - -/* --watch: the module file's bytes when last read (their digest), so a - * module written again is seen however soon and whatever its size or file - * times. Read at most five times a second, however often asked; bytes that - * do not load are not tried again. */ -static const char *watched; -static uint64_t watched_digest; -static struct timespec watched_at; - -static bool module_changed(void) { - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - if (!watched || (now.tv_sec - watched_at.tv_sec) * 1000000000L + now.tv_nsec - watched_at.tv_nsec < 200000000L) - return false; - watched_at = now; - long size = read_module(watched); - if (size < 0 || module_digest == watched_digest) return false; /* a missing file is looked for again */ - watched_digest = module_digest; - return swap(watched, size); -} - -static int key_input(const char *key) { return vega_key_input(app.module, key); } - -/* A key press and release; false when the app has no such key or the press failed. */ -static bool press(const char *key, int *input) { - *input = key_input(key); - return *input >= 0 && vega_press(&app, key); -} - -static bool commit(const char *text, size_t length) { return vega_commit(&app, &screen, text, length); } - -static const VegaPart *part_named(const char *name) { - for (unsigned i = 0; i < screen.count; ++i) - if (screen.parts[i].kind != VEGA_FIELD && !strcmp(app.module->view_names[screen.parts[i].view], name)) - return &screen.parts[i]; - return NULL; -} - -/* A mouse button on a line of the last frame: a click focuses the row there; - * the wheel moves the focus of the list or table under it. */ -static bool mouse(unsigned button, unsigned line) { - unsigned index; - size_t item; - if (!drawn || !vega_hit(drawn, line, &index, &item)) return false; - const VegaPart *part = &screen.parts[index]; - if (button == 64 || button == 65) { - size_t focus = vega_focus(&app, part), rows = vega_rows(&app, part); - if (focus < rows) { /* else the wheel focuses the row under it */ - if (button == 64 ? !focus : focus + 1 >= rows) return false; - item = button == 64 ? focus - 1 : focus + 1; - } - } else if (button) return false; - return vega_point(&app, part, item); -} - -static int hex(char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; } - -static size_t unhex(const char *digits, char *out, size_t size) { - size_t length = 0; - if (!strcmp(digits, "-")) return 0; - for (size_t i = 0; digits[i] && digits[i + 1] && length < size; i += 2) - if (hex(digits[i]) >= 0 && hex(digits[i + 1]) >= 0) out[length++] = (char)(hex(digits[i]) << 4 | hex(digits[i + 1])); - return length; -} - -/* ---- terminal ------------------------------------------------------- */ - -typedef struct { char *bytes; size_t used, capacity; } Out; - -static void out_put(Out *out, const char *bytes, size_t size) { - if (out->used + size > out->capacity) { - size_t capacity = out->capacity ? out->capacity : 4096; - while (capacity < out->used + size) capacity *= 2; - char *grown = realloc(out->bytes, capacity); - if (!grown) return; - out->bytes = grown; - out->capacity = capacity; - } - memcpy(out->bytes + out->used, bytes, size); - out->used += size; -} - -static void out_codepoint(Out *out, uint32_t c) { - char b[4]; - size_t n = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4; - if (n == 1) b[0] = (char)c; - else if (n == 2) { b[0] = (char)(0xC0 | c >> 6); b[1] = (char)(0x80 | (c & 0x3F)); } - else if (n == 3) { b[0] = (char)(0xE0 | c >> 12); b[1] = (char)(0x80 | (c >> 6 & 0x3F)); b[2] = (char)(0x80 | (c & 0x3F)); } - else { - b[0] = (char)(0xF0 | c >> 18); b[1] = (char)(0x80 | (c >> 12 & 0x3F)); - b[2] = (char)(0x80 | (c >> 6 & 0x3F)); b[3] = (char)(0x80 | (c & 0x3F)); - } - out_put(out, b, n); -} - -static const char *const tone_names[VEGA_TONES] = {"normal", "muted", "accent", "good", "warning", "danger"}; - -/* The grid as text, one line per row: '>' marks a focused row, and a row's - * tone (of its first character) follows a tab when it is not normal. */ -static void dump(const VegaGrid *grid) { - Out out = {0}; - for (unsigned r = 0; r < grid->height; ++r) { - const VegaCell *line = grid->cells + (size_t)r * grid->width; - bool focus = false; - int tone = -1; - size_t start = out.used, end; - for (unsigned c = 0; c < grid->width; ++c) { - focus = focus || line[c].focus; - if (tone < 0 && line[c].ch && line[c].ch != ' ') tone = line[c].tone; - } - out_put(&out, focus ? ">" : "|", 1); - for (unsigned c = 0; c < grid->width; ++c) { - if (!line[c].ch) continue; - out_codepoint(&out, line[c].ch); - if (line[c].mark) out_codepoint(&out, line[c].mark); - } - end = out.used; - while (end > start + 1 && out.bytes[end - 1] == ' ') --end; - out.used = end; - if (tone > 0) { out_put(&out, "\t", 1); out_put(&out, tone_names[tone], strlen(tone_names[tone])); } - out_put(&out, "\n", 1); - } - fwrite(out.bytes, 1, out.used, stdout); - free(out.bytes); -} - -static const char *const sgr_tones[VEGA_TONES] = {"", ";2", ";1;36", ";32", ";33", ";31"}; - -static bool same_cell(const VegaCell *a, const VegaCell *b) { - return a->ch == b->ch && a->mark == b->mark && a->tone == b->tone && a->focus == b->focus && a->strong == b->strong; -} - -/* The terminal bytes that turn what it shows into the grid: each run of - * changed cells after a cursor move (a short unchanged gap stays in the run, - * being cheaper than another move), everything after a resize. */ -static void paint(VegaGrid *grid, Out *out) { - bool all = !grid->painted; - int style = -1; - char code[48]; - if (all) out_put(out, "\033[2J", 4); - for (unsigned r = 0; r < grid->height; ++r) { - const VegaCell *line = grid->cells + (size_t)r * grid->width, *old = grid->shown + (size_t)r * grid->width; - for (unsigned c = 0; c < grid->width;) { - if (!all && same_cell(&line[c], &old[c])) { ++c; continue; } - unsigned start = c, end = c + 1; - for (unsigned k = c + 1, gap = 0; k < grid->width && gap <= 6; ++k) { - if (all || !same_cell(&line[k], &old[k])) { end = k + 1; gap = 0; } - else ++gap; - } - while (start && !line[start].ch) --start; /* the second half of a wide character: from its first */ - out_put(out, code, (size_t)snprintf(code, sizeof(code), "\033[%u;%uH", r + 1, start + 1)); - for (unsigned k = start; k < end; ++k) { - if (!line[k].ch) continue; - int next = line[k].tone | line[k].focus << 4 | line[k].strong << 5; - if (next != style) { - out_put(out, code, (size_t)snprintf(code, sizeof(code), "\033[0%s%s%sm", line[k].strong ? ";1" : "", - line[k].focus ? ";7" : "", - sgr_tones[line[k].tone < VEGA_TONES ? line[k].tone : 0])); - style = next; - } - out_codepoint(out, line[k].ch); - if (line[k].mark) out_codepoint(out, line[k].mark); - } - c = end; - } - } - if (style > 0) out_put(out, "\033[0m", 4); - if (grid->cursor_row >= 0) - out_put(out, code, (size_t)snprintf(code, sizeof(code), "\033[%d;%dH\033[?25h", grid->cursor_row + 1, - grid->cursor_column + 1)); - else out_put(out, "\033[?25l", 6); - memcpy(grid->shown, grid->cells, (size_t)grid->width * grid->height * sizeof(*grid->cells)); - grid->painted = true; -} - -static void paint_terminal(VegaGrid *grid) { - Out out = {0}; - paint(grid, &out); - if (write(STDOUT_FILENO, out.bytes, out.used) < 0) { /* the terminal went away; the loop ends on read */ } - free(out.bytes); -} - -static struct termios saved_terminal; -static bool raw_terminal; -static volatile sig_atomic_t resized = 1; - -static void on_resize(int signal_number) { (void)signal_number; resized = 1; } - -static void restore_terminal(void) { - if (!raw_terminal) return; - tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_terminal); - const char *leave = "\033[?1006l\033[?1000l\033[0m\033[?25h\033[?1049l"; - if (write(STDOUT_FILENO, leave, strlen(leave)) < 0) { /* nothing left to tell */ } - raw_terminal = false; -} - -static bool raw_mode(void) { - if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO) || tcgetattr(STDIN_FILENO, &saved_terminal)) return false; - struct termios t = saved_terminal; - t.c_iflag &= ~(tcflag_t)(BRKINT | ICRNL | INPCK | ISTRIP | IXON); - t.c_oflag &= ~(tcflag_t)OPOST; - t.c_cflag |= CS8; - t.c_lflag &= ~(tcflag_t)(ECHO | ICANON | IEXTEN | ISIG); - t.c_cc[VMIN] = 1; - t.c_cc[VTIME] = 0; - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t)) return false; - raw_terminal = true; - atexit(restore_terminal); - const char *enter = "\033[?1049h\033[?25l\033[?1000h\033[?1006h"; /* mouse buttons, reported as SGR */ - return write(STDOUT_FILENO, enter, strlen(enter)) >= 0; -} - -/* Whether the app takes Tab itself; otherwise Tab switches typing and keys. */ -static bool tab_switches(void) { return screen.prompt >= 0 && key_input("Tab") < 0; } - -/* One key from the terminal, as typed text or as a Pure key name. */ -static void handle_key(const char *name) { - int input; - if (!strcmp(name, "Tab") && tab_switches()) { typing = !typing; return; } - if (typing && !strcmp(name, "Enter")) { - if (entry_length && commit(entry, entry_length)) entry_length = 0; - return; - } - if (typing && !strcmp(name, "Backspace")) { - while (entry_length && ((unsigned char)entry[entry_length - 1] & 0xC0) == 0x80) --entry_length; - if (entry_length) --entry_length; - return; - } - press(name, &input); -} - -static void handle_text(const char *bytes, size_t length) { - if (typing) { - if (entry_length + length < sizeof(entry)) { memcpy(entry + entry_length, bytes, length); entry_length += length; } - return; - } - if (length != 1) return; - char c = bytes[0], name[16]; - if (c == ' ') strcpy(name, "Space"); - else if (c >= 'a' && c <= 'z') snprintf(name, sizeof(name), "Key%c", c - 'a' + 'A'); - else if (c >= 'A' && c <= 'Z') snprintf(name, sizeof(name), "Key%c", c); - else if (c >= '0' && c <= '9') snprintf(name, sizeof(name), "Digit%c", c); - else return; - handle_key(name); -} - -/* Parse terminal input: escape sequences, control keys and UTF-8 text. */ -static bool handle_input(const unsigned char *in, size_t length) { - static const struct { const char *sequence, *key; } sequences[] = { - {"[A", "ArrowUp"}, {"[B", "ArrowDown"}, {"[C", "ArrowRight"}, {"[D", "ArrowLeft"}, {"[H", "Home"}, - {"[F", "End"}, {"OA", "ArrowUp"}, {"OB", "ArrowDown"}, {"OC", "ArrowRight"}, {"OD", "ArrowLeft"}, - {"OH", "Home"}, {"OF", "End"}, {"[1~", "Home"}, {"[7~", "Home"}, {"[4~", "End"}, {"[8~", "End"}, - {"[3~", "Delete"}, {"[5~", "PageUp"}, {"[6~", "PageDown"}}; - for (size_t at = 0; at < length;) { - unsigned char c = in[at]; - if (c == 0x03 || c == 0x11) return false; /* Ctrl-C, Ctrl-Q */ - if (c == 0x1B && at + 2 < length && in[at + 1] == '[' && in[at + 2] == '<') { - /* A mouse report: ESC [ < button ; column ; line, M on press, m on release. */ - unsigned fields[3] = {0, 0, 0}, field = 0; - size_t end = at + 3; - for (; end < length && field < 3; ++end) { - if (in[end] >= '0' && in[end] <= '9') fields[field] = fields[field] * 10 + (unsigned)(in[end] - '0'); - else if (in[end] == ';') ++field; - else break; - } - if (end < length && in[end] == 'M' && field == 2 && fields[2]) mouse(fields[0], fields[2] - 1); - at = end + 1; - continue; - } - if (c == 0x1B) { - size_t matched = 0; - for (size_t i = 0; i < sizeof(sequences) / sizeof(*sequences) && !matched; ++i) { - size_t n = strlen(sequences[i].sequence); - if (at + 1 + n <= length && !memcmp(in + at + 1, sequences[i].sequence, n)) { - handle_key(sequences[i].key); - matched = 1 + n; - } - } - if (!matched) { handle_key("Escape"); matched = 1; } - at += matched; - continue; - } - if (c == '\r' || c == '\n') handle_key("Enter"); - else if (c == 0x7F || c == 0x08) handle_key("Backspace"); - else if (c == '\t') handle_key("Tab"); - else if (c >= 0x20) { - size_t n = c < 0x80 ? 1 : c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4; - if (at + n > length) n = length - at; - handle_text((const char *)in + at, n); - at += n; - continue; - } - ++at; - } - return true; -} - -static int interactive(void) { - if (!raw_mode()) { fputs("vega: the tui needs a terminal (or use --dump WxH)\n", stderr); return 2; } - signal(SIGWINCH, on_resize); - VegaGrid grid = {0}; - drawn = &grid; - VegaTimers timers; - vega_timers_start(&timers, app.module, vega_now()); - for (;;) { - if (resized) { - struct winsize size; - resized = 0; - if (!ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) && size.ws_col && size.ws_row) - vega_grid(&grid, size.ws_col, size.ws_row); - else vega_grid(&grid, 80, 24); - } - /* Tick what is due, draw, then wait for input, a frame or the next tick. */ - bool ticked = false; - long due = vega_timers_run(&timers, &app, vega_now(), &ticked); - vega_draw(&grid, &app, &screen, entry, entry_length, typing); - paint_terminal(&grid); - struct pollfd wait = {STDIN_FILENO, POLLIN, 0}; - int timeout = screen.ticks ? 16 : watched ? 200 : -1; - if (due >= 0 && (timeout < 0 || due < timeout)) timeout = (int)due; - int ready = poll(&wait, 1, timeout); - app.now = vega_now(); - if (ready == 0) { - if (screen.ticks) pure_app_frame(&app); - if (module_changed()) vega_timers_start(&timers, app.module, app.now); - continue; - } - if (ready < 0) continue; /* a signal, such as a resize */ - unsigned char in[256]; - ssize_t got = read(STDIN_FILENO, in, sizeof(in)); - if (got <= 0 || !handle_input(in, (size_t)got)) break; - } - restore_terminal(); - drawn = NULL; - vega_grid_free(&grid); - return 0; -} - -/* ---- scripts -------------------------------------------------------- */ - -typedef enum { SURFACE_CLI, SURFACE_SAY, SURFACE_TUI, SURFACE_WEB } Surface; - -/* painting: show writes the terminal bytes of the frame, not the grid. */ -static int script(Surface surface, unsigned width, unsigned height, bool painting) { - static char line[1 << 16]; - VegaGrid grid = {0}; - if (surface == SURFACE_TUI && !vega_grid(&grid, width, height)) return 2; - bool shown = false; - drawn = surface == SURFACE_TUI ? &grid : NULL; - bool color = surface == SURFACE_CLI && isatty(STDOUT_FILENO) && !getenv("NO_COLOR"); - unsigned long counted = vega_evaluations(); - while (fgets(line, sizeof(line), stdin)) { - char command[16] = "", argument[sizeof(line)] = ""; - line[strcspn(line, "\r\n")] = 0; - if (sscanf(line, "%15s %65535s", command, argument) < 1) continue; - bool acted = true, ok = true; - int input = -1; - const char *verb = "do that"; - if (!strcmp(command, "key")) { - ok = press(argument, &input); - if (input < 0) fprintf(stderr, "vega: no input answers %s\n", argument); - } else if (!strcmp(command, "text")) { - char text[VEGA_TEXT]; - ok = commit(text, unhex(argument, text, sizeof(text))); - } else if (!strcmp(command, "type")) { - entry_length += unhex(argument, entry + entry_length, sizeof(entry) - 1 - entry_length); - acted = false; - } else if (!strcmp(command, "enter")) { - handle_key("Enter"); - } else if (!strcmp(command, "tab")) { - handle_key("Tab"); - acted = false; - } else if (!strcmp(command, "point")) { - char name[PURE_MODULE_NAME]; - long row = -1; - const VegaPart *part = sscanf(line, "%*s %31s %ld", name, &row) == 2 ? part_named(name) : NULL; - verb = "point"; - if (!part) fprintf(stderr, "vega: no list or table %s\n", name); - ok = part && row >= 0 && vega_point(&app, part, (size_t)row); - } else if (!strcmp(command, "click")) { - unsigned at_line = 0, at_column = 0; - verb = "point"; - if (surface != SURFACE_TUI) { fputs("vega: click needs the tui\n", stderr); continue; } - acted = sscanf(line, "%*s %u %u", &at_line, &at_column) == 2 && at_line && shown; - ok = acted && mouse(0, at_line - 1); - } else if (!strcmp(command, "time")) { - /* The time the next events see: scripts replay any. */ - char *end; - double now = strtod(argument, &end); - acted = false; - if (!*argument || *end || !(now >= 0 && now < 1e15)) fprintf(stderr, "vega: time takes milliseconds\n"); - else app.now = now; - } else if (!strcmp(command, "tick")) { - input = pure_module_input(app.module, argument); - verb = "tick"; - if (input < 0 || app.module->input[input].kind != PURE_KIND_TIMER) { - fprintf(stderr, "vega: no timer %s\n", argument); - input = -1; - ok = false; - } else ok = pure_app_tick(&app, (uint32_t)input) == PURE_OK; - } else if (!strcmp(command, "swap")) { - acted = false; - if (!swap(argument, read_module(argument))) fprintf(stderr, "vega: kept the running module\n"); - } else if (!strcmp(command, "frame")) { - long count = *argument ? strtol(argument, NULL, 10) : 1; - for (long i = 0; i < count && i < 600; ++i) ok = pure_app_frame(&app) == PURE_OK && ok; - } else if (!strcmp(command, "show")) { - acted = false; - if (surface == SURFACE_CLI) vega_print(stdout, &app, &screen, color); - else if (surface == SURFACE_SAY) vega_say(stdout, &app, &screen, true); - else if ((shown = true) && painting) { - Out out = {0}; - vega_draw(&grid, &app, &screen, entry, entry_length, typing); - paint(&grid, &out); - printf("paint %zu\n", out.used); - fwrite(out.bytes, 1, out.used, stdout); - putchar('\n'); - free(out.bytes); - } else { - vega_draw(&grid, &app, &screen, entry, entry_length, typing); - dump(&grid); - printf("cells %lu\n", vega_evaluations() - counted); - } - counted = vega_evaluations(); - } else { - fprintf(stderr, "vega: unknown command %s\n", command); - continue; - } - if (!ok && input >= 0) fprintf(stderr, "vega: %s failed\n", app.module->input_names[input]); - if (surface == SURFACE_SAY && acted) { - if (!ok) printf("Could not %s.\n", input >= 0 ? app.module->input_names[input] : verb); - else vega_say(stdout, &app, &screen, false); - } - fflush(stdout); - } - drawn = NULL; - vega_grid_free(&grid); - return 0; -} - -int main(int argc, char **argv) { - unsigned width = 0, height = 0, port = 8080; - bool dumped = false, painting = false, watch = false, usage = argc < 3; - const char *store = NULL, *listen_on = "127.0.0.1"; - for (int i = 3; !usage && i < argc; i += 2) { - if (!strcmp(argv[i], "--watch") && (!strcmp(argv[1], "tui") || !strcmp(argv[1], "web"))) { - watch = true; - --i; /* a flag without a value */ - continue; - } - if (i + 1 >= argc) usage = true; - else if (!strcmp(argv[i], "--store")) store = argv[i + 1]; - else if (!strcmp(argv[i], "--listen") && !strcmp(argv[1], "web")) listen_on = argv[i + 1]; - else if (!strcmp(argv[i], "--port") && !strcmp(argv[1], "web") && sscanf(argv[i + 1], "%u", &port) == 1 && - port < 65536) {} - else if ((!strcmp(argv[i], "--dump") || !strcmp(argv[i], "--paint")) && - sscanf(argv[i + 1], "%ux%u", &width, &height) == 2 && width && height && width <= 1000 && height <= 1000) - dumped = true, painting = !strcmp(argv[i], "--paint"); - else usage = true; - } - if (usage || (dumped && strcmp(argv[1], "tui")) || (!strcmp(argv[1], "check") && argc != 3)) { - fputs("vega tui|cli|say MODULE.pmod [--store PATH]\n" - "vega tui MODULE.pmod [--store PATH] [--watch] | --dump|--paint WIDTHxHEIGHT < script\n" - "vega web MODULE.pmod [--store PATH] [--listen ADDRESS] [--port N] [--watch]\n" - "vega check MODULE.pmod\n", stderr); - return 2; - } - if (!strcmp(argv[1], "check")) { - if (!load_module(argv[2], &modules[0])) return 2; - return vega_check(&modules[0], print_finding, argv[2]) ? 1 : 0; - } - Surface surface = !strcmp(argv[1], "cli") ? SURFACE_CLI : !strcmp(argv[1], "say") ? SURFACE_SAY : - !strcmp(argv[1], "tui") ? SURFACE_TUI : !strcmp(argv[1], "web") ? SURFACE_WEB : (Surface)-1; - if ((int)surface < 0) { fprintf(stderr, "vega: unknown surface %s\n", argv[1]); return 2; } - if (!load(argv[2], store)) return 2; - if (watch && !dumped) { - watched = argv[2]; - watched_digest = module_digest; /* of what load read */ - } - int status = surface == SURFACE_WEB ? vega_web(&app, &screen, listen_on, port, watched ? module_changed : NULL) : - surface == SURFACE_TUI && !dumped ? interactive() : script(surface, width, height, painting); - /* Frame changes are stored with events; keep the last ones too. */ - if (pure_app_save(&app) != PURE_OK) { fputs("vega: cannot save\n", stderr); status = status ? status : 1; } - pure_app_stop(&app); - return status; -} diff --git a/src/screen.c b/src/screen.c deleted file mode 100644 index fdecaf4..0000000 --- a/src/screen.c +++ /dev/null @@ -1,229 +0,0 @@ -#define _POSIX_C_SOURCE 200809L -#include "vega.h" -#include "module-text.h" -#include <math.h> -#include <string.h> -#include <time.h> - -double vega_now(void) { - struct timespec t; - clock_gettime(CLOCK_REALTIME, &t); - return floor((double)t.tv_sec * 1000 + (double)t.tv_nsec / 1e6); -} - -void vega_timers_start(VegaTimers *timers, const PureModule *m, double now) { - for (uint32_t i = 0; i < m->inputs; ++i) - timers->next[i] = m->input[i].kind == PURE_KIND_TIMER ? now + m->input[i].period : 0; -} - -long vega_timers_run(VegaTimers *timers, PureApp *app, double now, bool *ticked) { - const PureModule *m = app->module; - double soonest = -1; - for (uint32_t i = 0; i < m->inputs; ++i) { - if (m->input[i].kind != PURE_KIND_TIMER) continue; - if (timers->next[i] <= now) { - app->now = now; - pure_app_tick(app, i); /* a failed tick changes nothing, as any failed event */ - *ticked = true; - timers->next[i] = now + m->input[i].period; - } - if (soonest < 0 || timers->next[i] - now < soonest) soonest = timers->next[i] - now; - } - return soonest < 0 ? -1 : (long)ceil(soonest); -} - -static unsigned long evaluations; - -unsigned long vega_evaluations(void) { return evaluations; } - -static int column(const PureModule *m, uint32_t table, const char *name, uint8_t type) { - int view = pure_module_column(m, table, name); - return view >= 0 && m->view_type[view] == type ? view : -1; -} - -void vega_screen(VegaScreen *screen, const PureModule *m) { - memset(screen, 0, sizeof(*screen)); - screen->module = m; - screen->title = pure_module_view(m, "title"); - if (screen->title >= 0 && m->view_type[screen->title] != PURE_VIEW_TEXT) screen->title = -1; - screen->prompt = -1; - for (uint32_t i = 0; i < m->inputs && screen->prompt < 0; ++i) - if (m->input[i].kind == PURE_KIND_TEXT) screen->prompt = (int)i; - for (uint32_t h = 0; h < m->handlers; ++h) screen->ticks = screen->ticks || m->handler[h].event == 0; - /* A <table>_focus view belongs to its table, not to the fields. */ - bool focus_view[PURE_MODULE_VIEWS] = {false}; - for (uint32_t i = 0; i < m->views; ++i) { - if (m->view_type[i] != PURE_VIEW_TABLE) continue; - char name[PURE_MODULE_NAME + 8]; - snprintf(name, sizeof(name), "%s_focus", m->view_names[i]); - int focus = pure_module_view(m, name); - if (focus >= 0 && m->view_type[focus] == PURE_VIEW_NUM) focus_view[focus] = true; - } - for (uint32_t i = 0; i < m->views && screen->count < VEGA_PARTS; ++i) { - if (m->view_table[i] || (int)i == screen->title || focus_view[i]) continue; - VegaPart *part = &screen->parts[screen->count++]; - *part = (VegaPart){.kind = VEGA_FIELD, .view = i, .focus = -1, .pointer = -1, .label = -1, .tone = -1, .detail = -1, - .badge = -1}; - if (m->view_type[i] != PURE_VIEW_TABLE) continue; - char name[PURE_MODULE_NAME + 8]; - snprintf(name, sizeof(name), "%s_focus", m->view_names[i]); - part->focus = pure_module_view(m, name); - if (part->focus >= 0 && !focus_view[part->focus]) part->focus = -1; - part->pointer = part->focus >= 0 ? pure_module_position(m, (uint32_t)part->focus) : -1; - part->label = column(m, i, "label", PURE_VIEW_TEXT); - part->tone = column(m, i, "tone", PURE_VIEW_NUM); - part->kind = part->label >= 0 ? VEGA_LIST : VEGA_TABLE; - if (part->kind == VEGA_LIST) { - part->detail = column(m, i, "detail", PURE_VIEW_TEXT); - part->badge = column(m, i, "badge", PURE_VIEW_TEXT); - } - for (uint32_t k = 0; k < m->views; ++k) - if (m->view_table[k] == i + 1 && (int)k != part->tone) part->columns[part->column_count++] = k; - } -} - -static bool is_view(const PureModule *m, int view, uint8_t type) { - return view >= 0 && m->view_type[view] == type; -} - -unsigned vega_check(const PureModule *m, void (*report)(void *, const char *name, const char *message), void *context) { - unsigned found = 0; - char name[2 * PURE_MODULE_NAME + 8], message[256]; -#define FIND(...) do { snprintf(message, sizeof(message), __VA_ARGS__); report(context, name, message); ++found; } while (0) - int title = pure_module_view(m, "title"); - snprintf(name, sizeof(name), "title"); - if (title >= 0 && m->view_type[title] != PURE_VIEW_TEXT) FIND("a title is text; this one shows as a field"); - for (uint32_t i = 0; i < m->views; ++i) { - if (m->view_table[i]) continue; - const char *view = m->view_names[i]; - size_t length = strlen(view); - snprintf(name, sizeof(name), "%s", view); - if (length > 6 && !strcmp(view + length - 6, "_focus")) { - char table[PURE_MODULE_NAME]; - snprintf(table, sizeof(table), "%.*s", (int)(length - 6), view); - int owner = pure_module_view(m, table); - if (!is_view(m, owner, PURE_VIEW_TABLE)) - FIND("names no list or table %s; it shows as a field", table); - else if (m->view_type[i] != PURE_VIEW_NUM) FIND("the focused row is a number; %s shows no focus", table); - } - if (m->view_type[i] != PURE_VIEW_TABLE) continue; - int label = pure_module_column(m, i, "label"), tone = pure_module_column(m, i, "tone"); - bool list = is_view(m, label, PURE_VIEW_TEXT); - unsigned shown = 0; - for (uint32_t k = 0; k < m->views; ++k) { - if (m->view_table[k] != i + 1) continue; - const char *column = m->view_names[k]; - bool text = m->view_type[k] == PURE_VIEW_TEXT; - snprintf(name, sizeof(name), "%s.%s", view, column); - if ((int)k == label && !text) FIND("a list's label is text; %s shows as a table", view); - else if ((int)k == tone && text) FIND("a tone is a number (vega.normal ... vega.danger); %s", list ? - "the list ignores it" : "it shows as a column"); - else if (list && (!strcmp(column, "badge") || !strcmp(column, "detail")) && !text) - FIND("a list's %s is text; this one is not shown", column); - else if (list && (int)k != label && (int)k != tone && strcmp(column, "badge") && strcmp(column, "detail")) - FIND("a list shows label, badge, detail and tone; this column is not shown"); - shown += (int)k != tone || text; - } - snprintf(name, sizeof(name), "%s", view); - if (!list && !shown) FIND("a table with no column to show"); - } - int prompt = -1; - for (uint32_t i = 0; i < m->inputs; ++i) { - if (m->input[i].kind != PURE_KIND_TEXT) continue; - snprintf(name, sizeof(name), "%s", m->input_names[i]); - if (prompt >= 0) FIND("only the first text input (%s) has a place to type into", m->input_names[prompt]); - else prompt = (int)i; - } -#undef FIND - return found; -} - -typedef struct { char *out; size_t used; } Collect; - -static bool collect(void *context, PureSpan span) { - Collect *c = context; - size_t room = VEGA_TEXT - 1 - c->used; - size_t take = span.size < room ? span.size : room; - /* Never cut a character: back off to a UTF-8 boundary. */ - if (take < span.size) while (take && (span.data[take] & 0xC0) == 0x80) --take; - if (take) memcpy(c->out + c->used, span.data, take); - c->used += take; - return true; -} - -bool vega_text(PureApp *app, uint32_t view, size_t row, char out[VEGA_TEXT], size_t *length) { - const PureModule *m = app->module; - bool column = m->view_table[view] != 0; - ++evaluations; - *length = 0; - out[0] = 0; - if (m->view_type[view] == PURE_VIEW_NUM) { - double value; - if (!(column ? pure_app_cell(app, view, row, &value) : pure_app_view(app, view, &value))) return false; - *length = pure_format_number(value, out); - out[*length] = 0; - return true; - } - Collect c = {out, 0}; - PureResult result = column ? pure_app_cell_text(app, view, row, collect, &c) : pure_app_view_text(app, view, collect, &c); - out[c.used] = 0; - *length = c.used; - return result == PURE_OK; -} - -bool vega_number(PureApp *app, uint32_t view, size_t row, double *out) { - ++evaluations; - return app->module->view_table[view] ? pure_app_cell(app, view, row, out) : pure_app_view(app, view, out); -} - -size_t vega_rows(PureApp *app, const VegaPart *part) { - return part->kind == VEGA_FIELD ? 0 : pure_app_rows(app, part->view); -} - -size_t vega_focus(PureApp *app, const VegaPart *part) { - size_t rows = vega_rows(app, part), row; - double focus; - if (part->focus < 0 || !rows || !vega_number(app, (uint32_t)part->focus, 0, &focus) || !(focus >= 0)) return 0; - bool filtered = app->module->view_code[part->view].length > 0; - size_t item = focus < (double)SIZE_MAX ? (size_t)focus : SIZE_MAX; - if (pure_app_item_row(app, part->view, item, &row)) return row; - return filtered ? VEGA_NO_ROW : rows - 1; -} - -size_t vega_scroll(size_t first, size_t focus, size_t visible, size_t rows) { - if (focus < rows && focus < first) first = focus; - if (focus < rows && visible && focus >= first + visible) first = focus - visible + 1; - if (first + visible > rows) first = rows > visible ? rows - visible : 0; - return first; -} - -int vega_key_input(const PureModule *m, const char *key) { - for (uint32_t i = 0; i < m->inputs; ++i) - if (m->input[i].kind == PURE_KIND_KEY && !strcmp(m->input_keys[i], key)) return (int)i; - return -1; -} - -bool vega_press(PureApp *app, const char *key) { - int input = vega_key_input(app->module, key); - if (input < 0) return false; - bool ok = pure_app_input(app, (uint32_t)input, PURE_EDGE_DOWN) == PURE_OK; - pure_app_input(app, (uint32_t)input, PURE_EDGE_UP); - return ok; -} - -bool vega_commit(PureApp *app, const VegaScreen *screen, const char *text, size_t length) { - return screen->prompt >= 0 && - pure_app_text(app, (uint32_t)screen->prompt, (PureSpan){(const unsigned char *)text, length}) == PURE_OK; -} - -bool vega_point(PureApp *app, const VegaPart *part, size_t row) { - size_t item; /* a filtered list's row shows some item of the list */ - return part->pointer >= 0 && pure_app_row_item(app, part->view, row, &item) && - pure_app_point(app, (uint32_t)part->pointer, item) == PURE_OK; -} - -VegaTone vega_tone(PureApp *app, const VegaPart *part, size_t row) { - double tone; - if (part->tone < 0 || !vega_number(app, (uint32_t)part->tone, row, &tone)) return VEGA_NORMAL; - return tone >= 0 && tone < VEGA_TONES && tone == floor(tone) ? (VegaTone)tone : VEGA_NORMAL; -} diff --git a/src/symbols-gl.c b/src/symbols-gl.c deleted file mode 100644 index 0e78efe..0000000 --- a/src/symbols-gl.c +++ /dev/null @@ -1,260 +0,0 @@ -#include "symbols-gl.h" -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -enum { BANDS = 16 }; - -/* Coordinates travel doubled, so implied points stay whole: 2x of a point - * is its raw i16 without the flag, and an implied point's is the sum of its - * two neighbours' coordinates. */ -#define SHARED \ - "#version 310 es\n" \ - "precision highp float;\n" \ - "precision highp int;\n" - -static const char vertex_source[] = SHARED - "layout(location = 0) in vec4 place; /* origin x, y in pixels; pixels per unit; unused */\n" - "layout(location = 1) in vec4 bounds; /* left, bottom, right, top in units */\n" - "layout(location = 2) in uint bands; /* its first band */\n" - "uniform vec2 target;\n" - "out vec2 units;\n" - "flat out float scale;\n" - "flat out ivec2 rows; /* bottom and top in units */\n" - "flat out uint base;\n" - "void main() {\n" - " vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1));\n" - " vec2 pad = vec2(1.0 / place.z);\n" - " units = mix(bounds.xy - pad, bounds.zw + pad, corner);\n" - " vec2 pixel = place.xy + vec2(units.x, -units.y) * place.z;\n" - " gl_Position = vec4(pixel.x / target.x * 2.0 - 1.0, 1.0 - pixel.y / target.y * 2.0, 0.0, 1.0);\n" - " scale = place.z;\n" - " rows = ivec2(bounds.yw);\n" - " base = bands;\n" - "}\n"; - -static const char fragment_source[] = SHARED - "layout(std430, binding = 0) readonly buffer Points { uint points[]; };\n" - "layout(std430, binding = 1) readonly buffer Bands { uint bands[]; };\n" - "uniform uint listed; /* where the lists of curves start in bands */\n" - "in vec2 units;\n" - "flat in float scale;\n" - "flat in ivec2 rows;\n" - "flat in uint base;\n" - "out vec4 color;\n" - "\n" - "ivec3 point(uint i) { /* doubled x, doubled y, off */\n" - " uint w = points[i];\n" - " int x = int(w << 16u) >> 16, y = int(w) >> 16;\n" - " return ivec3(x & ~1, y & ~1, x & 1);\n" - "}\n" - "int band(int doubled) { /* which of 16 bands a doubled y lies in */\n" - " return clamp((doubled - 2 * rows.x) * 16 / (2 * (rows.y - rows.x) + 2), 0, 15);\n" - "}\n" - "int roots(float a, float b, float c, inout float ts[10], int n) {\n" - " float r0 = -1.0, r1 = -1.0;\n" - " if (a == 0.0) { if (b != 0.0) r0 = -c / b; }\n" - " else {\n" - " float d = b * b - 4.0 * a * c;\n" - " if (d >= 0.0) {\n" - " float q = -0.5 * (b + (b >= 0.0 ? sqrt(d) : -sqrt(d)));\n" - " r0 = q / a;\n" - " if (q != 0.0) r1 = c / q;\n" - " }\n" - " }\n" - " if (r0 > 0.0 && r0 < 1.0) ts[n++] = r0;\n" - " if (r1 > 0.0 && r1 < 1.0) ts[n++] = r1;\n" - " return n;\n" - "}\n" - "/* As curve_area in symbols.c: the curve's share of the pixel [0, 1]^2. */\n" - "float area(vec2 p0, vec2 p1, vec2 p2) {\n" - " if (max(max(p0.y, p1.y), p2.y) <= 0.0 || min(min(p0.y, p1.y), p2.y) >= 1.0 || min(min(p0.x, p1.x), p2.x) >= 1.0)\n" - " return 0.0;\n" - " if (max(max(p0.x, p1.x), p2.x) <= 0.0) return clamp(p2.y, 0.0, 1.0) - clamp(p0.y, 0.0, 1.0);\n" - " vec2 a = p0 - 2.0 * p1 + p2, b = p0 - p1;\n" - " float ts[10];\n" - " ts[0] = 0.0;\n" - " int n = 1;\n" - " n = roots(a.y, -2.0 * b.y, p0.y, ts, n);\n" - " n = roots(a.y, -2.0 * b.y, p0.y - 1.0, ts, n);\n" - " n = roots(a.x, -2.0 * b.x, p0.x, ts, n);\n" - " n = roots(a.x, -2.0 * b.x, p0.x - 1.0, ts, n);\n" - " ts[n++] = 1.0;\n" - " for (int i = 1; i < n; ++i)\n" - " for (int j = i; j > 0 && ts[j] < ts[j - 1]; --j) { float t = ts[j]; ts[j] = ts[j - 1]; ts[j - 1] = t; }\n" - " float u0 = 1.0 - p0.x, u1 = 2.0 * b.x, u2 = -a.x, v0 = -2.0 * b.y, v1 = 2.0 * a.y;\n" - " float k1 = u0 * v0, k2 = (u0 * v1 + u1 * v0) / 2.0, k3 = (u1 * v1 + u2 * v0) / 3.0, k4 = u2 * v1 / 4.0, sum = 0.0;\n" - " for (int i = 0; i + 1 < n; ++i) {\n" - " float ta = ts[i], tb = ts[i + 1], t = (ta + tb) / 2.0;\n" - " if (tb <= ta) continue;\n" - " float y = (a.y * t - 2.0 * b.y) * t + p0.y, x = (a.x * t - 2.0 * b.x) * t + p0.x;\n" - " if (y <= 0.0 || y >= 1.0 || x >= 1.0) continue;\n" - " if (x <= 0.0) sum += (a.y * tb - 2.0 * b.y) * tb - (a.y * ta - 2.0 * b.y) * ta;\n" - " else sum += (((k4 * tb + k3) * tb + k2) * tb + k1) * tb - (((k4 * ta + k3) * ta + k2) * ta + k1) * ta;\n" - " }\n" - " return sum;\n" - "}\n" - "void main() {\n" - " vec2 corner = units - vec2(0.5 / scale); /* the pixel's lower left, in units */\n" - " int b0 = band(2 * int(floor(corner.y))), b1 = band(2 * int(ceil(corner.y + 1.0 / scale)));\n" - " float sum = 0.0;\n" - " for (int b = b0; b <= b1; ++b) {\n" - " uint at = bands[base + 2u * uint(b)], end = at + bands[base + 2u * uint(b) + 1u];\n" - " for (; at < end; ++at) {\n" - " uint k = bands[listed + at];\n" - " ivec3 p = point(k), n = point(k + 1u);\n" - " ivec2 s, c, e;\n" - " if (p.z == 1) {\n" - " ivec3 q = point(k - 1u);\n" - " s = q.z == 1 ? (q.xy + p.xy) / 2 : q.xy;\n" - " c = p.xy;\n" - " e = n.z == 1 ? (p.xy + n.xy) / 2 : n.xy;\n" - " } else {\n" - " s = p.xy;\n" - " c = (p.xy + n.xy) / 2;\n" - " e = n.xy;\n" - " }\n" - " /* a curve over several of this pixel's bands counts in the first */\n" - " if (max(band(min(min(s.y, c.y), e.y)), b0) != b) continue;\n" - " sum += area((vec2(s) * 0.5 - corner) * scale, (vec2(c) * 0.5 - corner) * scale, (vec2(e) * 0.5 - corner) * scale);\n" - " }\n" - " }\n" - " color = vec4(min(abs(sum), 1.0), 0.0, 0.0, 1.0);\n" - "}\n"; - -static GLuint compile(GLenum type, const char *source, char *error, size_t error_size) { - GLuint shader = glCreateShader(type); - glShaderSource(shader, 1, &source, NULL); - glCompileShader(shader); - GLint ok; - glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); - if (!ok) { - glGetShaderInfoLog(shader, (GLsizei)error_size, NULL, error); - glDeleteShader(shader); - return 0; - } - return shader; -} - -/* Doubled coordinates make implied points whole; bands as the shader cuts them. */ -static int band(int doubled, int bottom, int top) { - int b = (doubled - 2 * bottom) * BANDS / (2 * (top - bottom) + 2); - return b < 0 ? 0 : b >= BANDS ? BANDS - 1 : b; -} - -bool vega_gl_open(VegaGl *gl, const VegaSymbols *s, char *error, size_t error_size) { - *gl = (VegaGl){0}; - GLuint vertex = compile(GL_VERTEX_SHADER, vertex_source, error, error_size); - GLuint fragment = vertex ? compile(GL_FRAGMENT_SHADER, fragment_source, error, error_size) : 0; - if (!fragment) return false; - gl->program = glCreateProgram(); - glAttachShader(gl->program, vertex); - glAttachShader(gl->program, fragment); - glLinkProgram(gl->program); - glDeleteShader(vertex); - glDeleteShader(fragment); - GLint ok; - glGetProgramiv(gl->program, GL_LINK_STATUS, &ok); - if (!ok) { - glGetProgramInfoLog(gl->program, (GLsizei)error_size, NULL, error); - return false; - } - /* The point table, as the file has it. */ - glGenBuffers(1, &gl->points); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, gl->points); - glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)(s->points ? s->points : 1) * VEGA_POINT_BYTES, - s->bytes + VEGA_SYMBOLS_HEADER + (size_t)s->symbols * VEGA_SYMBOL_BYTES, GL_STATIC_DRAW); - /* Bands: per symbol, 16 (start, count) pairs into one list of curves, - * each curve (by the point that begins it) in every band it reaches. */ - size_t table = (size_t)s->symbols * BANDS * 2, listed = 0; - uint32_t *counts = calloc(table + 1, sizeof(*counts)); - for (int pass = 0; pass < 2; ++pass) { - uint32_t *bands = pass ? malloc((table + listed + 1) * sizeof(*bands)) : NULL; - size_t next = 0; - for (uint32_t i = 0; i < s->symbols; ++i) { - VegaSymbol symbol = vega_symbol_at(s, i); - uint32_t *mine = pass ? bands + (size_t)i * BANDS * 2 : counts + (size_t)i * BANDS * 2; - if (pass) - for (int b = 0; b < BANDS; ++b) { - mine[2 * b] = (uint32_t)next; - next += counts[(size_t)i * BANDS * 2 + 2 * b + 1]; - mine[2 * b + 1] = 0; - } - VegaCurve c; - for (uint32_t k = symbol.first; k < symbol.first + symbol.count; ++k) { - if (!vega_symbol_curve(s, &symbol, k, &c)) continue; - float low = c.y0 < c.y1 ? c.y0 : c.y1, high = c.y0 > c.y1 ? c.y0 : c.y1; - low = c.y2 < low ? c.y2 : low; - high = c.y2 > high ? c.y2 : high; - int from = band((int)(2 * low), symbol.bottom, symbol.top), to = band((int)(2 * high), symbol.bottom, symbol.top); - for (int b = from; b <= to; ++b) { - if (pass) bands[table + mine[2 * b] + mine[2 * b + 1]] = k; - ++mine[2 * b + 1]; - listed += !pass; - } - } - } - if (pass) { - glGenBuffers(1, &gl->bands); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, gl->bands); - glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)((table + listed + 1) * sizeof(*bands)), bands, GL_STATIC_DRAW); - free(bands); - } - } - free(counts); - glGenVertexArrays(1, &gl->array); - glGenBuffers(1, &gl->instances); - glBindVertexArray(gl->array); - glBindBuffer(GL_ARRAY_BUFFER, gl->instances); - glEnableVertexAttribArray(0); - glEnableVertexAttribArray(1); - glEnableVertexAttribArray(2); - glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 36, (void *)0); - glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 36, (void *)16); - glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, 36, (void *)32); - glVertexAttribDivisor(0, 1); - glVertexAttribDivisor(1, 1); - glVertexAttribDivisor(2, 1); - glBindVertexArray(0); - glUseProgram(gl->program); - glUniform1ui(glGetUniformLocation(gl->program, "listed"), (GLuint)table); - gl->viewport = glGetUniformLocation(gl->program, "target"); - if (glGetError() != GL_NO_ERROR) { - snprintf(error, error_size, "GL error while uploading"); - return false; - } - return true; -} - -void vega_gl_draw(VegaGl *gl, const VegaSymbols *s, const VegaGlyph *glyphs, uint32_t count, int width, int height) { - float *instances = malloc((size_t)(count ? count : 1) * 36); - for (uint32_t i = 0; i < count; ++i) { - VegaSymbol symbol = vega_symbol_at(s, glyphs[i].symbol); - float *f = instances + (size_t)i * 9; - f[0] = glyphs[i].x, f[1] = glyphs[i].y, f[2] = glyphs[i].scale, f[3] = 0; - f[4] = symbol.left, f[5] = symbol.bottom, f[6] = symbol.right, f[7] = symbol.top; - uint32_t base = glyphs[i].symbol * BANDS * 2; - memcpy(f + 8, &base, 4); - } - glUseProgram(gl->program); - glUniform2f(gl->viewport, (float)width, (float)height); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, gl->points); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, gl->bands); - glBindVertexArray(gl->array); - glBindBuffer(GL_ARRAY_BUFFER, gl->instances); - glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)count * 36, instances, GL_STREAM_DRAW); - glEnable(GL_BLEND); - glBlendFunc(GL_ONE, GL_ONE); - glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)count); - glBindVertexArray(0); - free(instances); -} - -void vega_gl_close(VegaGl *gl) { - glDeleteBuffers(1, &gl->points); - glDeleteBuffers(1, &gl->bands); - glDeleteBuffers(1, &gl->instances); - glDeleteVertexArrays(1, &gl->array); - glDeleteProgram(gl->program); - *gl = (VegaGl){0}; -} diff --git a/src/symbols-gl.h b/src/symbols-gl.h deleted file mode 100644 index 4c6db67..0000000 --- a/src/symbols-gl.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef VEGA_SYMBOLS_GL_H -#define VEGA_SYMBOLS_GL_H -#include "symbols.h" -#include <GLES3/gl31.h> - -/* Symbols drawn by the GPU (OpenGL ES 3.1): the file's point table goes up - * as it is, one storage buffer, and each pixel of a symbol's box sums the - * exact area of the curves crossing its row, as vega_coverage does. Rows - * are cut into bands at upload, each listing its curves, so a pixel reads - * only those. Needs a current context. */ -typedef struct { - GLuint program, points, bands, instances, array; - GLint viewport; -} VegaGl; - -/* A symbol placed: its origin in pixels from the target's top left, and - * pixels per unit. */ -typedef struct { float x, y, scale; uint32_t symbol; } VegaGlyph; - -bool vega_gl_open(VegaGl *, const VegaSymbols *, char *error, size_t error_size); -/* Coverage added into the red channel of the bound framebuffer. */ -void vega_gl_draw(VegaGl *, const VegaSymbols *, const VegaGlyph *, uint32_t count, int width, int height); -void vega_gl_close(VegaGl *); -#endif diff --git a/src/symbols-tool.c b/src/symbols-tool.c deleted file mode 100644 index a94feb3..0000000 --- a/src/symbols-tool.c +++ /dev/null @@ -1,881 +0,0 @@ -#define _POSIX_C_SOURCE 200809L -/* vega-symbols: build symbol files from fonts and SVG icons, and measure them - * against FreeType (docs/design/002-symbol-format.md). - * vega-symbols build [--font FONT.ttf --codes 0x20-0x7e,...] [--svg CODE=ICON.svg ...] [--units N] -o OUT.vsym - * vega-symbols compare FONT.ttf OUT.vsym --sizes 12,16,32,64 - * vega-symbols info OUT.vsym - * vega-symbols render OUT.vsym CODE PIXELS OUT.pgm - * vega-symbols gpu OUT.vsym --sizes 12,16,32,64 [--frames N] */ -#include "symbols-gl.h" -#include "symbols.h" -#include <EGL/egl.h> -#include <EGL/eglext.h> -#include <ft2build.h> -#include FT_FREETYPE_H -#include FT_OUTLINE_H -#include <math.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <time.h> - -#define PI 3.14159265358979323846 - -typedef struct { double x, y; } Point; -/* An outline as the pen drew it. smooth: the end lies halfway between this - * control and the next, by construction, so it may go unwritten. */ -typedef enum { MOVE, LINE, QUAD } Kind; -typedef struct { Kind kind; bool smooth; Point control, end; } Segment; -typedef struct { Segment *items; size_t count, capacity; Point at, start; bool open; double font_scale; } Path; -typedef struct { uint32_t code; size_t first, count; int16_t advance; } Built; /* segments [first, first + count) */ - -static void fail(const char *why) { - fprintf(stderr, "vega-symbols: %s\n", why); - exit(1); -} - -static void add(Path *path, Segment segment) { - if (path->count == path->capacity) { - path->capacity = path->capacity ? 2 * path->capacity : 64; - if (!(path->items = realloc(path->items, path->capacity * sizeof(*path->items)))) fail("out of memory"); - } - path->items[path->count++] = segment; - path->at = segment.end; -} - -static bool same(Point a, Point b) { return a.x == b.x && a.y == b.y; } - -static void line_to(Path *path, Point to) { - if (!same(to, path->at)) add(path, (Segment){LINE, false, to, to}); -} - -static void quad_to(Path *path, Point control, Point to) { add(path, (Segment){QUAD, false, control, to}); } - -static Point lerp(Point a, Point b, double t) { return (Point){a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t}; } -static double length(double x, double y) { return sqrt(x * x + y * y); } - -/* Whether the cubic of these differences stays within tolerance of zero - * (split in halves until its control points do). */ -static bool fits(Point p0, Point p1, Point p2, Point p3, double tolerance) { - if (length(p1.x, p1.y) <= tolerance && length(p2.x, p2.y) <= tolerance) return true; - Point mid = {(p0.x + 3 * (p1.x + p2.x) + p3.x) / 8, (p0.y + 3 * (p1.y + p2.y) + p3.y) / 8}; - if (length(mid.x, mid.y) > tolerance) return false; - Point d = {(p3.x + p2.x - p1.x - p0.x) / 8, (p3.y + p2.y - p1.y - p0.y) / 8}; - return fits(p0, lerp(p0, p1, 0.5), (Point){mid.x - d.x, mid.y - d.y}, mid, tolerance) && - fits(mid, (Point){mid.x + d.x, mid.y + d.y}, lerp(p2, p3, 0.5), p3, tolerance); -} - -/* The piece of cubic c over [a, b], as a cubic. */ -static void piece(const Point c[4], double a, double b, Point out[4]) { - Point q[4]; - for (int k = 0; k < 2; ++k) { - double t = k ? b : a, u = 1 - t; - Point e = {u * u * u * c[0].x + 3 * u * u * t * c[1].x + 3 * u * t * t * c[2].x + t * t * t * c[3].x, - u * u * u * c[0].y + 3 * u * u * t * c[1].y + 3 * u * t * t * c[2].y + t * t * t * c[3].y}; - Point d = {3 * (u * u * (c[1].x - c[0].x) + 2 * u * t * (c[2].x - c[1].x) + t * t * (c[3].x - c[2].x)), - 3 * (u * u * (c[1].y - c[0].y) + 2 * u * t * (c[2].y - c[1].y) + t * t * (c[3].y - c[2].y))}; - double h = (b - a) / 3 * (k ? -1 : 1); - q[3 * k] = e; - q[1 + k] = (Point){e.x + d.x * h, e.y + d.y * h}; - } - memcpy(out, q, sizeof(q)); -} - -/* A cubic as a quadratic spline whose joins lie halfway between neighbouring - * controls (so they go unwritten), within tolerance: the approach of cu2qu - * (fontTools). With n pieces, piece i takes its control on the line between - * the points where the cubic's end tangents reach 1.5 times their control - * arms, at i / (n - 1); the fewest pieces that fit win. */ -static bool spline(const Point c[4], int n, double tolerance, Point *controls) { - if (n == 1) { - /* One quadratic: its control where the end tangents meet. */ - Point a = {c[1].x - c[0].x, c[1].y - c[0].y}, b = {c[2].x - c[3].x, c[2].y - c[3].y}; - double cross = a.x * b.y - a.y * b.x; - if (fabs(cross) < 1e-12) return false; - double t = ((c[3].x - c[0].x) * b.y - (c[3].y - c[0].y) * b.x) / cross; - controls[0] = (Point){c[0].x + a.x * t, c[0].y + a.y * t}; - Point q1 = lerp(c[0], controls[0], 2.0 / 3), q2 = lerp(c[3], controls[0], 2.0 / 3); - return fits((Point){0, 0}, (Point){q1.x - c[1].x, q1.y - c[1].y}, (Point){q2.x - c[2].x, q2.y - c[2].y}, - (Point){0, 0}, tolerance); - } - Point from = c[0], before = {0, 0}; - for (int i = 0; i < n; ++i) { - Point p[4]; - piece(c, (double)i / n, (double)(i + 1) / n, p); - Point near = lerp(p[0], p[1], 1.5), far = lerp(p[3], p[2], 1.5); - controls[i] = lerp(near, far, (double)i / (n - 1)); - } - for (int i = 0; i < n; ++i) { - Point p[4]; - piece(c, (double)i / n, (double)(i + 1) / n, p); - Point to = i + 1 < n ? lerp(controls[i], controls[i + 1], 0.5) : c[3]; - Point after = {to.x - p[3].x, to.y - p[3].y}; - Point q1 = lerp(from, controls[i], 2.0 / 3), q2 = lerp(to, controls[i], 2.0 / 3); - if (length(after.x, after.y) > tolerance || - !fits(before, (Point){q1.x - p[1].x, q1.y - p[1].y}, (Point){q2.x - p[2].x, q2.y - p[2].y}, after, tolerance)) - return false; - from = to; - before = after; - } - return true; -} - -static void cubic_to(Path *path, Point c1, Point c2, Point to) { - Point c[4] = {path->at, c1, c2, to}, controls[64]; - if (same(c[0], c1) && same(c1, c2) && same(c2, to)) return; - for (int n = 1; n <= 64; ++n) - if (spline(c, n, 1, controls)) { - for (int i = 0; i < n; ++i) - add(path, (Segment){QUAD, i + 1 < n, controls[i], i + 1 < n ? lerp(controls[i], controls[i + 1], 0.5) : to}); - return; - } - fail("a cubic that 64 quadratics do not fit"); -} - -static void close_path(Path *path) { - if (path->open) { - line_to(path, path->start); - /* A contour of a move alone draws nothing: drop it. */ - if (path->count && path->items[path->count - 1].kind == MOVE) --path->count; - } - path->open = false; -} - -static void move_to(Path *path, Point to) { - close_path(path); - add(path, (Segment){MOVE, false, to, to}); - path->start = to; - path->open = true; -} - -/* ---- Fonts, through FreeType's outline walk (in its units, times font_scale). */ - -static Point point_of(const Path *path, const FT_Vector *v) { - return (Point){(double)v->x * path->font_scale, (double)v->y * path->font_scale}; -} -static int ft_move(const FT_Vector *to, void *user) { move_to(user, point_of(user, to)); return 0; } -static int ft_line(const FT_Vector *to, void *user) { line_to(user, point_of(user, to)); return 0; } -static int ft_conic(const FT_Vector *c, const FT_Vector *to, void *user) { - quad_to(user, point_of(user, c), point_of(user, to)); - return 0; -} -static int ft_cubic(const FT_Vector *c1, const FT_Vector *c2, const FT_Vector *to, void *user) { - cubic_to(user, point_of(user, c1), point_of(user, c2), point_of(user, to)); - return 0; -} - -/* ---- SVG icons: the d of each path, the viewBox's height as the em. */ - -static const char *number(const char *p, double *out) { - while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\t' || *p == '\r') ++p; - char *end; - *out = strtod(p, &end); - return end == p ? NULL : end; -} - -/* An arc's flags are single digits, perhaps with no space between them. */ -static const char *flag(const char *p, double *out) { - while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\t' || *p == '\r') ++p; - if (*p != '0' && *p != '1') return NULL; - *out = *p - '0'; - return p + 1; -} - -typedef struct { double x, y, width, height, scale; } Box; - -static Point svg_point(const Box *box, double x, double y) { - return (Point){(x - box->x) * box->scale, (box->y + box->height - y) * box->scale}; -} - -/* An elliptical arc from (x1, y1) to (x2, y2) as cubics of at most a quarter - * turn each (SVG 1.1, F.6.5: from its endpoints to its center). */ -static void arc_to(Path *path, const Box *box, double x1, double y1, double rx, double ry, double degrees, bool large, - bool sweep, double x2, double y2) { - rx = fabs(rx); - ry = fabs(ry); - if ((x1 == x2 && y1 == y2)) return; - if (!rx || !ry) { - line_to(path, svg_point(box, x2, y2)); - return; - } - double phi = degrees * PI / 180, cp = cos(phi), sp = sin(phi); - double hx = (x1 - x2) / 2, hy = (y1 - y2) / 2, px = cp * hx + sp * hy, py = -sp * hx + cp * hy; - double lambda = px * px / (rx * rx) + py * py / (ry * ry); - if (lambda > 1) { rx *= sqrt(lambda); ry *= sqrt(lambda); } - double num = rx * rx * ry * ry - rx * rx * py * py - ry * ry * px * px, den = rx * rx * py * py + ry * ry * px * px; - double k = (large == sweep ? -1 : 1) * sqrt(fmax(num / den, 0)); - double ccx = k * rx * py / ry, ccy = -k * ry * px / rx; - double cx = cp * ccx - sp * ccy + (x1 + x2) / 2, cy = sp * ccx + cp * ccy + (y1 + y2) / 2; - double start = atan2((py - ccy) / ry, (px - ccx) / rx), turn = atan2((-py - ccy) / ry, (-px - ccx) / rx) - start; - if (sweep && turn < 0) turn += 2 * PI; - if (!sweep && turn > 0) turn -= 2 * PI; - int n = (int)ceil(fabs(turn) / (PI / 2) - 1e-9); - double step = turn / n, kappa = 4.0 / 3 * tan(step / 4); - for (int i = 0; i < n; ++i) { - double a = start + i * step, b = a + step; - double u[4][2] = {{cos(a), sin(a)}, {cos(a) - kappa * sin(a), sin(a) + kappa * cos(a)}, - {cos(b) + kappa * sin(b), sin(b) - kappa * cos(b)}, {cos(b), sin(b)}}; - Point q[4]; - for (int j = 0; j < 4; ++j) - q[j] = svg_point(box, cx + rx * cp * u[j][0] - ry * sp * u[j][1], cy + rx * sp * u[j][0] + ry * cp * u[j][1]); - if (i == n - 1) q[3] = svg_point(box, x2, y2); - cubic_to(path, q[1], q[2], q[3]); - } -} - -static void svg_path(Path *path, const Box *box, const char *d) { - double cx = 0, cy = 0, sx = 0, sy = 0, lx = 0, ly = 0; /* current, subpath start, last control */ - char command = 0, last = 0; - for (const char *p = d; *p;) { - while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\t' || *p == '\r') ++p; - if (!*p) break; - if (strchr("MmLlHhVvQqTtCcSsZzAa", *p)) command = *p++; - else if (!command) fail("SVG path data without a command"); - bool relative = command >= 'a'; - double ox = relative ? cx : 0, oy = relative ? cy : 0, v[7]; - char upper = (char)(command & ~32); - int want = upper == 'M' || upper == 'L' || upper == 'T' ? 2 : upper == 'H' || upper == 'V' ? 1 : - upper == 'Q' || upper == 'S' ? 4 : upper == 'C' ? 6 : upper == 'A' ? 7 : 0; - for (int i = 0; i < want; ++i) - if (!(p = (upper == 'A' && (i == 3 || i == 4) ? flag : number)(p, &v[i]))) fail("SVG path data"); - switch (upper) { - case 'Z': - close_path(path); - cx = sx; cy = sy; - path->at = svg_point(box, cx, cy); - command = 0; /* a number after closepath is an error */ - break; - case 'A': { - double x = ox + v[5], y = oy + v[6]; - arc_to(path, box, cx, cy, v[0], v[1], v[2], v[3] != 0, v[4] != 0, x, y); - cx = x; cy = y; - break; - } - case 'M': - cx = ox + v[0]; cy = oy + v[1]; sx = cx; sy = cy; - move_to(path, svg_point(box, cx, cy)); - command = relative ? 'l' : 'L'; /* pairs after a moveto are linetos */ - break; - case 'L': cx = ox + v[0]; cy = oy + v[1]; line_to(path, svg_point(box, cx, cy)); break; - case 'H': cx = ox + v[0]; line_to(path, svg_point(box, cx, cy)); break; - case 'V': cy = (relative ? cy : 0) + v[0]; line_to(path, svg_point(box, cx, cy)); break; - case 'Q': - lx = ox + v[0]; ly = oy + v[1]; cx = ox + v[2]; cy = oy + v[3]; - quad_to(path, svg_point(box, lx, ly), svg_point(box, cx, cy)); - break; - case 'T': - lx = last == 'Q' || last == 'T' ? 2 * cx - lx : cx; ly = last == 'Q' || last == 'T' ? 2 * cy - ly : cy; - cx = ox + v[0]; cy = oy + v[1]; - quad_to(path, svg_point(box, lx, ly), svg_point(box, cx, cy)); - break; - case 'C': case 'S': { - double x1, y1; - if (upper == 'C') { x1 = ox + v[0]; y1 = oy + v[1]; } - else { x1 = last == 'C' || last == 'S' ? 2 * cx - lx : cx; y1 = last == 'C' || last == 'S' ? 2 * cy - ly : cy; } - int at = upper == 'C' ? 2 : 0; - lx = ox + v[at]; ly = oy + v[at + 1]; - double x = ox + v[at + 2], y = oy + v[at + 3]; - cubic_to(path, svg_point(box, x1, y1), svg_point(box, lx, ly), svg_point(box, x, y)); - cx = x; cy = y; - break; - } - } - last = upper; - } - close_path(path); -} - -static char *read_file(const char *name, size_t *size) { - FILE *f = fopen(name, "rb"); - if (!f) return NULL; - fseek(f, 0, SEEK_END); - long length = ftell(f); - fseek(f, 0, SEEK_SET); - char *bytes = length >= 0 ? malloc((size_t)length + 1) : NULL; - if (bytes && fread(bytes, 1, (size_t)length, f) != (size_t)length) { free(bytes); bytes = NULL; } - fclose(f); - if (bytes) { bytes[length] = 0; *size = (size_t)length; } - return bytes; -} - -static const char *attribute(const char *from, const char *end, const char *name) { - size_t length = strlen(name); - for (const char *p = from; p && p < end; ++p) { - p = strstr(p, name); - if (!p || p >= end) return NULL; - if ((p == from || p[-1] == ' ' || p[-1] == '\n' || p[-1] == '\t') && p[length] == '=' && - (p[length + 1] == '"' || p[length + 1] == '\'')) return p + length + 2; - } - return NULL; -} - -/* Only solid, nonzero-filled paths: anything else an icon asks for would be - * drawn wrong, so it is refused rather than dropped. */ -static const char *const unsupported[] = {"transform", "opacity", "evenodd", "stroke", "fill=\"none\"", "<use", - "<rect", "<circle", "<ellipse", "<poly", "<line", "<radial", "<clip", - "<mask", "<image", "<text", "<filter", "<pattern"}; - -static void svg_icon(Path *path, const char *file, uint16_t units, int16_t *advance) { - size_t size; - char *svg = read_file(file, &size), why[512]; - if (!svg) fail("cannot read an SVG file"); - for (size_t i = 0; i < sizeof(unsupported) / sizeof(*unsupported); ++i) - if (strstr(svg, unsupported[i])) { - snprintf(why, sizeof(why), "%s: icons hold only solid paths, not %s", file, unsupported[i]); - fail(why); - } - const char *tag = strstr(svg, "<svg"), *tag_end = tag ? strchr(tag, '>') : NULL, *view = tag ? attribute(tag, tag_end, "viewBox") : NULL; - /* Without a viewBox, the width and height (in px) are the user space. */ - Box box = {0}; - const char *width = tag ? attribute(tag, tag_end, "width") : NULL, *height = tag ? attribute(tag, tag_end, "height") : NULL; - if (view ? !(view = number(view, &box.x)) || !(view = number(view, &box.y)) || !(view = number(view, &box.width)) || - !number(view, &box.height) - : !width || !height || !number(width, &box.width) || !number(height, &box.height) || box.height <= 0) { - snprintf(why, sizeof(why), "%s: an icon needs a viewBox, or a width and height", file); - fail(why); - } - box.scale = units / box.height; - *advance = (int16_t)lround(box.width * box.scale); - for (const char *p = svg; (p = strstr(p, "<path"));) { - const char *end = strchr(p, '>'), *d = attribute(p, end, "d"); - if (d) { - char quote = d[-1]; - const char *close = strchr(d, quote); - char *data = strndup(d, (size_t)(close - d)); - svg_path(path, &box, data); - free(data); - } - p = end ? end : p + 5; - } - free(svg); -} - -/* ---- Writing. */ - -typedef struct { long x, y; bool off, move; } Emitted; -typedef struct { Emitted *items; size_t count, capacity; } Points; - -static void put16(unsigned char *p, int v) { p[0] = (unsigned char)v; p[1] = (unsigned char)((unsigned)v >> 8); } -static void put32(unsigned char *p, uint32_t v) { for (int i = 0; i < 4; ++i) p[i] = (unsigned char)(v >> (8 * i)); } - -static void emit(Points *points, Point at, bool off, bool move) { - if (points->count == points->capacity) { - points->capacity = points->capacity ? 2 * points->capacity : 256; - if (!(points->items = realloc(points->items, points->capacity * sizeof(*points->items)))) fail("out of memory"); - } - points->items[points->count++] = (Emitted){lround(at.x), lround(at.y), off, move}; -} - -/* Whether s ends where its control and the next curve's would imply. */ -static bool implied(const Segment *s, const Segment *next) { - if (s->kind != QUAD || !next || next->kind != QUAD) return false; - return s->smooth || (2 * lround(s->end.x) == lround(s->control.x) + lround(next->control.x) && - 2 * lround(s->end.y) == lround(s->control.y) + lround(next->control.y)); -} - -/* One symbol's segments as points: each contour an on point that starts it, - * then per segment its control (if a curve) and its end, unless the next - * curve's control implies it; the last end, back at the start, is written. - * A contour that starts at an implied point (as FreeType starts TrueType's - * when its first point is off) starts instead at its first written one. */ -static void emit_symbol(Points *points, const Segment *segments, size_t count) { - for (size_t i = 0; i < count;) { - size_t end = i + 1; - while (end < count && segments[end].kind != MOVE) ++end; - const Segment *body = segments + i + 1; - size_t n = end - i - 1, from = 0; - if (n > 1 && implied(&body[n - 1], &body[0])) - for (size_t k = 0; k + 1 < n; ++k) - if (!implied(&body[k], &body[k + 1])) { from = k + 1; break; } - emit(points, from ? body[from - 1].end : segments[i].end, false, true); - for (size_t m = 0; m < n; ++m) { - const Segment *s = &body[(from + m) % n], *next = m + 1 < n ? &body[(from + m + 1) % n] : NULL; - const Emitted *last = &points->items[points->count - 1]; - if (s->kind == LINE) { - if (last->off || last->x != lround(s->end.x) || last->y != lround(s->end.y)) emit(points, s->end, false, false); - continue; - } - emit(points, s->control, true, false); - if (!implied(s, next)) emit(points, s->end, false, false); - } - i = end; - } -} - -static int by_code(const void *a, const void *b) { - uint32_t x = ((const Built *)a)->code, y = ((const Built *)b)->code; - return (x > y) - (x < y); -} - -static void write_symbols(const char *out, uint16_t units, Built *built, size_t count, const Path *path) { - qsort(built, count, sizeof(*built), by_code); - for (size_t i = 1; i < count; ++i) if (built[i].code == built[i - 1].code) fail("a code is given twice"); - Points points = {0}; - size_t *first = calloc(count + 1, sizeof(*first)); - for (size_t i = 0; i < count; ++i) { - first[i] = points.count; - emit_symbol(&points, path->items + built[i].first, built[i].count); - } - first[count] = points.count; - /* Halve units while every coordinate is even: a TrueType font's own. */ - for (;;) { - bool even = units % 2 == 0 && units > 16; - for (size_t i = 0; even && i < points.count; ++i) even = points.items[i].x % 2 == 0 && points.items[i].y % 2 == 0; - for (size_t i = 0; even && i < count; ++i) even = built[i].advance % 2 == 0; - if (!even) break; - for (size_t i = 0; i < points.count; ++i) { points.items[i].x /= 2; points.items[i].y /= 2; } - for (size_t i = 0; i < count; ++i) built[i].advance = (int16_t)(built[i].advance / 2); - units /= 2; - } - size_t size = VEGA_SYMBOLS_HEADER + count * VEGA_SYMBOL_BYTES + points.count * VEGA_POINT_BYTES; - unsigned char *bytes = calloc(1, size); - if (!bytes) fail("out of memory"); - memcpy(bytes, "VEGASYM2", 8); - put16(bytes + 8, units); - put32(bytes + 12, (uint32_t)count); - put32(bytes + 16, (uint32_t)points.count); - put32(bytes + 20, (uint32_t)size); - unsigned char *at = bytes + VEGA_SYMBOLS_HEADER + count * VEGA_SYMBOL_BYTES; - for (size_t i = 0; i < points.count; ++i) { - const Emitted *e = &points.items[i]; - if (e->x < -16384 || e->x > 16383 || e->y < -16384 || e->y > 16383) fail("a coordinate does not fit 15 bits"); - put16(at + 4 * i, (int)(2 * e->x + e->off)); - put16(at + 4 * i + 2, (int)(2 * e->y + e->move)); - } - for (size_t i = 0; i < count; ++i) { - long left = 0, bottom = 0, right = 0, top = 0; - for (size_t k = first[i]; k < first[i + 1]; ++k) { - const Emitted *e = &points.items[k]; - if (k == first[i]) { left = right = e->x; bottom = top = e->y; } - left = e->x < left ? e->x : left; - right = e->x > right ? e->x : right; - bottom = e->y < bottom ? e->y : bottom; - top = e->y > top ? e->y : top; - } - unsigned char *p = bytes + VEGA_SYMBOLS_HEADER + i * VEGA_SYMBOL_BYTES; - put32(p, built[i].code); - put32(p + 4, (uint32_t)first[i]); - put32(p + 8, (uint32_t)(first[i + 1] - first[i])); - put16(p + 12, (int)left); - put16(p + 14, (int)bottom); - put16(p + 16, (int)right); - put16(p + 18, (int)top); - put16(p + 20, built[i].advance); - } - FILE *f = fopen(out, "wb"); - if (!f || fwrite(bytes, 1, size, f) != size || fclose(f)) fail("cannot write the symbol file"); - char error[128]; - VegaSymbols check; - if (!vega_symbols_open(&check, bytes, size, error, sizeof(error))) fail(error); - size_t curves = 0; - VegaCurve curve; - for (uint32_t i = 0; i < check.symbols; ++i) { - VegaSymbol symbol = vega_symbol_at(&check, i); - for (uint32_t k = symbol.first; k < symbol.first + symbol.count; ++k) curves += vega_symbol_curve(&check, &symbol, k, &curve); - } - printf("{\"symbols\":%zu,\"points\":%zu,\"curves\":%zu,\"units\":%u,\"bytes\":%zu}\n", count, points.count, curves, - units, size); - free(bytes); - free(first); - free(points.items); -} - -/* 0x20-0x7e,0xac00 as a list of codes. */ -static size_t codes_of(const char *text, uint32_t **out) { - size_t count = 0, capacity = 0; - *out = NULL; - for (const char *p = text; *p;) { - char *end; - unsigned long first = strtoul(p, &end, 0), last = first; - if (end == p) fail("--codes takes FIRST[-LAST],..."); - p = end; - if (*p == '-') { last = strtoul(p + 1, &end, 0); p = end; } - for (unsigned long c = first; c <= last; ++c) { - if (count == capacity && !(*out = realloc(*out, (capacity = capacity ? 2 * capacity : 256) * sizeof(**out)))) - fail("out of memory"); - (*out)[count++] = (uint32_t)c; - } - if (*p == ',') ++p; - } - return count; -} - -static double seconds(void) { - struct timespec t; - clock_gettime(CLOCK_MONOTONIC, &t); - return (double)t.tv_sec + (double)t.tv_nsec / 1e9; -} - -#define BUILD_USAGE "usage: vega-symbols build [--font FONT.ttf --codes 0x20-0x7e,...] [--svg CODE=ICON.svg ...] [--units N] -o OUT.vsym" - -static int build(int argc, char **argv) { - const char *font = NULL, *out = NULL, *codes = NULL, **icons = calloc((size_t)argc, sizeof(*icons)); - size_t icon_count = 0; - long units = 0; - for (int i = 2; i < argc; i += 2) { - if (i + 1 == argc) fail(BUILD_USAGE); - if (!strcmp(argv[i], "-o")) out = argv[i + 1]; - else if (!strcmp(argv[i], "--font")) font = argv[i + 1]; - else if (!strcmp(argv[i], "--codes")) codes = argv[i + 1]; - else if (!strcmp(argv[i], "--units")) units = strtol(argv[i + 1], NULL, 10); - else if (!strcmp(argv[i], "--svg")) icons[icon_count++] = argv[i + 1]; - else fail(BUILD_USAGE); - } - if (!out || !font != !codes) fail(BUILD_USAGE); - FT_Library library = NULL; - FT_Face face = NULL; - if (font && (FT_Init_FreeType(&library) || FT_New_Face(library, font, 0, &face))) fail("cannot open the font"); - /* Twice a font's units by default: TrueType's implied points, halfway - * between two control points, then land on whole units. (FreeType finds - * them by integer halving, so outlines are doubled before it walks them.) */ - if (!units) units = face ? 2 * face->units_per_EM : 4096; - if (units < 16 || units > 16384) fail("--units: 16 to 16384"); - uint32_t *list = NULL; - size_t count = codes ? codes_of(codes, &list) : 0; - Built *built = calloc(count + icon_count + 1, sizeof(*built)); - Path path = {.font_scale = face ? (double)units / (2.0 * face->units_per_EM) : 1}; - size_t made = 0; - FT_Outline_Funcs walk = {ft_move, ft_line, ft_conic, ft_cubic, 0, 0}; - for (size_t i = 0; i < count; ++i) { - FT_UInt glyph = FT_Get_Char_Index(face, list[i]); - if (!glyph || FT_Load_Glyph(face, glyph, FT_LOAD_NO_SCALE)) continue; /* the font lacks it */ - size_t first = path.count; - path.open = false; - if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE) { - FT_Outline_Transform(&face->glyph->outline, &(FT_Matrix){0x20000, 0, 0, 0x20000}); - if (FT_Outline_Decompose(&face->glyph->outline, &walk, &path)) fail("cannot walk an outline"); - } - close_path(&path); - long advance = lround((double)face->glyph->metrics.horiAdvance * units / face->units_per_EM); - built[made++] = (Built){list[i], first, path.count - first, (int16_t)(advance > INT16_MAX ? INT16_MAX : advance)}; - } - for (size_t i = 0; i < icon_count; ++i) { - char *equals = strchr(icons[i], '='); - if (!equals) fail("--svg takes CODE=FILE"); - size_t first = path.count; - path.open = false; - int16_t advance; - svg_icon(&path, equals + 1, (uint16_t)units, &advance); - built[made++] = (Built){(uint32_t)strtoul(icons[i], NULL, 0), first, path.count - first, advance}; - } - write_symbols(out, (uint16_t)units, built, made, &path); - if (face) FT_Done_Face(face); - if (library) FT_Done_FreeType(library); - free(icons); - free(list); - free(built); - free(path.items); - return 0; -} - -static VegaSymbols open_symbols(const char *name, char **bytes) { - size_t size; - char error[128]; - VegaSymbols s; - if (!(*bytes = read_file(name, &size))) fail("cannot read the symbol file"); - if (!vega_symbols_open(&s, (unsigned char *)*bytes, size, error, sizeof(error))) fail(error); - return s; -} - -static volatile long sink; /* keeps the timed loops' reads */ -static int ignore_point(const FT_Vector *to, void *user) { ++*(long *)user; (void)to; return 0; } -static int ignore_conic(const FT_Vector *c, const FT_Vector *to, void *user) { (void)c; return ignore_point(to, user); } -static int ignore_cubic(const FT_Vector *c1, const FT_Vector *c2, const FT_Vector *to, void *user) { - (void)c1; - (void)c2; - return ignore_point(to, user); -} - -/* What reading costs, both ways: opening (verifying the symbol file; parsing - * the font's tables) and reaching every symbol's outline. */ -static void measure_open(FT_Library library, const char *font, const VegaSymbols *s) { - size_t font_size; - char *font_bytes = read_file(font, &font_size), error[128]; - if (!font_bytes) fail("cannot read the font"); - int rounds = 200; - double started = seconds(); - for (int i = 0; i < rounds; ++i) { - VegaSymbols again; - if (!vega_symbols_open(&again, s->bytes, s->size, error, sizeof(error))) fail(error); - } - double open_us = (seconds() - started) / rounds * 1e6; - started = seconds(); - for (int i = 0; i < rounds; ++i) { - FT_Face face; - if (FT_New_Memory_Face(library, (const FT_Byte *)font_bytes, (FT_Long)font_size, 0, &face)) fail("cannot open the font"); - FT_Done_Face(face); - } - double freetype_open_us = (seconds() - started) / rounds * 1e6; - FT_Face face; - if (FT_New_Memory_Face(library, (const FT_Byte *)font_bytes, (FT_Long)font_size, 0, &face)) fail("cannot open the font"); - FT_Outline_Funcs walk = {ignore_point, ignore_point, ignore_conic, ignore_cubic, 0, 0}; - long touched = 0, walked = 0; - uint32_t most = 0; - for (uint32_t i = 0; i < s->symbols; ++i) most = vega_symbol_at(s, i).count > most ? vega_symbol_at(s, i).count : most; - VegaCurve *curves = malloc((most + 1) * sizeof(*curves)); - started = seconds(); - for (int r = 0; r < rounds; ++r) - for (uint32_t i = 0; i < s->symbols; ++i) { - VegaSymbol symbol; - if (!vega_symbol(s, vega_symbol_at(s, i).code, &symbol)) fail("lookup"); - uint32_t count = vega_symbol_curves(s, &symbol, curves); - touched += count ? (long)curves[count - 1].x2 : 0; - } - double outline_ns = (seconds() - started) / rounds / s->symbols * 1e9; - started = seconds(); - for (int r = 0; r < rounds; ++r) - for (uint32_t i = 0; i < s->symbols; ++i) { - FT_UInt glyph = FT_Get_Char_Index(face, vega_symbol_at(s, i).code); - if (glyph && !FT_Load_Glyph(face, glyph, FT_LOAD_NO_SCALE)) FT_Outline_Decompose(&face->glyph->outline, &walk, &walked); - } - double freetype_outline_ns = (seconds() - started) / rounds / s->symbols * 1e9; - printf("{\"symbol_bytes\":%zu,\"font_bytes\":%zu,\"open_us\":%.1f,\"freetype_open_us\":%.1f,\"outline_ns\":%.0f," - "\"freetype_outline_ns\":%.0f}\n", s->size, font_size, open_us, freetype_open_us, outline_ns, freetype_outline_ns); - sink = touched + walked; - FT_Done_Face(face); - free(font_bytes); - free(curves); -} - -/* Every symbol with a glyph in the font, at each size: our coverage against - * FreeType's (no hinting, 8-bit gray) over FreeType's bitmap. */ -static int compare(int argc, char **argv) { - if (argc < 6 || strcmp(argv[4], "--sizes")) fail("usage: vega-symbols compare FONT.ttf SYMBOLS.vsym --sizes 12,16,32,64"); - char *bytes; - VegaSymbols s = open_symbols(argv[3], &bytes); - FT_Library library; - FT_Face face; - if (FT_Init_FreeType(&library) || FT_New_Face(library, argv[2], 0, &face)) fail("cannot open the font"); - measure_open(library, argv[2], &s); - for (const char *p = argv[5]; *p;) { - char *end; - long size = strtol(p, &end, 10); - p = *end == ',' ? end + 1 : end; - if (size <= 0 || FT_Set_Pixel_Sizes(face, 0, (FT_UInt)size)) fail("--sizes"); - double scale = (double)size / s.units, total = 0, ours_seconds = 0; - uint64_t pixels = 0, close = 0; - int worst = 0, glyphs = 0; - unsigned char *raster = NULL; - size_t raster_size = 0; - for (uint32_t i = 0; i < s.symbols; ++i) { - VegaSymbol symbol = vega_symbol_at(&s, i); - FT_UInt glyph = FT_Get_Char_Index(face, symbol.code); - if (!glyph || FT_Load_Glyph(face, glyph, FT_LOAD_NO_HINTING | FT_LOAD_RENDER)) continue; - FT_Bitmap *b = &face->glyph->bitmap; - size_t need = (size_t)b->width * b->rows; - if (!need) continue; - if (need > raster_size && !(raster = realloc(raster, raster_size = need))) fail("out of memory"); - double started = seconds(); - vega_symbol_raster(&s, &symbol, scale, face->glyph->bitmap_left / scale, face->glyph->bitmap_top / scale, - (int)b->width, (int)b->rows, raster); - ours_seconds += seconds() - started; - for (unsigned j = 0; j < b->rows; ++j) - for (unsigned i2 = 0; i2 < b->width; ++i2) { - int theirs = b->buffer[j * (unsigned)b->pitch + i2], ours = raster[j * b->width + i2]; - int difference = abs(theirs - ours); - total += difference; - close += difference <= 16; - if (difference > worst) worst = difference; - ++pixels; - } - ++glyphs; - } - printf("{\"pixels_per_em\":%ld,\"glyphs\":%d,\"mean_difference\":%.2f,\"worst\":%d,\"within_16\":%.4f," - "\"reference_us_per_glyph\":%.1f}\n", size, glyphs, pixels ? total / (double)pixels : 0, worst, - pixels ? (double)close / (double)pixels : 1, glyphs ? ours_seconds / glyphs * 1e6 : 0); - free(raster); - } - FT_Done_Face(face); - FT_Done_FreeType(library); - free(bytes); - return 0; -} - -/* Each symbol's code, points, curves, bounds, advance and filled area (in square - * units: the integral of x dy around its outline, as a check against other - * readers of the same fonts and icons). */ -static int info(int argc, char **argv) { - if (argc < 3) fail("usage: vega-symbols info SYMBOLS.vsym"); - char *bytes; - VegaSymbols s = open_symbols(argv[2], &bytes); - printf("{\"units\":%u,\"symbols\":[", s.units); - for (uint32_t i = 0; i < s.symbols; ++i) { - VegaSymbol symbol = vega_symbol_at(&s, i); - double area = 0; - uint32_t curves = 0; - VegaCurve c; - for (uint32_t k = symbol.first; k < symbol.first + symbol.count; ++k) { - if (!vega_symbol_curve(&s, &symbol, k, &c)) continue; - ++curves; - /* x(t) = x0 - 2 bx t + ax t^2, y'(t) = -2 by + 2 ay t, over [0, 1]. */ - double ax = c.x0 - 2 * c.x1 + c.x2, bx = c.x0 - c.x1, ay = c.y0 - 2 * c.y1 + c.y2, by = c.y0 - c.y1; - area += c.x0 * -2 * by + (c.x0 * 2 * ay + 4 * bx * by) / 2 + (-4 * bx * ay - 2 * ax * by) / 3 + ax * ay / 2; - } - printf("%s{\"code\":%u,\"points\":%u,\"curves\":%u,\"bounds\":[%d,%d,%d,%d],\"advance\":%d,\"area\":%.1f}", - i ? "," : "", symbol.code, symbol.count, curves, symbol.left, symbol.bottom, symbol.right, symbol.top, - symbol.advance, area); - } - printf("]}\n"); - free(bytes); - return 0; -} - -static int render(int argc, char **argv) { - if (argc < 6) fail("usage: vega-symbols render SYMBOLS.vsym CODE PIXELS OUT.pgm"); - char *bytes; - VegaSymbols s = open_symbols(argv[2], &bytes); - VegaSymbol symbol; - if (!vega_symbol(&s, (uint32_t)strtoul(argv[3], NULL, 0), &symbol)) fail("no such symbol"); - double pixels = atof(argv[4]), scale = pixels / s.units; - int width = (int)ceil((symbol.advance > symbol.right ? symbol.advance : symbol.right) * scale) + 2; - int height = (int)ceil(pixels * 1.25) + 2; - unsigned char *raster = malloc((size_t)width * (size_t)height); - vega_symbol_raster(&s, &symbol, scale, -1 / scale, (pixels + 1) / scale, width, height, raster); - FILE *f = fopen(argv[5], "wb"); - if (!f) fail("cannot write"); - fprintf(f, "P5\n%d %d\n255\n", width, height); - fwrite(raster, 1, (size_t)width * (size_t)height, f); - fclose(f); - free(raster); - free(bytes); - return 0; -} - -/* ---- The GPU: a context of its own, no window (EGL, Mesa's surfaceless platform). */ - -static void gl_context(void) { - PFNEGLGETPLATFORMDISPLAYEXTPROC platform = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); - EGLDisplay display = platform ? platform(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, NULL) : EGL_NO_DISPLAY; - EGLint major, minor, found; - EGLConfig config; - EGLint wanted[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE}; - EGLint version[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE}; - if (display == EGL_NO_DISPLAY || !eglInitialize(display, &major, &minor) || !eglBindAPI(EGL_OPENGL_ES_API) || - !eglChooseConfig(display, wanted, &config, 1, &found)) - fail("no EGL display for OpenGL ES 3.1"); - /* With no surface, a context needs no config (EGL_KHR_no_config_context). */ - EGLContext context = eglCreateContext(display, found ? config : EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, version); - if (context == EGL_NO_CONTEXT || !eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context)) - fail("no OpenGL ES 3.1 context"); -} - -/* A width x height target of 8-bit red, bound. */ -static GLuint gl_target(int width, int height) { - GLuint texture, framebuffer; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width, height); - glGenFramebuffers(1, &framebuffer); - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) fail("no framebuffer"); - glViewport(0, 0, width, height); - glClearColor(0, 0, 0, 0); - glClear(GL_COLOR_BUFFER_BIT); - return texture; -} - -/* Every symbol in a grid of cells, drawn by the GPU and by vega_coverage: - * how far apart; then pages of text (glyphs cycling through the file) - * drawn again and again: how long a page takes. */ -static int gpu(int argc, char **argv) { - if (argc < 5 || strcmp(argv[3], "--sizes")) fail("usage: vega-symbols gpu SYMBOLS.vsym --sizes 12,16,32,64 [--frames N]"); - int frames = argc > 6 && !strcmp(argv[5], "--frames") ? atoi(argv[6]) : 20; - if (frames < 1) fail("--frames"); - char *bytes, error[4096]; - VegaSymbols s = open_symbols(argv[2], &bytes); - gl_context(); - printf("{\"renderer\":\"%s\"}\n", (const char *)glGetString(GL_RENDERER)); - VegaGl gl; - double started = seconds(); - if (!vega_gl_open(&gl, &s, error, sizeof(error))) fail(error); - glFinish(); - double open_ms = (seconds() - started) * 1e3; - for (const char *p = argv[4]; *p;) { - char *end; - long size = strtol(p, &end, 10); - p = *end == ',' ? end + 1 : end; - if (size <= 0 || size > 512) fail("--sizes"); - double scale = (double)size / s.units; - /* Cells that hold any symbol, so none reaches into another's. */ - int left = 0, bottom = 0, right = 0, top = 0; - for (uint32_t i = 0; i < s.symbols; ++i) { - VegaSymbol symbol = vega_symbol_at(&s, i); - left = symbol.left < left ? symbol.left : left; - bottom = symbol.bottom < bottom ? symbol.bottom : bottom; - right = symbol.right > right ? symbol.right : right; - top = symbol.top > top ? symbol.top : top; - } - int cell = (int)ceil((right - left > top - bottom ? right - left : top - bottom) * scale) + 4; - int across = 4096 / cell, rows = (int)((s.symbols + (uint32_t)across - 1) / (uint32_t)across); - if (rows * cell > 4096) rows = 4096 / cell; - int width = across * cell, height = rows * cell; - uint32_t shown = s.symbols < (uint32_t)(across * rows) ? s.symbols : (uint32_t)(across * rows); - VegaGlyph *glyphs = malloc(shown * sizeof(*glyphs)); - for (uint32_t i = 0; i < shown; ++i) - glyphs[i] = (VegaGlyph){(float)(i % (uint32_t)across * (uint32_t)cell) + 2 - (float)(left * scale), - (float)(i / (uint32_t)across * (uint32_t)cell) + 2 + (float)(top * scale), (float)scale, i}; - GLuint texture = gl_target(width, height); - vega_gl_draw(&gl, &s, glyphs, shown, width, height); - unsigned char *drawn = malloc((size_t)width * (size_t)height), *reference = malloc((size_t)cell * (size_t)cell); - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glReadPixels(0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, drawn); - double total = 0; - uint64_t pixels = 0; - int worst = 0; - for (uint32_t i = 0; i < shown; ++i) { - VegaSymbol symbol = vega_symbol_at(&s, i); - int x0 = (int)(i % (uint32_t)across) * cell, y0 = (int)(i / (uint32_t)across) * cell; - vega_symbol_raster(&s, &symbol, scale, (x0 - glyphs[i].x) / scale, (glyphs[i].y - y0) / scale, cell, cell, reference); - for (int j = 0; j < cell; ++j) - for (int k = 0; k < cell; ++k) { - /* GL's rows run bottom up */ - int gpu_value = drawn[(size_t)(height - 1 - (y0 + j)) * (size_t)width + (size_t)(x0 + k)]; - int difference = abs(gpu_value - reference[j * cell + k]); - total += difference; - worst = difference > worst ? difference : worst; - ++pixels; - } - } - /* Pages: glyphs at their advances in lines, a full target's worth. */ - int page_width = 1920, page_height = 1080; - uint32_t page = 0, capacity = 1 << 16; - VegaGlyph *text = malloc(capacity * sizeof(*text)); - float x = 0, line = (float)size * 1.2f; - for (float y = line; y + (float)size * 0.3f < (float)page_height && page < capacity; ) { - VegaSymbol symbol = vega_symbol_at(&s, page % s.symbols); - float advance = (float)(symbol.advance * scale); - if (x + advance > (float)page_width) { x = 0; y += line; continue; } - text[page++] = (VegaGlyph){x, y, (float)scale, page % s.symbols}; - x += advance; - } - glDeleteTextures(1, &texture); - texture = gl_target(page_width, page_height); - vega_gl_draw(&gl, &s, text, page, page_width, page_height); - glFinish(); - started = seconds(); - for (int f = 0; f < frames; ++f) vega_gl_draw(&gl, &s, text, page, page_width, page_height); - glFinish(); - double page_ms = (seconds() - started) / frames * 1e3; - printf("{\"pixels_per_em\":%ld,\"symbols\":%u,\"mean_difference\":%.3f,\"worst\":%d,\"page_glyphs\":%u," - "\"page_ms\":%.2f,\"upload_ms\":%.1f}\n", size, shown, pixels ? total / (double)pixels : 0, worst, page, page_ms, open_ms); - glDeleteTextures(1, &texture); - free(text); - free(glyphs); - free(drawn); - free(reference); - } - vega_gl_close(&gl); - free(bytes); - return 0; -} - -int main(int argc, char **argv) { - if (argc > 1 && !strcmp(argv[1], "build")) return build(argc, argv); - if (argc > 1 && !strcmp(argv[1], "compare")) return compare(argc, argv); - if (argc > 1 && !strcmp(argv[1], "info")) return info(argc, argv); - if (argc > 1 && !strcmp(argv[1], "render")) return render(argc, argv); - if (argc > 1 && !strcmp(argv[1], "gpu")) return gpu(argc, argv); - fail("vega-symbols build|compare|info|render|gpu ..."); - return 2; -} diff --git a/src/symbols.c b/src/symbols.c deleted file mode 100644 index cedd68f..0000000 --- a/src/symbols.c +++ /dev/null @@ -1,185 +0,0 @@ -#include "symbols.h" -#include <math.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -static const unsigned char magic[8] = {'V', 'E', 'G', 'A', 'S', 'Y', 'M', '2'}; - -static uint16_t u16(const unsigned char *p) { return (uint16_t)(p[0] | p[1] << 8); } -static int16_t i16(const unsigned char *p) { return (int16_t)u16(p); } -static uint32_t u32(const unsigned char *p) { return p[0] | p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; } - -VegaSymbol vega_symbol_at(const VegaSymbols *s, uint32_t index) { - const unsigned char *p = s->bytes + VEGA_SYMBOLS_HEADER + (size_t)index * VEGA_SYMBOL_BYTES; - return (VegaSymbol){u32(p), u32(p + 4), u32(p + 8), i16(p + 12), i16(p + 14), i16(p + 16), i16(p + 18), i16(p + 20)}; -} - -/* A point is two i16s, each its coordinate times two plus a flag: off the - * curve in x, starting a contour in y. */ -VegaPoint vega_point(const VegaSymbols *s, uint32_t index) { - const unsigned char *p = s->bytes + VEGA_SYMBOLS_HEADER + (size_t)s->symbols * VEGA_SYMBOL_BYTES + - (size_t)index * VEGA_POINT_BYTES; - int x = i16(p), y = i16(p + 2); - /* x >> 1 without relying on how a negative number shifts */ - return (VegaPoint){(int16_t)((x - (x & 1)) / 2), (int16_t)((y - (y & 1)) / 2), x & 1, y & 1}; -} - -#define REFUSE(...) do { snprintf(error, error_size, __VA_ARGS__); return false; } while (0) - -bool vega_symbols_open(VegaSymbols *s, const unsigned char *bytes, size_t size, char *error, size_t error_size) { - if (size < VEGA_SYMBOLS_HEADER || memcmp(bytes, magic, sizeof(magic))) REFUSE("not a symbol file"); - *s = (VegaSymbols){bytes, size, u16(bytes + 8), u32(bytes + 12), u32(bytes + 16)}; - uint64_t expected = VEGA_SYMBOLS_HEADER + (uint64_t)s->symbols * VEGA_SYMBOL_BYTES + (uint64_t)s->points * VEGA_POINT_BYTES; - if (u16(bytes + 10) || u32(bytes + 24) || !s->units) REFUSE("header"); - if (u32(bytes + 20) != size || expected != size) REFUSE("size: %zu bytes for %u symbols and %u points", size, s->symbols, s->points); - for (uint32_t i = 0; i < s->symbols; ++i) { - VegaSymbol symbol = vega_symbol_at(s, i); - const unsigned char *p = bytes + VEGA_SYMBOLS_HEADER + (size_t)i * VEGA_SYMBOL_BYTES; - if (u16(p + 22)) REFUSE("symbol %u: flags", i); - if (i && symbol.code <= vega_symbol_at(s, i - 1).code) REFUSE("symbol %u: codes out of order", i); - if (symbol.first > s->points || symbol.count > s->points - symbol.first) REFUSE("symbol %u: points", i); - if (symbol.left > symbol.right || symbol.bottom > symbol.top) REFUSE("symbol %u: bounds", i); - /* An off point then always has both neighbours within the symbol. */ - if (symbol.count) { - VegaPoint first = vega_point(s, symbol.first), last = vega_point(s, symbol.first + symbol.count - 1); - if (first.off || !first.move) REFUSE("symbol %u: its first point does not start a contour on the curve", i); - if (last.off) REFUSE("symbol %u: its last point is off the curve", i); - } - } - return true; -} - -bool vega_symbol(const VegaSymbols *s, uint32_t code, VegaSymbol *out) { - uint32_t low = 0, high = s->symbols; - while (low < high) { - uint32_t middle = low + (high - low) / 2; - VegaSymbol symbol = vega_symbol_at(s, middle); - if (symbol.code == code) { *out = symbol; return true; } - if (symbol.code < code) low = middle + 1; else high = middle; - } - return false; -} - -bool vega_symbol_curve(const VegaSymbols *s, const VegaSymbol *symbol, uint32_t index, VegaCurve *out) { - VegaPoint p = vega_point(s, index); - if (p.off) { - VegaPoint a = vega_point(s, index - 1), b = vega_point(s, index + 1); - *out = (VegaCurve){a.off ? (a.x + p.x) * 0.5f : a.x, a.off ? (a.y + p.y) * 0.5f : a.y, p.x, p.y, - b.off ? (p.x + b.x) * 0.5f : b.x, b.off ? (p.y + b.y) * 0.5f : b.y}; - return true; - } - if (index + 1 >= symbol->first + symbol->count) return false; - VegaPoint b = vega_point(s, index + 1); - if (b.off || b.move) return false; - *out = (VegaCurve){p.x, p.y, (p.x + b.x) * 0.5f, (p.y + b.y) * 0.5f, b.x, b.y}; - return true; -} - -/* Where a t^2 + b t + c = 0 inside (0, 1), appended to ts. */ -static int roots(double a, double b, double c, double *ts) { - int n = 0; - if (fabs(a) < 1e-12) { - if (fabs(b) > 1e-12) ts[n++] = -c / b; - } else { - double d = b * b - 4 * a * c; - if (d >= 0) { - /* The root that does not cancel, and the other by the product. */ - double q = -0.5 * (b + copysign(sqrt(d), b)); - ts[n++] = q / a; - if (q != 0) ts[n++] = c / q; - } - } - int kept = 0; - for (int i = 0; i < n; ++i) if (ts[i] > 0 && ts[i] < 1) ts[kept++] = ts[i]; - return kept; -} - -static double clamp01(double v) { return v < 0 ? 0 : v > 1 ? 1 : v; } - -/* The curve's share of the pixel [0, 1]^2 (points relative to its lower left - * corner, in pixels): the integral of clamp(1 - x, 0, 1) dy along the part - * with y in [0, 1]. Over a closed outline these add up to the covered area, - * negative for a counterclockwise one (Green's theorem), so the covered area - * is exact, as FreeType's is. The curve splits where it crosses the pixel's - * sides; each piece lies left of it (dy counts whole), right of it (nothing), - * or across it (a cubic in t, integrated in closed form). */ -static double curve_area(double x0, double y0, double x1, double y1, double x2, double y2) { - if (fmax(fmax(y0, y1), y2) <= 0 || fmin(fmin(y0, y1), y2) >= 1 || fmin(fmin(x0, x1), x2) >= 1) return 0; - if (fmax(fmax(x0, x1), x2) <= 0) return clamp01(y2) - clamp01(y0); - /* x(t) = x0 - 2 bx t + ax t^2, and y alike. */ - double ax = x0 - 2 * x1 + x2, bx = x0 - x1, ay = y0 - 2 * y1 + y2, by = y0 - y1; - double ts[10] = {0}; - int n = 1; - n += roots(ay, -2 * by, y0, ts + n); - n += roots(ay, -2 * by, y0 - 1, ts + n); - n += roots(ax, -2 * bx, x0, ts + n); - n += roots(ax, -2 * bx, x0 - 1, ts + n); - ts[n++] = 1; - for (int i = 1; i < n; ++i) - for (int j = i; j > 0 && ts[j] < ts[j - 1]; --j) { double t = ts[j]; ts[j] = ts[j - 1]; ts[j - 1] = t; } - /* (1 - x(t)) y'(t) = (u0 + u1 t + u2 t^2)(v0 + v1 t) */ - double u0 = 1 - x0, u1 = 2 * bx, u2 = -ax, v0 = -2 * by, v1 = 2 * ay; - double k1 = u0 * v0, k2 = (u0 * v1 + u1 * v0) / 2, k3 = (u1 * v1 + u2 * v0) / 3, k4 = u2 * v1 / 4, area = 0; - for (int i = 0; i + 1 < n; ++i) { - double ta = ts[i], tb = ts[i + 1], t = (ta + tb) / 2; - if (tb <= ta) continue; - double y = (ay * t - 2 * by) * t + y0, x = (ax * t - 2 * bx) * t + x0; - if (y <= 0 || y >= 1 || x >= 1) continue; - if (x <= 0) area += ((ay * tb - 2 * by) * tb) - ((ay * ta - 2 * by) * ta); - else area += (((k4 * tb + k3) * tb + k2) * tb + k1) * tb - (((k4 * ta + k3) * ta + k2) * ta + k1) * ta; - } - return area; -} - -/* Without branches on the flags, which no predictor guesses: implied points - * and whether a point begins a curve come out of arithmetic on them. */ -uint32_t vega_symbol_curves(const VegaSymbols *s, const VegaSymbol *symbol, VegaCurve *out) { - if (!symbol->count) return 0; - const unsigned char *p = s->bytes + VEGA_SYMBOLS_HEADER + (size_t)s->symbols * VEGA_SYMBOL_BYTES + - (size_t)symbol->first * VEGA_POINT_BYTES; - uint32_t count = 0; - int x = i16(p), y = i16(p + 2), off = x & 1; - float bx = 0, by = 0, bo = 0, hx = (float)(x - off) * 0.5f, hy = (float)(y - (y & 1)) * 0.5f, ho = (float)off; - for (uint32_t k = 1; k < symbol->count; ++k) { - p += VEGA_POINT_BYTES; - x = i16(p); - y = i16(p + 2); - float nx = (float)(x - (x & 1)) * 0.5f, ny = (float)(y - (y & 1)) * 0.5f, no = (float)(x & 1); - /* before, or halfway to here if before is off; here if here is on */ - float sx = bx + (hx - bx) * 0.5f * bo, sy = by + (hy - by) * 0.5f * bo; - out[count] = (VegaCurve){hx + (sx - hx) * ho, hy + (sy - hy) * ho, hx + (nx - hx) * 0.5f * (1 - ho), - hy + (ny - hy) * 0.5f * (1 - ho), nx + (hx - nx) * 0.5f * no, ny + (hy - ny) * 0.5f * no}; - /* an off point begins a curve, and so does an on point before an on - * point of the same contour (a line) */ - count += (uint32_t)(off | (((x & 1) ^ 1) & ((y & 1) ^ 1))); - off = x & 1; - bx = hx, by = hy, bo = ho; - hx = nx, hy = ny, ho = no; - } - return count; -} - -double vega_coverage(const VegaCurve *curves, uint32_t count, double scale, double x, double y) { - /* The pixel's lower left corner, in units. */ - double left = x - 0.5 / scale, bottom = y - 0.5 / scale, area = 0; - for (uint32_t k = 0; k < count; ++k) { - const VegaCurve *c = &curves[k]; - area += curve_area((c->x0 - left) * scale, (c->y0 - bottom) * scale, (c->x1 - left) * scale, (c->y1 - bottom) * scale, - (c->x2 - left) * scale, (c->y2 - bottom) * scale); - } - area = fabs(area); - return area > 1 ? 1 : area; -} - -void vega_symbol_raster(const VegaSymbols *s, const VegaSymbol *symbol, double scale, double left, double top, int width, - int height, unsigned char *out) { - VegaCurve *curves = malloc((symbol->count ? symbol->count : 1) * sizeof(*curves)); - uint32_t count = curves ? vega_symbol_curves(s, symbol, curves) : 0; - for (int j = 0; j < height; ++j) - for (int i = 0; i < width; ++i) { - double coverage = vega_coverage(curves, count, scale, left + (i + 0.5) / scale, top - (j + 0.5) / scale); - out[(size_t)j * (size_t)width + (size_t)i] = (unsigned char)lround(coverage * 255); - } - free(curves); -} diff --git a/src/symbols.h b/src/symbols.h deleted file mode 100644 index 998ff3f..0000000 --- a/src/symbols.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef VEGA_SYMBOLS_H -#define VEGA_SYMBOLS_H -#include <stdbool.h> -#include <stddef.h> -#include <stdint.h> - -/* Symbols: glyphs and icons as outlines of quadratic curves in one file, - * verified once and then read where it lies (docs/design/002-symbol-format.md). - * An outline is a chain of points, on or off the curve, as in TrueType: two - * off points in a row imply the on point halfway between them. */ -enum { VEGA_SYMBOLS_HEADER = 28, VEGA_SYMBOL_BYTES = 24, VEGA_POINT_BYTES = 4 }; -typedef struct { - int16_t x, y; - bool off; /* a control point */ - bool move; /* starts a contour */ -} VegaPoint; -typedef struct { float x0, y0, x1, y1, x2, y2; } VegaCurve; /* a line has its middle as control */ -typedef struct { - uint32_t code, first, count; /* its points: [first, first + count) */ - int16_t left, bottom, right, top; /* bounds, in units, y up */ - int16_t advance; -} VegaSymbol; -typedef struct { - const unsigned char *bytes; - size_t size; - uint16_t units; /* per em */ - uint32_t symbols, points; -} VegaSymbols; - -/* Verify bytes as a symbol file (kept, not copied); false with why. What - * reading needs is checked, per symbol: after this no read leaves the file - * or its symbol. The points themselves are not: one outside its symbol's - * bounds draws wrong. */ -bool vega_symbols_open(VegaSymbols *, const unsigned char *bytes, size_t size, char *error, size_t error_size); -/* The symbol of code (a binary search), or false. */ -bool vega_symbol(const VegaSymbols *, uint32_t code, VegaSymbol *out); -VegaSymbol vega_symbol_at(const VegaSymbols *, uint32_t index); -VegaPoint vega_point(const VegaSymbols *, uint32_t index); -/* The curve that point index of symbol begins, if any: an off point is the - * control of one, and an on point followed by another on point in the same - * contour starts a line. Every curve of a symbol is had once this way, from - * the point and its two neighbours alone (as a GPU would fetch it). */ -bool vega_symbol_curve(const VegaSymbols *, const VegaSymbol *, uint32_t index, VegaCurve *out); -/* All of a symbol's curves, in one pass over its points; out has room for - * symbol->count (a symbol has no more curves than points). */ -uint32_t vega_symbol_curves(const VegaSymbols *, const VegaSymbol *, VegaCurve *out); -/* The coverage (0 to 1) of the pixel whose center is (x, y) in units, at - * scale pixels per unit: the exact area the curves' outline covers. */ -double vega_coverage(const VegaCurve *, uint32_t count, double scale, double x, double y); -/* width x height 8-bit coverage, rows top first; pixel (i, j) has its center - * at (left + (i + 0.5) / scale, top - (j + 0.5) / scale) in units. */ -void vega_symbol_raster(const VegaSymbols *, const VegaSymbol *, double scale, double left, double top, int width, - int height, unsigned char *out); -#endif diff --git a/src/vega.h b/src/vega.h deleted file mode 100644 index f01667b..0000000 --- a/src/vega.h +++ /dev/null @@ -1,124 +0,0 @@ -#ifndef VEGA_H -#define VEGA_H -#include "module.h" -#include <stdio.h> - -/* Vega reads a verified Pure module by convention (docs/design/001-first-component.md): - * - a text view named `title` is the screen's title; - * - a table view with a text column `label` is a list; any other table is a - * table. A number column `tone` gives a row its tone; text columns `detail` - * and `badge` add to a list row; - * - a number view named `<table>_focus` is the focused row of that table; - * - every other number or text view is a field. - * A tone says what a value means (lib/vega.aup); each surface decides how it - * looks or sounds. Surfaces evaluate only the cells they present. */ -enum { VEGA_PARTS = 64, VEGA_TEXT = 1024 }; -typedef enum { VEGA_NORMAL, VEGA_MUTED, VEGA_ACCENT, VEGA_GOOD, VEGA_WARNING, VEGA_DANGER, VEGA_TONES } VegaTone; -typedef enum { VEGA_FIELD, VEGA_LIST, VEGA_TABLE } VegaKind; - -typedef struct { - VegaKind kind; - uint32_t view; - int focus; /* number view with the focused row, or -1 */ - int pointer; /* the index state that view shows, or -1: a surface may move it */ - int label, tone, detail, badge; /* columns, or -1 */ - uint32_t columns[PURE_MODULE_VIEWS]; /* a table's shown columns, in order */ - unsigned column_count; -} VegaPart; - -typedef struct { - const PureModule *module; - int title; /* text view, or -1 */ - int prompt; /* the first text input, or -1 */ - bool ticks; /* it handles frames: a surface runs them, about 60 a second */ - VegaPart parts[VEGA_PARTS]; - unsigned count; -} VegaScreen; - -void vega_screen(VegaScreen *, const PureModule *); - -/* The wall-clock time events see on a live surface: milliseconds since 1970 - * (UTC). Scripts set their own (time MS). */ -double vega_now(void); -/* When each timer input ticks next. run ticks every one due at now (a tick - * sees now) and gives the milliseconds until the next, or -1 without timers; - * ticked says whether any did. Missed ticks are not made up. */ -typedef struct { double next[PURE_MODULE_INPUTS]; } VegaTimers; -void vega_timers_start(VegaTimers *, const PureModule *, double now); -long vega_timers_run(VegaTimers *, PureApp *, double now, bool *ticked); -/* What a module seems to mean but does not meet: a list label that is not - * text, a _focus view without its table, a column a list never shows, a - * second text input. Each finding names the view or input it is about; - * returns how many there were. */ -unsigned vega_check(const PureModule *, void (*report)(void *, const char *name, const char *message), void *context); - -/* A view (row ignored) or a column at a row as text; numbers in Pure's - * shortest form. false when it has no value (for example a missing item). */ -bool vega_text(PureApp *, uint32_t view, size_t row, char out[VEGA_TEXT], size_t *length); -bool vega_number(PureApp *, uint32_t view, size_t row, double *out); -size_t vega_rows(PureApp *, const VegaPart *); -/* The row of the focused item: the focus view names an item (an index's - * position), shown on the row that holds it; VEGA_NO_ROW when a filtered - * list leaves that item out. Clamped to the rows without a filter; 0 - * without a focus view. */ -#define VEGA_NO_ROW SIZE_MAX -size_t vega_focus(PureApp *, const VegaPart *); -/* Focus a row, as a user pointing at it (a click); false when the part's - * focus is not a plain index position or the row is not there. */ -bool vega_point(PureApp *, const VegaPart *, size_t row); -/* The first row to show of `rows` when `visible` fit: the least scrolling - * from first that keeps the focused row in view (any, without one). */ -size_t vega_scroll(size_t first, size_t focus, size_t visible, size_t rows); -/* The key input answering a key name (Pure's names, as KeyboardEvent.code - * spells them), or -1; press sends its press and release. */ -int vega_key_input(const PureModule *, const char *key); -bool vega_press(PureApp *, const char *key); -/* Commit text to the screen's text input. */ -bool vega_commit(PureApp *, const VegaScreen *, const char *text, size_t length); -VegaTone vega_tone(PureApp *, const VegaPart *, size_t row); -/* Values evaluated so far, for checking that surfaces stay lazy. */ -unsigned long vega_evaluations(void); - -/* Display cells of a code point in a terminal: 0 for combining and - * zero-width characters, 2 for East Asian wide and emoji, else 1. */ -unsigned vega_width(uint32_t codepoint); -/* The code point at text[at] (valid UTF-8) and the bytes it takes. */ -size_t vega_decode(const char *text, size_t length, size_t at, uint32_t *codepoint); -/* A control character shown as its control picture (U+2400 block), else itself. */ -uint32_t vega_visible(uint32_t codepoint); -unsigned vega_text_width(const char *text, size_t length); - -/* CLI: everything, as plain lines for pipes; color only when asked. */ -void vega_print(FILE *, PureApp *, const VegaScreen *, bool color); -/* NUI: the screen as sentences (all), or only the focused item of the first list. */ -void vega_say(FILE *, PureApp *, const VegaScreen *, bool all); -/* GUI: the screen as a web page on listen:port (0: any free port, printed as - * {"port":N}); runs until SIGINT or SIGTERM. reload, when given, is asked - * a few times a second whether the module was replaced (the screen with it). */ -int vega_web(PureApp *, const VegaScreen *, const char *listen, unsigned port, bool (*reload)(void)); - -/* TUI: a grid of cells, redrawn from the app on every change. first keeps each - * list's scroll between frames; the focused row stays visible. shown is what - * the terminal shows, so a paint sends only the cells that changed (all of - * them until the first paint after a resize). */ -typedef struct { - uint32_t ch, mark; /* a character and at most one combining mark; ch 0 continues a wide one */ - uint8_t tone; - bool focus, strong; -} VegaCell; -typedef struct { - unsigned width, height; - VegaCell *cells, *shown; - struct VegaRow { int part; size_t item; } *rows; /* the item each line shows, or part -1 */ - bool painted; /* shown holds what the terminal shows */ - size_t first[VEGA_PARTS]; - int cursor_row, cursor_column; /* where the terminal cursor goes, or -1 */ -} VegaGrid; -bool vega_grid(VegaGrid *, unsigned width, unsigned height); -void vega_grid_free(VegaGrid *); -/* prompt is the text being typed for the screen's text input; typing says - * whether keys go to it. */ -void vega_draw(VegaGrid *, PureApp *, const VegaScreen *, const char *prompt, size_t prompt_length, bool typing); -/* The part and item a line of the last drawn grid shows. */ -bool vega_hit(const VegaGrid *, unsigned line, unsigned *part, size_t *item); -#endif diff --git a/src/web.c b/src/web.c deleted file mode 100644 index 535d92f..0000000 --- a/src/web.c +++ /dev/null @@ -1,453 +0,0 @@ -#define _GNU_SOURCE -#include "vega.h" -#include <arpa/inet.h> -#include <errno.h> -#include <fcntl.h> -#include <netinet/in.h> -#include <poll.h> -#include <signal.h> -#include <stdlib.h> -#include <string.h> -#include <sys/socket.h> -#include <unistd.h> - -/* vega web: the screen as a web page, a GUI surface. The page asks for the - * screen as JSON with the rows it can show per list or table, so only the - * cells on the page are evaluated; it sends keys (KeyboardEvent.code names are - * Pure's), committed text and clicks on rows, and an event stream tells every - * open page when the app changed. - * - * GET / the page - * GET /screen?rows=R&first=F,G title, fields, each list or table with R rows - * from its last first row (F, G, ... by part), keys - * POST /key KEYNAME press and release the input bound to the key - * POST /text UTF-8 commit text to the screen's text input - * POST /point PART ROW focus a row of a list or table (0 first) - * GET /events server-sent events: one per change */ - -enum { CLIENTS = 64, REQUEST = 1 << 16, ROWS_MAX = 500 }; - -typedef struct { char *bytes; size_t used, capacity; } Buffer; - -typedef struct { - int fd; - bool events; /* an open event stream */ - bool closing; - char in[REQUEST]; - size_t in_used; - Buffer out; - size_t out_at; -} Client; - -static const char *const tone_names[VEGA_TONES] = {"normal", "muted", "accent", "good", "warning", "danger"}; -static volatile sig_atomic_t stopping; - -static void on_stop(int signal_number) { - (void)signal_number; - stopping = 1; -} - -static void put(Buffer *b, const void *bytes, size_t size) { - if (b->used + size > b->capacity) { - size_t capacity = b->capacity ? b->capacity : 4096; - while (capacity < b->used + size) capacity *= 2; - char *grown = realloc(b->bytes, capacity); - if (!grown) return; - b->bytes = grown; - b->capacity = capacity; - } - memcpy(b->bytes + b->used, bytes, size); - b->used += size; -} - -static void puts_(Buffer *b, const char *text) { put(b, text, strlen(text)); } - -static void putf(Buffer *b, const char *format, ...) __attribute__((format(printf, 2, 3))); -#include <stdarg.h> -static void putf(Buffer *b, const char *format, ...) { - char line[256]; - va_list args; - va_start(args, format); - int n = vsnprintf(line, sizeof(line), format, args); - va_end(args); - if (n > 0) put(b, line, (size_t)n < sizeof(line) ? (size_t)n : sizeof(line) - 1); -} - -/* A JSON string: UTF-8 as it is, quotes, backslashes and control characters escaped. */ -static void json_string(Buffer *b, const char *text, size_t length) { - put(b, "\"", 1); - for (size_t i = 0; i < length; ++i) { - unsigned char c = (unsigned char)text[i]; - if (c == '"' || c == '\\') { put(b, "\\", 1); put(b, &text[i], 1); } - else if (c < 0x20 || c == 0x7F) putf(b, "\\u%04x", c); - else put(b, &text[i], 1); - } - put(b, "\"", 1); -} - -static void json_name(Buffer *b, const char *name) { json_string(b, name, strlen(name)); } - -/* A view's text at a row, or null. */ -static void json_view(Buffer *b, PureApp *app, uint32_t view, size_t row) { - char text[VEGA_TEXT]; - size_t length; - if (vega_text(app, view, row, text, &length)) json_string(b, text, length); - else puts_(b, "null"); -} - -static void screen_json(Buffer *b, PureApp *app, const VegaScreen *screen, size_t visible, size_t *firsts) { - const PureModule *m = app->module; - puts_(b, "{\"title\":"); - if (screen->title >= 0) json_view(b, app, (uint32_t)screen->title, 0); - else puts_(b, "null"); - puts_(b, ",\"fields\":["); - bool comma = false; - for (unsigned i = 0; i < screen->count; ++i) { - const VegaPart *part = &screen->parts[i]; - if (part->kind != VEGA_FIELD) continue; - puts_(b, comma ? ",{\"name\":" : "{\"name\":"); - json_name(b, m->view_names[part->view]); - puts_(b, ",\"text\":"); - json_view(b, app, part->view, 0); - puts_(b, "}"); - comma = true; - } - puts_(b, "],\"parts\":["); - comma = false; - for (unsigned i = 0; i < screen->count; ++i) { - const VegaPart *part = &screen->parts[i]; - if (part->kind == VEGA_FIELD) continue; - size_t rows = vega_rows(app, part), focus = vega_focus(app, part); - size_t first = vega_scroll(firsts[i], focus, visible, rows); - firsts[i] = first; - puts_(b, comma ? ",{\"part\":" : "{\"part\":"); - putf(b, "%u,\"name\":", i); - json_name(b, m->view_names[part->view]); - putf(b, ",\"kind\":\"%s\",\"rows\":%zu,\"focus\":", part->kind == VEGA_LIST ? "list" : "table", rows); - if (focus < rows) putf(b, "%zu", focus); - else puts_(b, "null"); - putf(b, ",\"first\":%zu,\"pointable\":%s,\"columns\":[", first, part->pointer >= 0 ? "true" : "false"); - if (part->kind == VEGA_TABLE) - for (unsigned c = 0; c < part->column_count; ++c) { - if (c) puts_(b, ","); - json_name(b, m->view_names[part->columns[c]]); - } - puts_(b, "],\"items\":["); - for (size_t row = first; row < first + visible && row < rows; ++row) { - putf(b, "%s{\"row\":%zu,\"tone\":\"%s\"", row > first ? "," : "", row, - tone_names[vega_tone(app, part, row)]); - if (part->kind == VEGA_LIST) { - const int columns[] = {part->label, part->badge, part->detail}; - const char *const names[] = {"label", "badge", "detail"}; - for (unsigned k = 0; k < 3; ++k) { - if (columns[k] < 0) continue; - putf(b, ",\"%s\":", names[k]); - json_view(b, app, (uint32_t)columns[k], row); - } - } else { - puts_(b, ",\"cells\":["); - for (unsigned c = 0; c < part->column_count; ++c) { - if (c) puts_(b, ","); - json_view(b, app, part->columns[c], row); - } - puts_(b, "]"); - } - puts_(b, "}"); - } - puts_(b, "]}"); - comma = true; - } - puts_(b, "],\"keys\":["); - comma = false; - for (uint32_t i = 0; i < m->inputs; ++i) { - if (m->input[i].kind != PURE_KIND_KEY) continue; - bool bound = false; - for (uint32_t h = 0; h < m->handlers && !bound; ++h) bound = m->handler[h].event == 1 + 2 * i; - if (!bound) continue; - puts_(b, comma ? ",{\"input\":" : "{\"input\":"); - json_name(b, m->input_names[i]); - puts_(b, ",\"key\":"); - json_name(b, m->input_keys[i]); - puts_(b, "}"); - comma = true; - } - puts_(b, "],\"prompt\":"); - if (screen->prompt >= 0) json_name(b, m->input_names[screen->prompt]); - else puts_(b, "null"); - putf(b, ",\"events\":%llu}", (unsigned long long)app->events); -} - -/* The page: one file, no dependencies. It keeps each part's first row, asks - * for as many rows as fit, and redraws when the event stream says so. */ -static const char *const page_parts[] = { -"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">" -"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Vega</title><style>" -":root{color-scheme:light dark;--fg:#1d1d1f;--bg:#fbfbfd;--line:#d2d2d7;--focus:#0a58ca;--muted:#86868b;" -"--accent:#0071e3;--good:#1e8e3e;--warning:#b06000;--danger:#d93025}" -"@media(prefers-color-scheme:dark){:root{--fg:#f5f5f7;--bg:#161618;--line:#3a3a3c;--focus:#4c8dff;--muted:#8e8e93;" -"--accent:#64a8ff;--good:#5ec26a;--warning:#f0a33a;--danger:#ff6b5e}}" -"body{margin:0;font:15px/1.5 system-ui,-apple-system,'Noto Sans KR',sans-serif;color:var(--fg);background:var(--bg)}" -"main{max-width:760px;margin:0 auto;padding:24px 16px}h1{font-size:22px;margin:0 0 12px}" -"dl{display:grid;grid-template-columns:max-content 1fr;gap:2px 16px;margin:0 0 16px}dt{color:var(--muted)}dd{margin:0}" -"section{margin:0 0 20px}h2{font-size:14px;font-weight:600;margin:0 0 6px}h2 span{color:var(--muted);font-weight:400}" -"ul{list-style:none;margin:0;padding:0;border:1px solid var(--line);border-radius:8px;overflow:hidden}" -"li,tbody tr{cursor:pointer}li{display:flex;gap:10px;padding:7px 12px;border-top:1px solid var(--line)}" -"li:first-child{border-top:0}.label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:pre}" -".detail{color:var(--muted);white-space:pre}.badge{font-size:12px;padding:1px 8px;border:1px solid currentColor;" -"border-radius:10px;align-self:center}[aria-selected=true]{background:var(--focus);color:#fff}" -"table{width:100%;border-collapse:collapse;border:1px solid var(--line)}th{text-align:left;color:var(--muted);" -"font-weight:500}th,td{padding:6px 12px;border-top:1px solid var(--line);white-space:pre}" -".muted{color:var(--muted)}.accent{color:var(--accent)}.good{color:var(--good)}.warning{color:var(--warning)}" -".danger{color:var(--danger)}[aria-selected=true].muted,[aria-selected=true] *{color:#fff}" -"form{display:flex;gap:8px;margin-top:16px}input{flex:1;font:inherit;padding:8px 10px;border:1px solid var(--line);" -"border-radius:8px;background:transparent;color:inherit}button{font:inherit;padding:8px 16px;border:1px solid var(--line);" -"border-radius:8px;background:transparent;color:inherit;cursor:pointer}button:hover{border-color:var(--focus)}" -"footer{margin-top:14px;color:var(--muted);font-size:13px}" -"kbd{font:12px ui-monospace,monospace;border:1px solid var(--line);border-radius:4px;padding:0 5px;margin-right:4px}" -"</style></head><body><main>", -"<h1 id=title></h1><dl id=fields></dl><div id=parts></div>" -"<form id=prompt hidden><input id=entry autocomplete=off><button>Enter</button></form><footer id=keys></footer></main>" -"<script>", -"'use strict';let screen=null,firsts=[],busy=false,again=false;const rows=Math.max(5,Math.floor((innerHeight-220)/34));" -"const $=id=>document.getElementById(id);" -"const el=(tag,props,...kids)=>{const e=document.createElement(tag);Object.assign(e,props||{});" -"for(const k of kids)if(k!=null)e.append(k);return e};" -"async function post(path,body){await fetch(path,{method:'POST',body});refresh()}" -"async function refresh(){if(busy){again=true;return}busy=true;try{" -"const r=await fetch('/screen?rows='+rows+'&first='+firsts.join(','));screen=await r.json();render()}" -"finally{busy=false;if(again){again=false;refresh()}}}" -"function render(){const s=screen;$('title').textContent=s.title?\?'';document.title=s.title?\?'Vega';" -"$('fields').replaceChildren(...s.fields.flatMap(f=>[el('dt',{textContent:f.name}),el('dd',{textContent:f.text?\?'-'})]));" -"firsts=s.parts.map(p=>p.first);" -"$('parts').replaceChildren(...s.parts.map(p=>{const head=el('h2',{},p.name+' ',el('span',{textContent:'('+p.rows+')'}));" -"const pick=row=>{if(p.pointable)post('/point',p.part+' '+row)};" -"if(p.kind==='list'){const list=el('ul',{role:'listbox'});list.setAttribute('aria-label',p.name);" -"for(const it of p.items){const item=el('li',{className:it.tone,onclick:()=>pick(it.row)}," -"el('span',{className:'label',textContent:it.label?\?''}),it.detail?el('span',{className:'detail',textContent:it.detail}):null," -"it.badge?el('span',{className:'badge',textContent:it.badge}):null);item.setAttribute('role','option');" -"item.setAttribute('aria-selected',it.row===p.focus);list.append(item)}" -"list.onwheel=e=>{e.preventDefault();pick(Math.max(0,Math.min(p.rows-1,(p.focus?\?-1)+Math.sign(e.deltaY))))};" -"return el('section',{},head,list)}" -"const table=el('table',{},el('thead',{},el('tr',{},...p.columns.map(c=>el('th',{textContent:c})))));" -"const body=el('tbody');for(const it of p.items){const tr=el('tr',{className:it.tone,onclick:()=>pick(it.row)}," -"...it.cells.map(c=>el('td',{textContent:c?\?'-'})));tr.setAttribute('aria-selected',it.row===p.focus);body.append(tr)}" -"table.append(body);table.onwheel=e=>{e.preventDefault();pick(Math.max(0,Math.min(p.rows-1,(p.focus?\?-1)+Math.sign(e.deltaY))))};" -"return el('section',{},head,table)}));" -"$('prompt').hidden=!s.prompt;if(s.prompt)$('entry').placeholder=s.prompt;" -"const symbols={ArrowUp:'\u2191',ArrowDown:'\u2193',ArrowLeft:'\u2190',ArrowRight:'\u2192',Backspace:'Bksp'," -"Delete:'Del',Escape:'Esc',PageUp:'PgUp',PageDown:'PgDn'};" -"$('keys').replaceChildren(...s.keys.map(k=>el('span',{},el('kbd',{textContent:symbols[k.key]?\?k.key.replace(/^(Key|Digit)/,'')})," -"k.input+' ')))}" -"$('prompt').onsubmit=e=>{e.preventDefault();const v=$('entry').value;if(v){$('entry').value='';post('/text',v)}};" -"addEventListener('keydown',e=>{if(!screen||e.ctrlKey||e.metaKey||e.altKey)return;" -"const typing=document.activeElement===$('entry');" -"if(typing&&!['ArrowUp','ArrowDown','PageUp','PageDown','Escape'].includes(e.code))return;" -"if(screen.keys.some(k=>k.key===e.code)){e.preventDefault();post('/key',e.code)}});" -"new EventSource('/events').onmessage=refresh;refresh();" -"</script></body></html>"}; - -/* The page, joined once from its pieces. */ -static const Buffer *page(void) { - static Buffer joined; - if (!joined.used) - for (size_t i = 0; i < sizeof(page_parts) / sizeof(*page_parts); ++i) puts_(&joined, page_parts[i]); - return &joined; -} - -static void respond(Client *c, int status, const char *reason, const char *type, const char *body, size_t length) { - putf(&c->out, "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\nCache-Control: no-store\r\n%s\r\n", - status, reason, type, length, c->closing ? "Connection: close\r\n" : ""); - put(&c->out, body, length); -} - -static bool query_number(const char *query, const char *name, size_t *out) { - size_t length = strlen(name); - for (const char *at = query; at && *at; at = strchr(at, '&') ? strchr(at, '&') + 1 : NULL) - if (!strncmp(at, name, length) && at[length] == '=') { - *out = strtoul(at + length + 1, NULL, 10); - return true; - } - return false; -} - -/* first=F,G,...: the parts' first rows, in the order of the screen's parts. */ -static void query_firsts(const char *query, const VegaScreen *screen, size_t *firsts) { - const char *at = query ? strstr(query, "first=") : NULL; - if (!at || (at != query && at[-1] != '&')) return; - at += 6; - for (unsigned i = 0; i < screen->count && *at && *at != '&'; ++i) { - if (screen->parts[i].kind == VEGA_FIELD) continue; - firsts[i] = strtoul(at, (char **)&at, 10); - if (*at == ',') ++at; - } -} - -/* One request. Returns whether the app changed. */ -static bool handle(Client *c, PureApp *app, const VegaScreen *screen, const char *method, char *target, - const char *body, size_t body_size) { - char *query = strchr(target, '?'); - if (query) *query++ = 0; - bool get = !strcmp(method, "GET"), post = !strcmp(method, "POST"); - uint64_t before = app->events; - if (get && !strcmp(target, "/")) { - respond(c, 200, "OK", "text/html; charset=utf-8", page()->bytes, page()->used); - return false; - } - if (get && !strcmp(target, "/screen")) { - size_t visible = 50, firsts[VEGA_PARTS] = {0}; - query_number(query, "rows", &visible); - if (visible > ROWS_MAX) visible = ROWS_MAX; - query_firsts(query, screen, firsts); - Buffer json = {0}; - screen_json(&json, app, screen, visible, firsts); - respond(c, 200, "OK", "application/json", json.bytes ? json.bytes : "", json.used); - free(json.bytes); - return false; - } - if (get && !strcmp(target, "/events")) { - puts_(&c->out, "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-store\r\n\r\n"); - putf(&c->out, "data: %llu\n\n", (unsigned long long)app->events); - c->events = true; - return false; - } - if (post && (!strcmp(target, "/key") || !strcmp(target, "/text") || !strcmp(target, "/point"))) { - char text[VEGA_TEXT]; - size_t length = body_size < sizeof(text) - 1 ? body_size : sizeof(text) - 1; - memcpy(text, body, length); - text[length] = 0; - bool ok = false; - if (target[1] == 'k') ok = vega_press(app, text); - else if (target[1] == 't') ok = vega_commit(app, screen, text, length); - else { - unsigned part = 0; - long row = -1; - ok = sscanf(text, "%u %ld", &part, &row) == 2 && part < screen->count && row >= 0 && - screen->parts[part].kind != VEGA_FIELD && vega_point(app, &screen->parts[part], (size_t)row); - } - const char *answer = ok ? "{\"ok\":true}" : "{\"ok\":false}"; - respond(c, 200, "OK", "application/json", answer, strlen(answer)); - return app->events != before; - } - respond(c, 404, "Not Found", "text/plain", "not found\n", 10); - return false; -} - -/* The requests complete in c->in, answered in order. */ -static bool serve(Client *c, PureApp *app, const VegaScreen *screen) { - bool changed = false; - while (!c->events && !c->closing) { - char *end = memmem(c->in, c->in_used, "\r\n\r\n", 4); - if (!end) { - if (c->in_used == sizeof(c->in)) c->closing = true; - return changed; - } - size_t head = (size_t)(end - c->in) + 4, body_size = 0; - *end = 0; - char *length = strcasestr(c->in, "\r\ncontent-length:"); - if (length) body_size = strtoul(length + 17, NULL, 10); - if (body_size > sizeof(c->in) - head) { c->closing = true; return changed; } - if (c->in_used < head + body_size) { *end = '\r'; return changed; } - if (strcasestr(c->in, "\r\nconnection: close")) c->closing = true; - char method[8] = "", target[1024] = ""; - if (sscanf(c->in, "%7s %1023s", method, target) == 2) - changed = handle(c, app, screen, method, target, c->in + head, body_size) || changed; - else c->closing = true; - memmove(c->in, c->in + head + body_size, c->in_used - head - body_size); - c->in_used -= head + body_size; - } - return changed; -} - -static void drop(Client *c) { - close(c->fd); - free(c->out.bytes); - c->fd = -1; -} - -int vega_web(PureApp *app, const VegaScreen *screen, const char *listen_on, unsigned port, bool (*reload)(void)) { - int listener = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0), one = 1; - struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port)}; - socklen_t size = sizeof(address); - if (listener < 0 || inet_pton(AF_INET, listen_on, &address.sin_addr) != 1 || - setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) || - bind(listener, (struct sockaddr *)&address, sizeof(address)) || listen(listener, 64) || - getsockname(listener, (struct sockaddr *)&address, &size)) { - fprintf(stderr, "vega: cannot listen on %s:%u\n", listen_on, port); - return 2; - } - printf("{\"port\":%u}\n", ntohs(address.sin_port)); - fflush(stdout); - signal(SIGINT, on_stop); - signal(SIGTERM, on_stop); - signal(SIGPIPE, SIG_IGN); - static Client clients[CLIENTS]; - for (unsigned i = 0; i < CLIENTS; ++i) clients[i].fd = -1; - VegaTimers timers; - vega_timers_start(&timers, app->module, vega_now()); - while (!stopping) { - /* Tick what is due; every page hears of it this round, without waiting. */ - bool ticked = false; - long due = vega_timers_run(&timers, app, vega_now(), &ticked); - struct pollfd fds[CLIENTS + 1]; - unsigned slot[CLIENTS + 1], count = 1; - fds[0] = (struct pollfd){listener, POLLIN, 0}; - for (unsigned i = 0; i < CLIENTS; ++i) { - Client *c = &clients[i]; - if (c->fd < 0) continue; - fds[count] = (struct pollfd){c->fd, (short)(POLLIN | (c->out_at < c->out.used ? POLLOUT : 0)), 0}; - slot[count++] = i; - } - int timeout = ticked ? 0 : screen->ticks ? 33 : reload ? 200 : 1000; - if (due >= 0 && due < timeout) timeout = (int)due; - int ready = poll(fds, count, timeout); - if (ready < 0 && errno != EINTR) break; - bool changed = ticked; - app->now = vega_now(); /* what the events of this round see */ - if (ready == 0 && screen->ticks) changed = pure_app_frame(app) == PURE_OK; - if (reload && reload()) { /* a new module: every page draws it */ - changed = true; - vega_timers_start(&timers, app->module, app->now); - } - if (fds[0].revents & POLLIN) - for (int fd; (fd = accept4(listener, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC)) >= 0;) { - unsigned free_slot = CLIENTS; - for (unsigned i = 0; i < CLIENTS && free_slot == CLIENTS; ++i) if (clients[i].fd < 0) free_slot = i; - if (free_slot == CLIENTS) { close(fd); continue; } - clients[free_slot] = (Client){.fd = fd}; - } - for (unsigned k = 1; k < count; ++k) { - Client *c = &clients[slot[k]]; - if (fds[k].revents & (POLLIN | POLLHUP | POLLERR)) { - ssize_t n = read(c->fd, c->in + c->in_used, sizeof(c->in) - c->in_used); - if (n > 0) { - c->in_used += (size_t)n; - changed = serve(c, app, screen) || changed; - } else if (n == 0 || (errno != EAGAIN && errno != EINTR)) { drop(c); continue; } - } - } - /* Every open page hears of a change once. */ - if (changed) - for (unsigned i = 0; i < CLIENTS; ++i) - if (clients[i].fd >= 0 && clients[i].events) - putf(&clients[i].out, "data: %llu\n\n", (unsigned long long)app->events); - for (unsigned i = 0; i < CLIENTS; ++i) { - Client *c = &clients[i]; - if (c->fd < 0) continue; - while (c->out_at < c->out.used) { - ssize_t n = write(c->fd, c->out.bytes + c->out_at, c->out.used - c->out_at); - if (n <= 0) { - if (n < 0 && (errno == EAGAIN || errno == EINTR)) break; - c->closing = true; - c->out_at = c->out.used; - break; - } - c->out_at += (size_t)n; - } - if (c->out_at == c->out.used) c->out_at = c->out.used = 0; - if (c->closing && !c->out.used) drop(c); - } - } - for (unsigned i = 0; i < CLIENTS; ++i) if (clients[i].fd >= 0) drop(&clients[i]); - close(listener); - return 0; -} diff --git a/src/width.c b/src/width.c deleted file mode 100644 index 1e893f6..0000000 --- a/src/width.c +++ /dev/null @@ -1,70 +0,0 @@ -#include "vega.h" - -typedef struct { uint32_t first, last; } Range; - -/* Zero-width: combining marks, Hangul vowel and final jamo, joiners, - * variation selectors, format characters. */ -static const Range zero[] = { - {0x0300, 0x036F}, {0x0483, 0x0489}, {0x0591, 0x05BD}, {0x0610, 0x061A}, {0x064B, 0x065F}, {0x0E31, 0x0E31}, - {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E}, {0x1160, 0x11FF}, {0x1AB0, 0x1AFF}, {0x1DC0, 0x1DFF}, {0x200B, 0x200F}, - {0x202A, 0x202E}, {0x2060, 0x2064}, {0x20D0, 0x20FF}, {0x302A, 0x302F}, {0x3099, 0x309A}, {0xD7B0, 0xD7FF}, - {0xFE00, 0xFE0F}, {0xFE20, 0xFE2F}, {0xFEFF, 0xFEFF}, {0x1F3FB, 0x1F3FF}, {0xE0000, 0xE0FFF}, -}; - -/* East Asian Wide and Fullwidth, and emoji shown as pictures. */ -static const Range wide[] = { - {0x1100, 0x115F}, {0x231A, 0x231B}, {0x2329, 0x232A}, {0x23E9, 0x23EC}, {0x23F0, 0x23F0}, {0x23F3, 0x23F3}, - {0x25FD, 0x25FE}, {0x2614, 0x2615}, {0x2648, 0x2653}, {0x267F, 0x267F}, {0x2693, 0x2693}, {0x26A1, 0x26A1}, - {0x26AA, 0x26AB}, {0x26BD, 0x26BE}, {0x26C4, 0x26C5}, {0x26CE, 0x26CE}, {0x26D4, 0x26D4}, {0x26EA, 0x26EA}, - {0x26F2, 0x26F3}, {0x26F5, 0x26F5}, {0x26FA, 0x26FA}, {0x26FD, 0x26FD}, {0x2705, 0x2705}, {0x270A, 0x270B}, - {0x2728, 0x2728}, {0x274C, 0x274C}, {0x274E, 0x274E}, {0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797}, - {0x27B0, 0x27B0}, {0x27BF, 0x27BF}, {0x2B1B, 0x2B1C}, {0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x303E}, - {0x3041, 0x33FF}, {0x3400, 0x4DBF}, {0x4E00, 0x9FFF}, {0xA000, 0xA4CF}, {0xA960, 0xA97F}, {0xAC00, 0xD7A3}, - {0xF900, 0xFAFF}, {0xFE10, 0xFE19}, {0xFE30, 0xFE6F}, {0xFF00, 0xFF60}, {0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE4}, - {0x17000, 0x18CFF}, {0x1B000, 0x1B2FF}, {0x1F004, 0x1F004}, {0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, - {0x1F191, 0x1F19A}, {0x1F200, 0x1F251}, {0x1F300, 0x1F64F}, {0x1F680, 0x1F6FF}, {0x1F7E0, 0x1F7EB}, - {0x1F90C, 0x1F9FF}, {0x1FA70, 0x1FAFF}, {0x20000, 0x2FFFD}, {0x30000, 0x3FFFD}, -}; - -static bool in(const Range *ranges, size_t count, uint32_t codepoint) { - size_t low = 0, high = count; - while (low < high) { - size_t middle = (low + high) / 2; - if (codepoint < ranges[middle].first) high = middle; - else if (codepoint > ranges[middle].last) low = middle + 1; - else return true; - } - return false; -} - -unsigned vega_width(uint32_t codepoint) { - if (in(zero, sizeof(zero) / sizeof(*zero), codepoint)) return 0; - return in(wide, sizeof(wide) / sizeof(*wide), codepoint) ? 2 : 1; -} - -uint32_t vega_visible(uint32_t codepoint) { - if (codepoint < 0x20) return 0x2400 + codepoint; - if (codepoint == 0x7F) return 0x2421; - if (codepoint >= 0x80 && codepoint < 0xA0) return 0xFFFD; - return codepoint; -} - -size_t vega_decode(const char *text, size_t length, size_t at, uint32_t *codepoint) { - const unsigned char *p = (const unsigned char *)text + at; - size_t left = length - at, need = p[0] < 0x80 ? 1 : p[0] < 0xE0 ? 2 : p[0] < 0xF0 ? 3 : 4; - if (need > left) { *codepoint = 0xFFFD; return left; } - uint32_t value = need == 1 ? p[0] : need == 2 ? p[0] & 0x1Fu : need == 3 ? p[0] & 0x0Fu : p[0] & 0x07u; - for (size_t i = 1; i < need; ++i) value = value << 6 | (p[i] & 0x3Fu); - *codepoint = value; - return need; -} - -unsigned vega_text_width(const char *text, size_t length) { - unsigned width = 0; - for (size_t at = 0; at < length;) { - uint32_t codepoint; - at += vega_decode(text, length, at, &codepoint); - width += vega_width(vega_visible(codepoint)); - } - return width; -} |
