699 lines
24 KiB
C
699 lines
24 KiB
C
#include "../base/all.h"
|
|
#include "../string_chunk.h"
|
|
#include <stdlib.h>
|
|
#include <fcntl.h>
|
|
|
|
#define UTF8_MAX_WIDTH 4
|
|
#define ANSI_HP_RED (196)
|
|
#define ANSI_MP_BLUE (33)
|
|
#define ANSI_LIGHT_GREEN (82)
|
|
#define ANSI_MID_GREEN (70)
|
|
#define ANSI_DARK_GREEN (35)
|
|
#define ANSI_BLACK (16)
|
|
#define ANSI_WHITE (15)
|
|
#define ANSI_BROWN (130)
|
|
#define ANSI_DULL_RED (1)
|
|
#define ANSI_HIGHLIGHT_RED (9)
|
|
#define ANSI_DULL_GREEN (2)
|
|
#define ANSI_HIGHLIGHT_GREEN (10)
|
|
#define ANSI_DULL_YELLOW (3)
|
|
#define ANSI_HIGHLIGHT_YELLOW (11)
|
|
#define ANSI_DULL_BLUE (4)
|
|
#define ANSI_HIGHLIGHT_BLUE (12)
|
|
#define ANSI_DULL_GRAY (7)
|
|
#define ANSI_HIGHLIGHT_GRAY (16)
|
|
#define MAX_COMMAND_PALETTE_COMMANDS (1000)
|
|
|
|
///// TYPES
|
|
typedef struct Pixel {
|
|
u8 foreground;
|
|
u8 background;
|
|
u8 bytes[UTF8_MAX_WIDTH];
|
|
} Pixel;
|
|
|
|
typedef struct TuiState {
|
|
bool redraw;
|
|
ptr writeable_output_ansi_string;
|
|
Pixel* frame_buffer;
|
|
Pixel* back_buffer;
|
|
u64 buffer_len; // how many Pixels
|
|
Pos2 cursor;
|
|
Pos2 prev_cursor;
|
|
Dim2 screen_dimensions;
|
|
Dim2 prev_screen_dimensions;
|
|
} TuiState;
|
|
|
|
typedef struct RGB {
|
|
u8 r;
|
|
u8 g;
|
|
u8 b;
|
|
} RGB;
|
|
|
|
typedef struct CommandPaletteCommand {
|
|
u32 id;
|
|
ptr display_name;
|
|
ptr description;
|
|
ptr* tags;
|
|
} CommandPaletteCommand;
|
|
|
|
typedef struct CommandPaletteCommandList {
|
|
u32 length;
|
|
CommandPaletteCommand* items;
|
|
} CommandPaletteCommandList;
|
|
|
|
typedef struct StringSearchScore {
|
|
bool name_matched;
|
|
bool description_matched;
|
|
u16 tag_match_count;
|
|
u32 score;
|
|
u32 name_match_start;
|
|
u32 name_match_len;
|
|
u32 description_match_start;
|
|
u32 description_match_len;
|
|
} StringSearchScore;
|
|
|
|
///// Functions()
|
|
fn u32 rgbToNum(RGB rgb) {
|
|
return ((rgb.r<<16) | (rgb.g<<8) | rgb.b);
|
|
}
|
|
|
|
fn u8 rgbToAnsi(RGB color) {
|
|
u8 r = color.r / (255 / 5);
|
|
u8 g = color.g / (255 / 5);
|
|
u8 b = color.b / (255 / 5);
|
|
return 16 + (36*r) + (6*g) + b;
|
|
}
|
|
|
|
fn RGB rgbDarken(RGB color, f32 factor) {
|
|
RGB result = {
|
|
.r = ((f32)color.r) * factor,
|
|
.g = ((f32)color.g) * factor,
|
|
.b = ((f32)color.b) * factor,
|
|
};
|
|
return result;
|
|
}
|
|
|
|
fn bool rgbEq(RGB a, RGB b) {
|
|
return a.r == b.r && a.g == b.g && a.b == b.b;
|
|
}
|
|
|
|
fn bool isPixelEq(Pixel a, Pixel b) {
|
|
return a.background == b.background
|
|
&& a.foreground == b.foreground
|
|
&& a.bytes[0] == b.bytes[0]
|
|
&& a.bytes[1] == b.bytes[1]
|
|
&& a.bytes[2] == b.bytes[2]
|
|
&& a.bytes[3] == b.bytes[3]
|
|
;
|
|
}
|
|
|
|
fn TuiState tuiInit(Arena* a, u64 buffer_len) {
|
|
TuiState result = {
|
|
.redraw = false,
|
|
.writeable_output_ansi_string = arenaAlloc(a, MB(1)),
|
|
.buffer_len = buffer_len,
|
|
.back_buffer = arenaAllocArray(a, Pixel, buffer_len), // allocate biggest possible dimensions
|
|
.frame_buffer = arenaAllocArray(a, Pixel, buffer_len), // allocate biggest possible dimensions
|
|
};
|
|
MemoryZero(result.back_buffer, buffer_len * sizeof(Pixel));
|
|
MemoryZero(result.frame_buffer, buffer_len * sizeof(Pixel));
|
|
return result;
|
|
}
|
|
|
|
fn void copyStr(u8* bytes, str cstring) {
|
|
for (u32 i = 0; i < strlen(cstring); i++) {
|
|
bytes[i] = cstring[i];
|
|
}
|
|
}
|
|
|
|
fn void clearBox(TuiState* tui, Box box) {
|
|
for (u32 i = 0; i <= box.height; i++) {
|
|
for (u32 ii = 0; ii <= box.width; ii++) {
|
|
u32 pos = XYToPos(box.x+ii, box.y+i, tui->screen_dimensions.width);
|
|
tui->frame_buffer[pos].background = 0;
|
|
tui->frame_buffer[pos].foreground = 0;
|
|
tui->frame_buffer[pos].bytes[0] = ' ';
|
|
tui->frame_buffer[pos].bytes[1] = 0;
|
|
tui->frame_buffer[pos].bytes[2] = 0;
|
|
tui->frame_buffer[pos].bytes[3] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn void drawAnsiBox(Pixel* buf, Box box, Dim2 sd, bool bold) {
|
|
str items[] = {"┌","─","┐","│","└","┘"};
|
|
str b_items[] = {"┏","━","┓","┃","┗","┛"};
|
|
ptr* use = (ptr*)items;
|
|
if (bold) {
|
|
use = (ptr*)b_items;
|
|
}
|
|
u32 pos = XYToPos(box.x, box.y, sd.width);
|
|
|
|
// print the upper box border
|
|
copyStr(buf[pos].bytes, use[0]);
|
|
for (i32 i = 0; i < box.width; i++) {
|
|
copyStr(buf[pos+1+i].bytes, use[1]);
|
|
}
|
|
copyStr(buf[pos+1+box.width].bytes, use[2]);
|
|
|
|
// start printing the rows
|
|
for (i32 i = 0; i < box.height; i++) {
|
|
pos = XYToPos(box.x, (box.y+1+i), sd.width); // move cursor to beginning of the row
|
|
copyStr(buf[pos].bytes, use[3]);
|
|
copyStr(buf[pos+1+box.width].bytes, use[3]);
|
|
}
|
|
|
|
// print the bottom box border
|
|
pos = XYToPos(box.x, (box.y+1+box.height), sd.width);
|
|
copyStr(buf[pos].bytes, use[4]);
|
|
for (i32 i = 0; i < box.width; i++) {
|
|
copyStr(buf[pos+1+i].bytes, use[1]);
|
|
}
|
|
copyStr(buf[pos+1+box.width].bytes, use[5]);
|
|
}
|
|
|
|
fn void renderUtf8CodePoint(TuiState* tui, u16 x, u16 y, str text) {
|
|
u32 bufpos = XYToPos(x,y,tui->screen_dimensions.width);
|
|
Pixel* buf = tui->frame_buffer;
|
|
Utf8Character first_character_class = classifyUtf8Character(text[0]);
|
|
// if it's a single ASCII character
|
|
if (first_character_class == Utf8CharacterAscii && text[1] == '\0') {
|
|
buf[bufpos].bytes[0] = text[0];
|
|
// if it's a pair of ASCII characters
|
|
} else if (first_character_class == Utf8CharacterAscii && text[1] > 0 && text[2] == '\0') {
|
|
buf[bufpos].bytes[0] = text[0];
|
|
buf[bufpos+1].bytes[0] = text[1];
|
|
} else if (first_character_class == Utf8CharacterThreeByte) {
|
|
for (u32 k = 0; k < strlen(text)/3; k++) {
|
|
for (u32 l = 0; l < 3; l++) {
|
|
buf[bufpos+k].bytes[l] = text[l+(k*3)];
|
|
}
|
|
}
|
|
} else if (first_character_class == Utf8CharacterFourByte) {
|
|
for (u32 k = 0; k < strlen(text)/UTF8_MAX_WIDTH; k++) {
|
|
for (u32 l = 0; l < UTF8_MAX_WIDTH; l++) {
|
|
buf[bufpos+k].bytes[l] = text[l+(k*UTF8_MAX_WIDTH)];
|
|
}
|
|
}
|
|
// handle secondary bytes that didn't divide evenly
|
|
for (u32 k = 0; k < strlen(text)%UTF8_MAX_WIDTH; k++) {
|
|
buf[bufpos+1].bytes[k] = text[UTF8_MAX_WIDTH+k];
|
|
}
|
|
} else {
|
|
assert(false && "unhandled bullshit");
|
|
}
|
|
}
|
|
|
|
fn void renderUtf8CharToBuffer(Pixel* buf, u16 x, u16 y, str text, Dim2 screen_dimensions) {
|
|
assert(strlen(text) <= UTF8_MAX_WIDTH);
|
|
u32 pos = x + (screen_dimensions.width*y);
|
|
for (u32 i = 0; i < strlen(text); i++) {
|
|
buf[pos].bytes[i] = text[i];
|
|
}
|
|
}
|
|
|
|
fn void renderStrToBufferMaxWidth(Pixel* buf, u16 x, u16 y, str text, u16 width, Dim2 screen_dimensions) {
|
|
u32 pos = x + (screen_dimensions.width*y);
|
|
for (u32 i = 0; i < strlen(text); i++) {
|
|
buf[pos + (i % width)].background = 0;
|
|
buf[pos + (i % width)].foreground = 0;
|
|
buf[pos + (i % width)].bytes[0] = text[i];
|
|
if (i % width == (width-1)) { // on last char i inside width
|
|
pos += screen_dimensions.width; // move pos to next line
|
|
}
|
|
}
|
|
}
|
|
|
|
fn void renderStrToBufferMaxWidthWithoutChangingColor(Pixel* buf, u16 x, u16 y, str text, u16 width, Dim2 screen_dimensions) {
|
|
u32 pos = x + (screen_dimensions.width*y);
|
|
for (u32 i = 0; i < strlen(text); i++) {
|
|
buf[pos + (i % width)].bytes[0] = text[i];
|
|
if (i % width == (width-1)) { // on last char i inside width
|
|
pos += screen_dimensions.width; // move pos to next line
|
|
}
|
|
}
|
|
}
|
|
|
|
fn void renderStrToBuffer(Pixel* buf, u16 x, u16 y, str text, Dim2 screen_dimensions) {
|
|
u32 pos = x + (screen_dimensions.width*y);
|
|
for (u32 i = 0; i < strlen(text); i++) {
|
|
buf[pos+i].bytes[0] = text[i];
|
|
buf[pos+i].bytes[1] = 0;
|
|
buf[pos+i].bytes[2] = 0;
|
|
buf[pos+i].bytes[3] = 0;
|
|
}
|
|
}
|
|
|
|
fn void renderStringChunkList(TuiState* tui, StringChunkList* list, u16 x, u16 y) {
|
|
u32 pos = XYToPos(x, y, tui->screen_dimensions.width);
|
|
StringChunk* chunk = list->first;
|
|
for (u32 i = 0; i < list->total_size; i++) {
|
|
if (i > 0 && i % STRING_CHUNK_PAYLOAD_SIZE == 0) {
|
|
chunk = chunk->next;
|
|
}
|
|
tui->frame_buffer[pos+i].bytes[0] = *((char*)(chunk + 1) + (i%STRING_CHUNK_PAYLOAD_SIZE));
|
|
}
|
|
}
|
|
|
|
fn void colorizeBox(TuiState* tui, Box box, u8 background, u8 foreground, u8 byte) {
|
|
for (u32 i = 0; i < box.width; i++) {
|
|
for (u32 ii = 0; ii < box.height; ii++) {
|
|
u32 bufpos = XYToPos(box.x+i, box.y+ii, tui->screen_dimensions.width);
|
|
tui->frame_buffer[bufpos].background = background;
|
|
tui->frame_buffer[bufpos].foreground = foreground;
|
|
if (byte) {
|
|
tui->frame_buffer[bufpos].bytes[0] = byte;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn void colorizeRange(TuiState* tui, Range1u32 range, u8 background, u8 foreground) {
|
|
assert(range.max > range.min);
|
|
for (u32 i = 0; i < (range.max - range.min); i++) {
|
|
tui->frame_buffer[range.min+i].background = background;
|
|
tui->frame_buffer[range.min+i].foreground = foreground;
|
|
}
|
|
}
|
|
|
|
fn u32 sprintfAnsiMoveCursorTo(ptr output, u16 x, u16 y) {
|
|
return sprintf(output, "\x1b[%d;%df",y,x);
|
|
}
|
|
|
|
fn void printfBufferAndSwap(TuiState* tui) {
|
|
Pixel* old = tui->back_buffer;
|
|
Pixel* next = tui->frame_buffer;
|
|
u32 length = tui->screen_dimensions.height * tui->screen_dimensions.width;
|
|
bool screen_dimensions_changed = tui->screen_dimensions.height != tui->prev_screen_dimensions.height
|
|
|| tui->screen_dimensions.width != tui->prev_screen_dimensions.width;
|
|
bool should_redraw_whole_screen = screen_dimensions_changed || tui->redraw;
|
|
|
|
// "quick" exit this fn if old == next
|
|
if (!should_redraw_whole_screen) {
|
|
bool old_equals_new = true;
|
|
for (u32 i = 0; i < length; i++) {
|
|
if (old[i].background != next[i].background
|
|
|| old[i].foreground != next[i].foreground
|
|
|| old[i].bytes[0] != next[i].bytes[0]
|
|
|| old[i].bytes[1] != next[i].bytes[1]
|
|
|| old[i].bytes[2] != next[i].bytes[2]
|
|
|| old[i].bytes[3] != next[i].bytes[3]) {
|
|
old_equals_new = false;
|
|
break;
|
|
}
|
|
}
|
|
if (old_equals_new
|
|
&& tui->prev_cursor.x == tui->cursor.x
|
|
&& tui->prev_cursor.y == tui->cursor.y
|
|
) {
|
|
length += 0; // debugging helper line
|
|
return; // skip all the write() and sprintf() calls, since the frames are the same.
|
|
}
|
|
}
|
|
|
|
ptr output = tui->writeable_output_ansi_string;
|
|
u8 bg = 0;
|
|
u8 fg = 0;
|
|
u16 x = 1;
|
|
u16 y = 1;
|
|
u16 last_x = 0;
|
|
u16 last_y = 0;
|
|
str RESET_STYLES = "\033[0m";
|
|
str CLEAR_SCREEN = "\033[2J";
|
|
|
|
if (should_redraw_whole_screen) {
|
|
output += sprintf(output, "\033[0m\033[2J");
|
|
output += sprintfAnsiMoveCursorTo(output, x, y);
|
|
bool printed_last = false;
|
|
for (u32 i = 0; i < length; i++) {
|
|
x = (i % tui->screen_dimensions.width) + 1;
|
|
y = (i / tui->screen_dimensions.width) + 1;
|
|
if (next[i].bytes[0] != 0) {
|
|
if (printed_last == false) {
|
|
output += sprintfAnsiMoveCursorTo(output, x, y);
|
|
}
|
|
char bytes[5] = {0}; // do this nonsense to ensure null-terminated characters
|
|
bytes[0] = next[i].bytes[0];
|
|
bytes[1] = next[i].bytes[1];
|
|
bytes[2] = next[i].bytes[2];
|
|
bytes[3] = next[i].bytes[3];
|
|
|
|
if (next[i].background != bg || next[i].foreground != fg) {
|
|
bg = next[i].background;
|
|
fg = next[i].foreground;
|
|
if (bg == 0 && fg == 0) {
|
|
output += sprintf(output, "%s%s", RESET_STYLES, bytes);
|
|
} else if (bg != 0 && fg == 0) {
|
|
output += sprintf(output, "\033[48;5;%dm%s", bg, bytes);
|
|
} else if (bg == 0 && fg != 0) {
|
|
output += sprintf(output, "\033[38;5;%dm%s", fg, bytes);
|
|
} else {
|
|
output += sprintf(output, "\033[48;5;%d;38;5;%dm%s", bg, fg, bytes);
|
|
}
|
|
} else {
|
|
output += sprintf(output, "%s", bytes);
|
|
}
|
|
printed_last = true;
|
|
} else {
|
|
printed_last = false;
|
|
}
|
|
}
|
|
} else {
|
|
// clearing pass, to overwrite things that were there on the last frame, but are no longer present
|
|
// we do this before the "rendering" pass so that multi-space characters (emojis) are easier to deal with
|
|
output += sprintf(output, "%s", RESET_STYLES);
|
|
output += sprintfAnsiMoveCursorTo(output, x, y);
|
|
for (u32 i = 0; i < length; i++) {
|
|
x = (i % tui->screen_dimensions.width) + 1;
|
|
y = (i / tui->screen_dimensions.width) + 1;
|
|
bool needs_clearing = (next[i].bytes[0] == 0 && old[i].bytes[0] != 0)
|
|
|| (next[i].background == 0 && old[i].background != 0)
|
|
|| (next[i].foreground == 0 && old[i].foreground != 0);
|
|
if (needs_clearing) {
|
|
bool last_printed_pos_is_adjacent_to_current_x_y = last_x+1 == x && last_y == y;
|
|
if (!last_printed_pos_is_adjacent_to_current_x_y) {
|
|
output += sprintfAnsiMoveCursorTo(output, x, y);
|
|
}
|
|
output += sprintf(output, " ");
|
|
last_x = x;
|
|
last_y = y;
|
|
}
|
|
}
|
|
|
|
// rendering pass
|
|
last_x = 0;
|
|
last_y = 0;
|
|
for (u32 i = 0; i < length; i++) {
|
|
x = (i % tui->screen_dimensions.width) + 1;
|
|
y = (i / tui->screen_dimensions.width) + 1;
|
|
if (!isPixelEq(old[i], next[i])) {
|
|
if (next[i].bytes[0] != 0) {
|
|
bool last_printed_pos_is_adjacent_to_current_x_y = last_x+1 == x && last_y == y;
|
|
if (!last_printed_pos_is_adjacent_to_current_x_y) {
|
|
output += sprintfAnsiMoveCursorTo(output, x, y);
|
|
}
|
|
|
|
u8 bytes[5] = {0}; // do this nonsense to ensure null-terminated characters
|
|
bytes[0] = next[i].bytes[0];
|
|
bytes[1] = next[i].bytes[1];
|
|
bytes[2] = next[i].bytes[2];
|
|
bytes[3] = next[i].bytes[3];
|
|
|
|
if (next[i].background != bg || next[i].foreground != fg) {
|
|
bg = next[i].background;
|
|
fg = next[i].foreground;
|
|
if (bg == 0 && fg == 0) {
|
|
output += sprintf(output, "%s%s", RESET_STYLES, bytes);
|
|
} else if (bg != 0 && fg == 0) {
|
|
output += sprintf(output, "\033[48;5;%dm%s", bg, bytes);
|
|
} else if (bg == 0 && fg != 0) {
|
|
output += sprintf(output, "\033[38;5;%dm%s", fg, bytes);
|
|
} else {
|
|
output += sprintf(output, "\033[48;5;%d;38;5;%dm%s", bg, fg, bytes);
|
|
}
|
|
} else {
|
|
output += sprintf(output, "%s", bytes);
|
|
}
|
|
|
|
last_x = x;
|
|
last_y = y;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
output += sprintfAnsiMoveCursorTo(output, tui->cursor.x+1, tui->cursor.y+1); // re-position the cursor according to what the rendering logic set it to
|
|
|
|
// finally write our whole string to the terminal
|
|
i64 count = output - tui->writeable_output_ansi_string;
|
|
osBlitToTerminal(tui->writeable_output_ansi_string, count);
|
|
|
|
// swap our buffers
|
|
Pixel* tmp = tui->back_buffer;
|
|
tui->back_buffer = tui->frame_buffer;
|
|
tui->frame_buffer = tmp;
|
|
|
|
// reset the `redraw` flag
|
|
tui->redraw = false;
|
|
}
|
|
|
|
fn u32 matchCommandPaletteCommands(String current_search, CommandPaletteCommandList commands, u32 menu_index, u32* scores, StringSearchScore* score_details) {
|
|
// returns the `commands` id that matches the `menu_index`
|
|
for (u32 i = 0; i < commands.length; i++) {
|
|
CommandPaletteCommand* cmd = &commands.items[i];
|
|
bool name_matches = false;
|
|
for (u32 j = 0; j < strlen(cmd->display_name); j++) {
|
|
if (current_search.length > 0 && lowerAscii(cmd->display_name[j]) == lowerAscii(current_search.bytes[0])) {
|
|
for (u32 k = 0; k < current_search.length && k+j < strlen(cmd->display_name); k++) {
|
|
if (lowerAscii(current_search.bytes[k]) != lowerAscii(cmd->display_name[j+k])) {
|
|
break;
|
|
}
|
|
if (k == current_search.length - 1) {
|
|
name_matches = true;
|
|
score_details[i].name_match_start = j;
|
|
score_details[i].name_match_len = current_search.length;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
bool description_matches = false;
|
|
u32 tag_match_count = 0;
|
|
scores[i] = (name_matches * 100 * MAX_COMMAND_PALETTE_COMMANDS) +
|
|
(description_matches * 10 * MAX_COMMAND_PALETTE_COMMANDS) +
|
|
(tag_match_count * MAX_COMMAND_PALETTE_COMMANDS) +
|
|
cmd->id;
|
|
score_details[i].score = scores[i];
|
|
score_details[i].name_matched = name_matches;
|
|
score_details[i].description_matched = description_matches;
|
|
score_details[i].tag_match_count = tag_match_count;
|
|
}
|
|
u32Quicksort(scores, 0, commands.length - 1);
|
|
u32ReverseArray(scores, commands.length);
|
|
return (scores[menu_index] % MAX_COMMAND_PALETTE_COMMANDS);
|
|
}
|
|
|
|
fn Pos2 renderCommandPalette(TuiState* tui, String current_search, CommandPaletteCommandList commands, u32 menu_index) {
|
|
// returns the cursor position as Pos2
|
|
|
|
ScratchMem scratch = scratchGet();
|
|
Dim2 sd = tui->screen_dimensions;
|
|
Pos2 result = { 0 };
|
|
|
|
// draw the outline
|
|
Box outline = {
|
|
.width = sd.width / 2,
|
|
.height = sd.height / 2,
|
|
};
|
|
outline.x = outline.width / 2;
|
|
outline.y = outline.height / 2;
|
|
drawAnsiBox(tui->frame_buffer, outline, sd, true);
|
|
|
|
// draw the "search bar"
|
|
result.x = outline.x + 1 + current_search.length;
|
|
result.y = outline.y + 1;
|
|
renderStrToBufferMaxWidth(tui->frame_buffer, outline.x+1, outline.y+1, current_search.bytes, outline.width - 2, sd);
|
|
for (u32 i = 1; i < outline.width-1; i++) {
|
|
u32 pos = XYToPos(outline.x+1, outline.y+2, sd.width);
|
|
copyStr(tui->frame_buffer[pos].bytes, "━");
|
|
}
|
|
|
|
// sort the command options
|
|
u32* scores = arenaAllocArray(&scratch.arena, u32, commands.length);
|
|
StringSearchScore* score_details = arenaAllocArray(&scratch.arena, StringSearchScore, commands.length);
|
|
matchCommandPaletteCommands(current_search, commands, menu_index, scores, score_details);
|
|
|
|
// draw the command options
|
|
u32 x = outline.x + 1;
|
|
u32 y = outline.y + 3;
|
|
for (u32 i = 0; (y-outline.y) < (outline.height-1) && i < commands.length; i++, y+=2) {
|
|
if (i == menu_index) {
|
|
for (u32 ii = 0; ii < outline.width-1; ii++) {
|
|
u32 pos = XYToPos(x+ii, y, tui->screen_dimensions.width);
|
|
tui->frame_buffer[pos].bytes[0] = ' ';
|
|
tui->frame_buffer[pos].foreground = ANSI_BLACK;
|
|
tui->frame_buffer[pos].background = ANSI_WHITE;
|
|
pos = XYToPos(x+ii, y+1, tui->screen_dimensions.width);
|
|
tui->frame_buffer[pos].bytes[0] = ' ';
|
|
tui->frame_buffer[pos].foreground = ANSI_BLACK;
|
|
tui->frame_buffer[pos].background = ANSI_WHITE;
|
|
}
|
|
}
|
|
u32 id = scores[i] % MAX_COMMAND_PALETTE_COMMANDS;
|
|
StringSearchScore score = score_details[id];
|
|
CommandPaletteCommand* cmd = NULL;
|
|
for (u32 j = 0; j < commands.length; j++) {
|
|
if (commands.items[j].id == id) {
|
|
cmd = &commands.items[j];
|
|
break;
|
|
}
|
|
}
|
|
assert(cmd != NULL);
|
|
if (score.name_matched) {
|
|
u32 end_idx = score.name_match_len + score.name_match_start;
|
|
for (u32 ii = 0; ii < strlen(cmd->display_name); ii++) {
|
|
u32 pos = XYToPos(x+ii, y, tui->screen_dimensions.width);
|
|
if (ii >= score.name_match_start && ii < end_idx) {
|
|
if (i == menu_index) {
|
|
tui->frame_buffer[pos].foreground = ANSI_DULL_RED;
|
|
} else {
|
|
tui->frame_buffer[pos].foreground = ANSI_HIGHLIGHT_YELLOW;
|
|
tui->frame_buffer[pos].background = 0;
|
|
}
|
|
}
|
|
tui->frame_buffer[pos].bytes[0] = cmd->display_name[ii];
|
|
}
|
|
} else {
|
|
renderStrToBufferMaxWidthWithoutChangingColor(tui->frame_buffer, x, y, cmd->display_name, outline.width - 2, sd);
|
|
}
|
|
renderStrToBufferMaxWidthWithoutChangingColor(tui->frame_buffer, x, y+1, cmd->description, outline.width - 2, sd);
|
|
}
|
|
|
|
scratchReturn(&scratch);
|
|
return result;
|
|
}
|
|
|
|
fn void renderTable(TuiState* tui, TableDrawInfo t, u32 selected_index, FieldDescriptor fields[], u32 table_col_pad, void* data, u32 sizeof_data_item) {
|
|
char tmp_buffer[64] = {0};
|
|
u32 col_x_pos = 0;
|
|
// render headers
|
|
for (u32 i = 0; i < t.cols; col_x_pos += fields[i++].width+table_col_pad) {
|
|
renderStrToBuffer(tui->frame_buffer, t.x_offset+col_x_pos, t.y_offset, fields[i].name, tui->screen_dimensions);
|
|
for (u32 ii = 0; ii < fields[i].width; ii++) {
|
|
renderUtf8CharToBuffer(tui->frame_buffer, t.x_offset+col_x_pos+ii, t.y_offset+1, "━", tui->screen_dimensions);
|
|
}
|
|
}
|
|
t.y_offset += 2;
|
|
// render the rows
|
|
for (u32 i = 0; i < t.rows; t.y_offset++, i++) {
|
|
col_x_pos = 0;
|
|
ptr item_offset = data + (sizeof_data_item * i);
|
|
// render the columns
|
|
for (u32 ii = 0; ii < t.cols; col_x_pos += fields[ii].width+table_col_pad, ii++) {
|
|
if (i == selected_index) {
|
|
tui->cursor.x = t.x_offset;
|
|
tui->cursor.y = t.y_offset;
|
|
for (u32 iii = 0; iii < fields[ii].width+table_col_pad; iii++) {
|
|
u32 pos = XYToPos(t.x_offset+col_x_pos+iii, t.y_offset, tui->screen_dimensions.width);
|
|
tui->frame_buffer[pos].background = ANSI_WHITE;
|
|
tui->frame_buffer[pos].foreground = ANSI_BLACK;
|
|
tui->frame_buffer[pos].bytes[0] = ' ';
|
|
}
|
|
}
|
|
FieldDescriptor details = fields[ii];
|
|
void *field_ptr = item_offset + details.offset;
|
|
MemoryZero(tmp_buffer, 64);
|
|
switch (details.type) {
|
|
case FieldTypeEnum: {
|
|
renderStrToBuffer(tui->frame_buffer, t.x_offset+col_x_pos, t.y_offset, details.enum_vals[*(u8 *)field_ptr], tui->screen_dimensions);
|
|
} break;
|
|
case FieldTypeU8: {
|
|
sprintf(tmp_buffer, "%d", *(u8 *)field_ptr);
|
|
renderStrToBuffer(tui->frame_buffer, t.x_offset+col_x_pos, t.y_offset, tmp_buffer, tui->screen_dimensions);
|
|
} break;
|
|
case FieldTypeU16: {
|
|
sprintf(tmp_buffer, "%d", *(u16 *)field_ptr);
|
|
renderStrToBuffer(tui->frame_buffer, t.x_offset+col_x_pos, t.y_offset, tmp_buffer, tui->screen_dimensions);
|
|
} break;
|
|
case FieldTypeU32: {
|
|
sprintf(tmp_buffer, "%d", *(u32 *)field_ptr);
|
|
renderStrToBuffer(tui->frame_buffer, t.x_offset+col_x_pos, t.y_offset, tmp_buffer, tui->screen_dimensions);
|
|
} break;
|
|
case FieldTypeFloat: {
|
|
sprintf(tmp_buffer, "%f", *(f32 *)field_ptr);
|
|
renderStrToBuffer(tui->frame_buffer, t.x_offset+col_x_pos, t.y_offset, tmp_buffer, tui->screen_dimensions);
|
|
} break;
|
|
case FieldTypeString: {
|
|
renderStrToBuffer(tui->frame_buffer, t.x_offset+col_x_pos, t.y_offset, *(ptr *)field_ptr, tui->screen_dimensions);
|
|
} break;
|
|
case FieldType_Count:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn void renderChoiceMenu(TuiState* tui, u16 x, u16 y, ptr options[], u32 len, bool choosable, u32 selected_index, u8* colors) {
|
|
for (u32 i = 0; i < len; i++) {
|
|
u32 pos = x + (tui->screen_dimensions.width*(y+i));
|
|
if (choosable && selected_index == i) {
|
|
tui->cursor.x = x;
|
|
tui->cursor.y = y+i;
|
|
}
|
|
u8 bytes[80] = {0};
|
|
sprintf((char*)bytes, "- %d. ", i+1);
|
|
u32 offset = strlen((char*)bytes);
|
|
u32 bytes_remaining = (80 - offset);
|
|
for (
|
|
u32 j = 0;
|
|
j < strlen(options[i]) && j < bytes_remaining;
|
|
j++
|
|
) {
|
|
bytes[offset+j] = options[i][j];
|
|
}
|
|
// write our `bytes` buffer into the Pixel* buf
|
|
for (u32 j = 0; j < 80; j++) {
|
|
tui->frame_buffer[pos+j].bytes[0] = bytes[j];
|
|
if (choosable && selected_index == i) {
|
|
tui->frame_buffer[pos+j].foreground = ANSI_BLACK;
|
|
tui->frame_buffer[pos+j].background = ANSI_WHITE;
|
|
} else {
|
|
tui->frame_buffer[pos+j].foreground = colors == NULL ? ANSI_WHITE : colors[i];
|
|
tui->frame_buffer[pos+j].background = ANSI_BLACK;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn void infiniteUILoop(
|
|
u32 max_screen_width,
|
|
u32 max_screen_height,
|
|
u64 goal_input_loop_us,
|
|
void* state,
|
|
// updateAndRender should return a bool `should_quit`
|
|
bool (*updateAndRender)(TuiState* tui, void* state, u8* input_buffer, u64 loop_count)
|
|
) {
|
|
bool should_quit = false;
|
|
Arena permanent_arena = {0};
|
|
arenaInit(&permanent_arena);
|
|
// set up the TUI incantations
|
|
TermIOs old_terminal_attributes = osStartTUI(false);
|
|
TuiState tui = tuiInit(&permanent_arena, max_screen_width*max_screen_height);
|
|
tui.screen_dimensions = osGetTerminalDimensions();
|
|
|
|
// ui loop (read input, simulate next frame, render)
|
|
u8 input_buffer[5] = {0};
|
|
u64 loop_start;
|
|
u64 loop_count = 0;
|
|
while (!should_quit) {
|
|
loop_count += 1;
|
|
loop_start = osTimeMicrosecondsNow();
|
|
|
|
// both reads local input from the keyboard AND from the network
|
|
osReadConsoleInput(input_buffer, 4);
|
|
|
|
// prep rendering
|
|
MemoryZero(tui.frame_buffer, tui.buffer_len * sizeof(Pixel));
|
|
tui.prev_screen_dimensions = tui.screen_dimensions;
|
|
if (loop_count % 17 == 0) { // every 17 frames, update our terminal_dimensions
|
|
tui.screen_dimensions = osGetTerminalDimensions();
|
|
}
|
|
tui.prev_cursor = tui.cursor;// save last frame's cursor
|
|
|
|
// operate on input + render new tui.frame_buffer
|
|
should_quit = updateAndRender(&tui, state, input_buffer, loop_count);
|
|
|
|
printfBufferAndSwap(&tui);
|
|
|
|
// loop timing
|
|
u32 loop_duration = osTimeMicrosecondsNow() - loop_start;
|
|
i32 remaining_time = goal_input_loop_us - loop_duration;
|
|
if (remaining_time > 0) {
|
|
osSleepMicroseconds(remaining_time);
|
|
}
|
|
}
|
|
|
|
// cleanup terminal TUI incantations
|
|
osEndTUI(old_terminal_attributes);
|
|
}
|
|
|