diff options
Diffstat (limited to 'src/draw.c')
| -rw-r--r-- | src/draw.c | 429 |
1 files changed, 429 insertions, 0 deletions
diff --git a/src/draw.c b/src/draw.c new file mode 100644 index 0000000..cf4e227 --- /dev/null +++ b/src/draw.c @@ -0,0 +1,429 @@ +#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); + 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) { + for (unsigned i = 0; i < screen->count; ++i) + if (screen->parts[i].kind != VEGA_FIELD) { say_focus(out, app, &screen->parts[i]); return; } + 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)); + if (!cells) return false; + free(grid->cells); + grid->cells = cells; + grid->width = width; + grid->height = height; + return true; +} + +void vega_grid_free(VegaGrid *grid) { + free(grid->cells); + *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, 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; + 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, 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; + 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); + } +} + +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}; + 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]; + if (focus < *first) *first = focus; + if (visible && focus >= *first + visible) *first = focus - visible + 1; + if (*first + visible > rows) *first = rows > visible ? rows - visible : 0; + if (part->kind == VEGA_LIST) draw_list(grid, app, part, *first, row + 1, area); + else draw_table(grid, app, part, *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; } + } +} |
