diff options
author | Raymaekers Luca <raymaekers.luca@gmail.com> | 2024-12-07 19:17:33 +0100 |
---|---|---|
committer | Raymaekers Luca <luca@spacehb.net> | 2025-01-07 18:51:21 +0100 |
commit | 0574f5a7c5159a2ae1d7d2182cec982509947db9 (patch) | |
tree | 2a9678fd39bcf8ead03c7ee979caf668f2f17892 | |
parent | 4aec2e1331650cabb966bbf24ed83f76cbbe9912 (diff) |
checkpoint
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | archived/input_box.c | 70 | ||||
-rw-r--r-- | archived/network_compression.c | 154 | ||||
-rw-r--r-- | archived/ui_checkmark.c | 87 | ||||
-rw-r--r-- | archived/ui_meter.c | 108 | ||||
-rw-r--r-- | archived/ui_selection.c | 71 | ||||
-rw-r--r-- | archived/wrap.c | 2 | ||||
-rw-r--r-- | arena.h | 53 | ||||
-rw-r--r-- | box.c | 456 | ||||
-rw-r--r-- | box_test.c | 27 | ||||
-rwxr-xr-x | build.sh | 9 | ||||
-rw-r--r-- | chatty.c | 138 | ||||
-rw-r--r-- | chatty.h | 73 | ||||
-rw-r--r-- | chatty2.c | 90 | ||||
-rw-r--r-- | gdb_scripts | 12 | ||||
-rw-r--r-- | protocol.h | 9 | ||||
-rw-r--r-- | scroll.md | 63 | ||||
-rw-r--r-- | scrollandwrapped.c | 225 | ||||
-rw-r--r-- | server.c | 93 | ||||
-rw-r--r-- | test.h | 217 | ||||
-rw-r--r-- | tests.c | 37 | ||||
-rw-r--r-- | types.h | 15 | ||||
-rw-r--r-- | ui.h | 730 | ||||
-rw-r--r-- | ui_box.h | 472 | ||||
-rw-r--r-- | ui_wrapped.c | 82 | ||||
-rw-r--r-- | utf8toASCII.c | 163 |
27 files changed, 2452 insertions, 1021 deletions
@@ -1,6 +1,4 @@ -chatty -send -server +build/* external/keyboard _id @@ -9,12 +9,13 @@ The idea is the following: - authentication ## client -- [ ] bug: when connecting two clients of the same account -- [ ] bug: wrapping does not work and displays nothing if there is no screen space -- [ ] bug: reconnect does not work when server does not know id -- [ ] convert tabs to spaces -- [ ] bug: when using lots of markup characters -- [ ] newline support +- [ ] BUG: text is not appearing after typing +- [ ] BUG: when connecting two clients of the same account +- [ ] BUG: wrapping does not work and displays nothing if there is no screen space +- [ ] BUG: reconnect does not work when server does not know id +- [ ] TODO: Convert tabs to spaces +- [ ] BUG: when using lots of markup characters +- [ ] TODO: Newline support - [ ] resizable box ## server diff --git a/archived/input_box.c b/archived/input_box.c new file mode 100644 index 0000000..466c03d --- /dev/null +++ b/archived/input_box.c @@ -0,0 +1,70 @@ +#define Assert(expr) if (!(expr)) { \ + tb_shutdown(); \ + raise(SIGTRAP); \ +} + +#define TB_IMPL +#include "termbox2.h" +#undef TB_IMPL + +#define TEXTBOX_MAX_INPUT 255 + +#define UI_IMPL +#include "ui.h" + +#define ARENA_IMPL +#include "arena.h" + +#include <locale.h> + +int +main(void) +{ + struct tb_event ev = {0}; + + u32 InputLen = 0; + wchar_t Input[TEXTBOX_MAX_INPUT] = {0}; + rect TextBox = {0, 0, 32, 5}; + rect TextR = { + 2, 1, + TextBox.W - 2*TEXTBOX_PADDING_X - 2*TEXTBOX_BORDER_WIDTH, + TextBox.H - 2*TEXTBOX_BORDER_WIDTH + }; + // Used for scrolling the text. Text before TextOffset will not be printed. + u32 TextOffset = 0; + // Position in input based on cursor position. + u32 InputPos = 0; + + Assert(setlocale(LC_ALL, "")); + tb_init(); + bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); + global.cursor_x = TextR.X; + global.cursor_y = TextR.Y; + + DrawBox(TextBox, 0); + + while (ev.key != TB_KEY_CTRL_C) + { + DrawTextBoxWrapped(TextR, Input + TextOffset, InputLen - TextOffset); + + InputPos = TextOffset + (global.cursor_x - TextR.X) + (global.cursor_y - TextR.Y) * TextR.W; + Assert(InputPos <= InputLen); + + tb_present(); + tb_poll_event(&ev); + + u32 Ret = TextBoxKeypress(ev, TextR, Input, &InputLen, InputPos, &TextOffset); + + u32 ShouldInsert = (!Ret) && (ev.ch && InputLen < TEXTBOX_MAX_INPUT); + // Insert new character in Input at InputPos + if (ShouldInsert) + { + TextBoxInsert(Input, InputPos, InputLen++, ev.ch); + TextBoxScrollRight(TextR, &TextOffset); + } + } + + + tb_shutdown(); + return 0; +} diff --git a/archived/network_compression.c b/archived/network_compression.c new file mode 100644 index 0000000..eef9e3c --- /dev/null +++ b/archived/network_compression.c @@ -0,0 +1,154 @@ +#include <strings.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <wchar.h> +#include <fcntl.h> +#include <assert.h> +#include <stddef.h> +#include <stdint.h> + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int8_t s8; +typedef int16_t s16; +typedef int32_t s32; +typedef int64_t s64; + +size_t +GetMaximumCompressedOutputSize(size_t FileSize) +{ + // TODO: figure out equation + return FileSize*2 + 256; +} + +u32 +RLECompress(size_t InSize, u8* In, size_t MaxOutSize, u8* OutBase) +{ + u8* Out = OutBase; + +#define MAX_LITERAL_COUNT 255 +#define MAX_RUN_COUNT 255 + u32 LiteralCount = 0; + u8 Literals[MAX_LITERAL_COUNT] = {0}; + + u8 *InEnd = In + InSize; + while(In < InEnd) + { + u8 StartingValue = In[0]; + size_t Run = 1; // first one is the character itself + while((Run < (InEnd - In)) && + (Run < MAX_RUN_COUNT) && + (In[Run] == StartingValue)) + { + ++Run; + } + + if ((Run > 1) || + (LiteralCount == MAX_LITERAL_COUNT)) // stop doing runs when there is no + // space left in the buffer. + { + // Encode a literal/run pair + u8 LiteralCount8 = (u8)LiteralCount; + assert(LiteralCount8 == LiteralCount); + *Out++ = LiteralCount8; + + for(u32 LiteralIndex = 0; + LiteralIndex < LiteralCount; + ++LiteralIndex) + { + *Out++ = Literals[LiteralIndex]; + } + LiteralCount = 0; + + u8 Run8 = (u8)Run; + assert(Run8 == Run); + *Out++ = Run8; + + *Out++ = StartingValue; + + In += Run; + } + else + { + // Buffer literals, you have to because we are encoding them in pairs + Literals[LiteralCount++] = StartingValue; + ++In; + } + + } +#undef MAX_LITERAL_COUNT +#undef MAX_RUN_COUNT + + assert(In == InEnd); + + size_t OutSize = Out - OutBase; + assert(OutSize <= MaxOutSize); + + return OutSize; +} + +void +RLEDecompress(size_t InSize, u8* In, size_t OutSize, u8* Out) +{ + u8 *InEnd = In + InSize; + while(In < InEnd) + { + // TODO: I think Casey made a mistake and this should be an u8 + u8 LiteralCount = *In++; + while(LiteralCount--) + { + *Out++ = *In++; + } + + // Alternate to Run + u8 RepCount = *In++; + u8 RepValue = *In++; + while(RepCount--) + { + *Out++ = RepValue; + } + } + + assert(In == InEnd); +} + +int main(int Argc, char* Argv[]) { + + if (Argc < 2) + { + fprintf(stderr, "Usage: %s [compress|decompress]\n", Argv[0]); + return 1; + } + + char* Command = Argv[1]; + + if (!strcmp(Command, "compress")) + { + u32* Message = (u32*)L"abcabcbbbbbbb"; + size_t MessageSize = wcslen((const wchar_t*)Message) * 4; + + size_t OutBufferSize = GetMaximumCompressedOutputSize(MessageSize); + u8 OutBuffer[OutBufferSize + 8]; + bzero(OutBuffer, OutBufferSize + 8); + *(u32*)OutBuffer = MessageSize; + + size_t CompressedSize = RLECompress(MessageSize, (u8*)Message, OutBufferSize, OutBuffer + 8); + + s32 OutFile = open("test.compressed", O_WRONLY | O_CREAT, 0600); + + write(OutFile, OutBuffer, CompressedSize); + + fprintf(stdout, "%lu -> %lu bytes\n", MessageSize, CompressedSize); + } + else if (!strcmp(Command, "decompress")) + { + fprintf(stderr, "Not implemented yet.\n"); + } + else + fprintf(stderr, "Unknown command: '%s'\n", Command); + + return 0; +} diff --git a/archived/ui_checkmark.c b/archived/ui_checkmark.c new file mode 100644 index 0000000..df7e507 --- /dev/null +++ b/archived/ui_checkmark.c @@ -0,0 +1,87 @@ +#define TB_IMPL +#include "../chatty/external/termbox2.h" + +int +main(void) +{ + struct tb_event ev = {0}; + + typedef struct { + int X, Y; + int Checked; + } Checkmark; + +#define NUM_CHECKMARKS 4 + int Y = 0; + Checkmark Marks[NUM_CHECKMARKS] = { + {0, Y++, 0}, + {0, Y++, 0}, + {0, Y++, 1}, + {0, Y++, 0} + }; + Y++; + + int Selected = 0; + + tb_init(); + + int Quit = 0; + while (!Quit) + { + tb_clear(); + + for (int CheckmarkIndex = 0; + CheckmarkIndex < NUM_CHECKMARKS; + CheckmarkIndex++) + { + Checkmark Mark = Marks[CheckmarkIndex]; + if (Mark.Checked) + { + tb_printf(Mark.X, Mark.Y, 0, 0, "[x]"); + } + else + { + tb_printf(Mark.X, Mark.Y, 0, 0, "[ ]"); + } + } + Checkmark Mark = Marks[Selected]; + if (Mark.Checked) + { + tb_set_cell(Mark.X + 1, Mark.Y, L'x', TB_UNDERLINE, 0); + } + else + { + tb_set_cell(Mark.X + 1, Mark.Y, L' ', TB_UNDERLINE, 0); + } + + int BaseY = Y; + tb_printf(0, BaseY, TB_BOLD, 0, "j"); tb_printf(2, BaseY++, 0, 0, "next"); + tb_printf(0, BaseY, TB_BOLD, 0, "k"); tb_printf(2, BaseY++, 0, 0, "previous"); + tb_printf(0, BaseY, TB_BOLD, 0, "c"); tb_printf(2, BaseY++, 0, 0, "toggle"); + tb_printf(0, BaseY, TB_BOLD, 0, "q"); tb_printf(2, BaseY++, 0, 0, "quit"); + + tb_present(); + + tb_poll_event(&ev); + if (ev.ch == 'q') + { + Quit = 1; + } + else if (ev.ch == 'j') + { + if (Selected == NUM_CHECKMARKS - 1) Selected = 0; + else Selected++; + } + else if (ev.ch == 'k') + { + if (Selected) Selected--; + else Selected = NUM_CHECKMARKS - 1; + } + else if (ev.ch == 'c') + { + Marks[Selected].Checked = !Marks[Selected].Checked; + } + } + + tb_shutdown(); +} diff --git a/archived/ui_meter.c b/archived/ui_meter.c new file mode 100644 index 0000000..8636ba4 --- /dev/null +++ b/archived/ui_meter.c @@ -0,0 +1,108 @@ +#define TB_IMPL +#include "../chatty/external/termbox2.h" + +#include <locale.h> +#include <signal.h> +#include <unistd.h> +#include <sys/wait.h> + +#define Assert(expr) \ + if (!(expr)) \ + { \ + tb_shutdown(); \ + raise(SIGTRAP); \ + } + +#define METER_WIDTH 4 + +static char ***EnvironmentPath = 0; + +void +draw_meter(int X, int Y, int Height, int MaxLevel, int Level) +{ + int FillColor = 0; + + Height -= 2; // substract top and bottom border + + tb_printf(X, Y, 0, 0, "%ls", L"┌──┐"); + tb_printf(X, Y + Height + 1, 0, 0, "%ls", L"└ ┘"); + + if (Level == MaxLevel) + { + FillColor = TB_GREEN; + tb_printf(X + 1, Y + Height + 1, 0, 0, "00"); + } + else + { + FillColor = TB_CYAN; + tb_printf(X + 1, Y + Height + 1, 0, 0, "%02d", Level); + } + + int FilledLevel = (Level * Height)/MaxLevel; + + for (int LevelIndex = 0; LevelIndex < FilledLevel; LevelIndex++) + { + int YLevel = Y + (Height - LevelIndex); + tb_set_cell(X + 1, YLevel, L'▒', FillColor, FillColor); + tb_set_cell(X + 2, YLevel, L'▒', FillColor, FillColor); + } + + for (int YOffset = 0; YOffset < Height; YOffset++) + { + tb_set_cell(X + 0, Y + YOffset + 1, L'│', 0, 0); + tb_set_cell(X + 3, Y + YOffset + 1, L'│', 0, 0); + } + +} + +int +main(int Argc, char *Args[], char *Envp[]) +{ + EnvironmentPath = &Envp; + + struct tb_event ev = {0}; + Assert(setlocale(LC_ALL, "")); + int Quit = 0; + int Level = 0; + int LevelStep = 10; + int MeterMaxLevel = 100; + int MeterHeight = 10 + 2; + + tb_init(); + tb_set_input_mode(TB_INPUT_ESC | TB_INPUT_MOUSE); + + while (!Quit) + { + tb_clear(); + + int Y = 0; + draw_meter(1, Y, MeterHeight, MeterMaxLevel, Level); + Y += MeterHeight + 1;; + + tb_print(0, Y, TB_BOLD, 0, "j"); tb_print(2, Y++, 0, 0, "decrease"); + tb_print(0, Y, TB_BOLD, 0, "k"); tb_print(2, Y++, 0, 0, "increase"); + tb_print(0, Y, TB_BOLD, 0, "q"); tb_print(2, Y++, 0, 0, "to quit"); + + tb_present(); + + tb_poll_event(&ev); + + if (ev.ch == 'q' || ev.key == TB_KEY_CTRL_C) Quit = 1; + else if ((ev.ch == 'j' || ev.key == TB_KEY_MOUSE_WHEEL_DOWN) && + Level > 0) + { + if (LevelStep > Level) Level = 0; + else Level -= LevelStep; + } + else if ((ev.ch == 'k' || ev.key == TB_KEY_MOUSE_WHEEL_UP) && + Level < MeterMaxLevel) + { + if (Level + LevelStep > MeterMaxLevel) Level = MeterMaxLevel; + else Level += LevelStep; + } + + } + + tb_shutdown(); + return 0; +} diff --git a/archived/ui_selection.c b/archived/ui_selection.c new file mode 100644 index 0000000..3e45ee2 --- /dev/null +++ b/archived/ui_selection.c @@ -0,0 +1,71 @@ +#define TB_IMPL +#include "../chatty/external/termbox2.h" + +#include <signal.h> + +#define Assert(expr) if (!(expr)) raise(SIGTRAP) + +typedef struct { + int X, Y; +} Position; + +int +main(void) +{ + int Selected = 0; + int NumChoices = 3; + int Y = 0; + + Position Positions[NumChoices]; + Positions[0] = (Position){1, Y}; + Positions[1] = (Position){5, Y}; + Positions[2] = (Position){9, Y}; + Y += 2; + + struct tb_event ev = {0}; + int Color = TB_GREEN; + + tb_init(); + + int Quit = 0; + while (!Quit) + { + tb_clear(); + + int BaseY = Y; + tb_printf(0, BaseY, TB_BOLD, 0, "j"); tb_printf(2, BaseY++, 0, 0, "select next"); + tb_printf(0, BaseY, TB_BOLD, 0, "k"); tb_printf(2, BaseY++, 0, 0, "select previous"); + tb_printf(0, BaseY, TB_BOLD, 0, "s"); tb_printf(2, BaseY++, 0, 0, "change color"); + tb_printf(0, BaseY, TB_BOLD, 0, "q"); tb_printf(2, BaseY++, 0, 0, "quit"); + + // Draw a box at position + for (int PositionsIndex = 0; PositionsIndex < NumChoices; PositionsIndex++) + { + Assert(Positions[PositionsIndex].X > 0); + tb_printf(Positions[PositionsIndex].X - 1, Positions[PositionsIndex].Y, Color, 0, "[ ]"); + } + // Draw mark in selected box + tb_printf(Positions[Selected].X, Positions[Selected].Y, Color, 0, "x"); + + tb_present(); + tb_poll_event(&ev); + + if (ev.ch == 'q') Quit = 1; + else if (ev.ch == 'j' && Selected < NumChoices - 1) Selected++; + else if (ev.ch == 'k' && Selected) Selected--; + else if (ev.ch == 's') + { + switch (Selected) + { + case 0: Color = TB_GREEN; break; + case 1: Color = TB_BLUE; break; + case 2: Color = TB_RED; break; + default: Assert(0); break; + } + } + } + + tb_shutdown(); + + return 0; +} diff --git a/archived/wrap.c b/archived/wrap.c index 57506b8..2317c6e 100644 --- a/archived/wrap.c +++ b/archived/wrap.c @@ -33,7 +33,7 @@ wrap(u8* Text, u32 Len, u32 XLimit, u32 YLimit) // break t = Text[X]; - *(Text + X) = '\0'; + Text[X] = '\0'; tb_printf(0, Y, 0, 0, "%s", Text + PrevX); Text[X] = t; Y++; @@ -0,0 +1,53 @@ +#ifndef ARENA_H +#define ARENA_H + +#include <sys/mman.h> +#include <stdint.h> +#include "types.h" + +// Arena Allocator +typedef struct { + void* addr; + u64 size; + u64 pos; +} Arena; +#define PushArray(arena, type, count) (type*)ArenaPush((arena), sizeof(type) * (count)) +#define PushArrayZero(arena, type, count) (type*)ArenaPushZero((arena), sizeof(type) * (count)) +#define PushStruct(arena, type) PushArray((arena), (type), 1) +#define PushStructZero(arena, type) PushArrayZero((arena), (type), 1) + +void ArenaAlloc(Arena* arena, u64 size); +void ArenaRelease(Arena* arena); +void* ArenaPush(Arena* arena, u64 size); + +#endif // ARENA_H + +#ifdef ARENA_IMPL + +// Returns arena in case of success, or 0 if it failed to alllocate the memory +void +ArenaAlloc(Arena* arena, u64 size) +{ + arena->addr = mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + Assert(arena->addr != MAP_FAILED); + arena->pos = 0; + arena->size = size; +} + +void +ArenaRelease(Arena* arena) +{ + munmap(arena->addr, arena->size); +} + +void* +ArenaPush(Arena* arena, u64 size) +{ + u8* mem; + mem = (u8*)arena->addr + arena->pos; + arena->pos += size; + Assert(arena->pos <= arena->size); + return mem; +} + +#endif // ARENA_IMPL @@ -1,456 +0,0 @@ -#define MAX_INPUT_LEN 128 -#define DEBUG - -#define TB_IMPL -#include "external/termbox2.h" -#undef TB_IMPL - -#define CHATTY_IMPL -#include "chatty.h" -#undef CHATTY_IMPL - -#include "ui.h" - -#include <locale.h> - -#ifdef DEBUG -#define Assert(expr) if (!(expr)) \ - { \ - tb_shutdown(); \ - *(u8*)0 = 0; \ - } -#else -#define Assert(expr) ; -#endif - -typedef struct { - s32 X, Y, W, H; -} rect; - -// Characters to use for drawing a box -// See DrawBox() for an example -typedef struct { - wchar_t ur, ru, rd, dr, lr, ud; -} box_characters; - -// Draw box Rect with optional Chars characters -void -DrawBox(rect Rect, box_characters *Chars) -{ - wchar_t ur, ru, rd, dr, lr, ud; - if (!Chars) - { - ur = L'╭'; - ru = L'╯'; - rd = L'╮'; - dr = L'╰'; - lr = L'─'; - ud = L'│'; - } - else - { - ur = Chars->ur; - ru = Chars->ru; - rd = Chars->rd; - dr = Chars->dr; - lr = Chars->lr; - ud = Chars->ud; - } - - Rect.H--; - Rect.W--; - - tb_printf(Rect.X, Rect.Y, 0, 0, "%lc", ur); - for (u32 X = 1; X < Rect.W; X++) - { - tb_printf(Rect.X + X, Rect.Y, 0, 0, "%lc", lr); - } - tb_printf(Rect.X + Rect.W, Rect.Y, 0, 0, "%lc", rd); - - // Draw vertical bars - for (u32 Y = 1; Y < Rect.H; Y++) - { - tb_printf(Rect.X, Rect.Y + Y, 0, 0, "%lc", ud); - tb_printf(Rect.X + Rect.W, Rect.Y + Y, 0, 0, "%lc", ud); - } - - tb_printf(Rect.X, Rect.Y + Rect.H, 0, 0, "%lc", dr); - for (u32 X = 1; X < Rect.W; X++) - { - tb_printf(Rect.X + X, Rect.Y + Rect.H, 0, 0, "%lc", lr); - } - tb_printf(Rect.X + Rect.W, Rect.Y + Rect.H, 0, 0, "%lc", ru); -} - - -// Delete a character in Text at Pos -void -Delete(wchar_t* Text, u64 Pos) -{ - memmove((u8*)(Text + Pos), - (u8*)(Text + Pos + 1), - (MAX_INPUT_LEN - Pos - 1) * sizeof(*Text)); -} - -// Check if coordinate (X,Y) is in rect boundaries -Bool -IsInRect(rect Rect, u32 X, u32 Y) -{ - if ((X >= Rect.X && X <= Rect.X + Rect.W) && - (Y >= Rect.Y && Y <= Rect.Y + Rect.H)) return True; - return False; -} - -void -ScrollLeft(rect Text, u32 *TextOffset, u32 Left) -{ - u32 TextSurface = Text.H * Text.W; - - if (global.cursor_x - Left >= Text.X) - { - global.cursor_x -= Left; - } - else - { - global.cursor_x += Text.W - 1; - - if (global.cursor_y > Text.Y) - { - global.cursor_y--; - } - else - { - global.cursor_y = Text.Y + Text.H - 1; - if (*TextOffset < TextSurface) - { - *TextOffset = 0; - } - else - { - *TextOffset -= TextSurface; - } - } - } -} - -int -main(void) -{ - assert(setlocale(LC_ALL, "")); - struct tb_event ev = {0}; - - tb_init(); - - u32 InputLen = 0; - wchar_t Input[MAX_INPUT_LEN] = {0}; - -#if 1 - wchar_t *t = L"This is some text that no one would want to know, but you could."; - u32 Len = wcslen(t); - for (u32 At = 0; At < Len; At++) Input[At] = t[At]; - InputLen = Len; -#endif - - bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); - - rect TextBox = {0, 0, 32, 4}; - -#define BOX_PADDING_X 1 -#define BOX_BORDER_WIDTH 1 - rect Text = { - 2, 1, - TextBox.W - 2*BOX_PADDING_X - 2*BOX_BORDER_WIDTH, - TextBox.H - 2*BOX_BORDER_WIDTH - }; -#undef BOX_PADDING_X -#undef BOX_BORDER_WIDTH - u32 TextSurface = Text.W * Text.H; - - global.cursor_x = Text.X; - global.cursor_y = Text.Y; - - u32 TextOffset = 0; - u32 InputPos = 0; - - while (ev.key != TB_KEY_CTRL_C) - { - - tb_clear(); - - DrawBox(TextBox, 0); - - // Draw the text right of the cursor - // NOTE: the cursor is assumed to be in the box - Assert(IsInRect(Text, global.cursor_x, global.cursor_y)); - u32 AtX = Text.X, AtY = Text.Y; - u32 At = TextOffset; - while (AtY < Text.Y + Text.H) - { - if (At < InputLen) - { - tb_printf(AtX++, AtY, 0, 0, "%lc", Input[At++]); - } - else - { - // Erase previous text - tb_printf(AtX++, AtY, 0, 0, " "); - } - - if (AtX == Text.X + Text.W) { AtY++; AtX = Text.X; } - } - - // Position in input based on cursor position - InputPos = TextOffset + (global.cursor_x - Text.X) + (global.cursor_y - Text.Y) * Text.W; - -#ifdef DEBUG - tb_printf(TextBox.X, TextBox.Y, 0, 0, - "%2d,%-2d +%d #%-2d %d/%d", - global.cursor_x, global.cursor_y, - TextOffset, InputPos, - InputLen, MAX_INPUT_LEN); -#endif - - tb_present(); - - tb_poll_event(&ev); - - if (ev.mod & TB_MOD_CTRL) - { - switch (ev.key) - { - // Delete character backwards - case TB_KEY_CTRL_8: - // case TB_KEY_BACKSPACE2: - { - if (InputPos == 0) break; - Delete(Input, InputPos - 1); - InputLen--; - // TODO: scrolling - - ScrollLeft(Text, &TextOffset, 1); - - } break; - // Delete character forwards - case TB_KEY_CTRL_D: - { - if (InputPos == InputLen) break; - Delete(Input, InputPos); - InputLen--; - // Delete(Input, Position) - } break; - // Delete Word backwards - case TB_KEY_CTRL_W: - { - u32 At = InputPos; - // Find character to stop on - while (At && is_whitespace(Input[At - 1])) At--; - while (At && !is_whitespace(Input[At - 1])) At--; - - s32 NDelete = InputPos - At; - memmove(Input + At, Input + InputPos, (InputLen - InputPos) * sizeof(Input[At])); - InputLen -= NDelete; -#ifdef DEBUG - Input[InputLen] = 0; -#endif - - // Update cursor position - if (global.cursor_x - NDelete >= Text.X) - { - global.cursor_x -= NDelete; - } - else - { - global.cursor_x += Text.W - NDelete; - - if (global.cursor_y > Text.Y) - { - global.cursor_y--; - } - else - { - // Scroll - global.cursor_y = Text.Y + Text.H - 1; - TextOffset -= TextSurface; - } - } - - Assert(IsInRect(Text, global.cursor_x, global.cursor_y)); - - } break; - // Delete until start of line - case TB_KEY_CTRL_U: - { - memmove(Input, Input + InputPos, (InputLen - InputPos) * sizeof(*Input)); - InputLen -= InputPos; -#ifdef DEBUG - Input[InputLen] = 0; -#endif - global.cursor_x = Text.X; - global.cursor_y = Text.Y; - TextOffset = 0; - // memmove until first character - } break; - // Delete until end of input - case TB_KEY_CTRL_K: break; - // Move to start - case TB_KEY_CTRL_A: global.cursor_x = Text.X; break; - // Move to end - case TB_KEY_CTRL_E: - { - if (global.cursor_x == Text.X + Text.W - 1) break; - - if (InputPos + Text.W > InputLen) - { - // Put the cursor on the last character - global.cursor_x = Text.X + (InputLen - TextOffset) % (Text.W); - } - else - { - global.cursor_x = Text.X + Text.W - 1; - } - } break; - // Move backwards by one character - case TB_KEY_CTRL_B: break; - // Move forwards by one character - case TB_KEY_CTRL_F: break; - // Move backwards by word - case TB_KEY_ARROW_LEFT: break; - // Move forwards by word - case TB_KEY_ARROW_RIGHT: break; - } - - } - // Insert new character in Input at InputPos - else if (ev.ch && InputLen < MAX_INPUT_LEN) - { - if (InputPos < InputLen) - { - memmove(Input + InputPos, - Input + InputPos - 1, - (InputLen - InputPos + 1) * sizeof(*Input)); - } - Input[InputPos] = ev.ch; - InputLen++; - - if (global.cursor_x == Text.X + Text.W - 1) - { - if (global.cursor_y == Text.Y + Text.H - 1) - { - TextOffset += Text.W; - } - else - { - global.cursor_y++; - } - global.cursor_x = Text.X; - } - else - { - global.cursor_x++; - } - } - else - { - switch (ev.key) - { - case TB_KEY_ARROW_UP: - { - if (global.cursor_y == Text.Y) - { - if (TextOffset == 0) - { - global.cursor_x = Text.X; - - break; - } - - TextOffset -= TextSurface; - global.cursor_y = Text.Y + Text.H - 1; - } - else - { - global.cursor_y--; - } - } break; - case TB_KEY_ARROW_DOWN: - { - if (InputPos + Text.W > InputLen) - { - // Put the cursor on the last character - global.cursor_x = Text.X + (InputLen - TextOffset) % (Text.W); - global.cursor_y = Text.Y + (InputLen - TextOffset) / Text.W; - - break; - } - - if (global.cursor_y == Text.Y + Text.H - 1) - { - TextOffset += TextSurface; - global.cursor_y = Text.Y; - } - else - { - global.cursor_y++; - } - } break; - - // Move character left or scroll - case TB_KEY_ARROW_LEFT: - { - if (InputPos == 0) break; - - // If text is on the first character of the box scroll backwards - if (global.cursor_x == Text.X && - global.cursor_y == Text.Y) - { - - global.cursor_x = Text.X + Text.W - 1; - global.cursor_y = Text.Y + Text.H - 1; - - // Scroll - TextOffset -= TextSurface; - - break; - } - - global.cursor_x--; - if (global.cursor_x < Text.X) - { - global.cursor_x = Text.X + Text.W - 1; - global.cursor_y--; - } - - } break; - // Move character right or scroll - case TB_KEY_ARROW_RIGHT: - { - if (InputPos == InputLen) break; - - // If cursor is on the last character scroll forwards - if (global.cursor_x == Text.X + Text.W - 1 && - global.cursor_y == Text.Y + Text.H - 1) - { - global.cursor_x = Text.X; - global.cursor_y = Text.Y; - - // Scroll - TextOffset += TextSurface; - - break; - } - - global.cursor_x++; - if (global.cursor_x == Text.X + Text.W) - { - global.cursor_x = Text.X; - global.cursor_y++; - } - } break; - } - } - - } - - tb_shutdown(); - return 0; -} diff --git a/box_test.c b/box_test.c deleted file mode 100644 index 57663f9..0000000 --- a/box_test.c +++ /dev/null @@ -1,27 +0,0 @@ -#include <stdio.h> -#include <stdbool.h> - -#define Assert(expr) if (!(expr)) return false; - -char -DrawingTest() -{ - Assert(2 == 3); - - return true; -} - -char -FooTest() -{ - return false; -} - - -int -main(int Argc, char* Argv[]) -{ - char *i(void); - i = &DrawingTest; - -} @@ -1,6 +1,7 @@ #!/bin/sh set -x -gcc external/keyboard.c -gcc -DDEBUG -ggdb -Wall -pedantic -std=c99 -o chatty chatty.c -gcc -DDEBUG -ggdb -Wall -pedantic -std=c99 -o server server.c -gcc -DDEBUG -ggdb -Wall -pedantic -std=c99 -o send send.c +# gcc external/keyboard.c +gcc -DDEBUG -ggdb -Wall -pedantic -std=c11 -I external -o build/chatty chatty.c +gcc -DDEBUG -ggdb -Wall -pedantic -std=c99 -o build/server server.c +# gcc -DDEBUG -ggdb -Wall -pedantic -std=c99 -o build/send send.c +gcc -DDEBUG -ggdb -Wall -pedantic -std=c11 -I external -I . -o build/input_box archived/input_box.c @@ -1,8 +1,8 @@ #define TB_IMPL #include "external/termbox2.h" +#undef TB_IMPL #include <arpa/inet.h> -#include <assert.h> #include <locale.h> #include <poll.h> #include <pthread.h> @@ -22,12 +22,28 @@ // Number of spaces inserted when pressing Tab/Ctrl+I #define TAB_WIDTH 4 +#ifndef Assert +#ifdef DEBUG +#define Assert(expr) if (!(expr)) \ + { \ + tb_shutdown(); \ + raise(SIGTRAP); \ + } +#else +#define Assert(expr) ; +#endif // DEBUG +#endif // Assert + #define CHATTY_IMPL #include "chatty.h" -#undef CHATTY_IMPL #include "protocol.h" + +#define TEXTBOX_MAX_INPUT MAX_INPUT_LEN #include "ui.h" +#define ARENA_IMPL +#include "arena.h" + enum { FDS_BI = 0, // for one-way communication with the server (eg. TextMessage) FDS_UNI, // For two-way communication with the server (eg. IDMessage) FDS_TTY, @@ -64,7 +80,7 @@ void popup(u32 fg, u32 bg, u8* text) { u32 len = strlen((char*)text); - assert(len > 0); + Assert(len > 0); tb_print(global.width / 2 - len / 2, global.height / 2, fg, bg, (char*)text); } @@ -96,7 +112,7 @@ add_user_info(Arena* clientsArena, s32 fd, u64 id) header.id = user.ID; IDMessage message = {id}; s32 nsend = sendAnyMessage(fd, header, &message); - assert(nsend != -1); + Assert(nsend != -1); // Wait for response IntroductionMessage introduction_message; @@ -107,7 +123,7 @@ add_user_info(Arena* clientsArena, s32 fd, u64 id) memcpy(client->Author, introduction_message.author, AUTHOR_LEN); client->ID = id; - loggingf("Got " USER_FMT "\n", USER_ARG((*client))); + LoggingF("Got " USER_FMT "\n", USER_ARG((*client))); return client; } @@ -136,11 +152,11 @@ authenticate(User* user, s32 fd) HeaderMessage header = HEADER_INIT(HEADER_TYPE_ID); IDMessage message = {user->ID}; s32 nsend = sendAnyMessage(fd, header, &message); - assert(nsend != -1); + Assert(nsend != -1); ErrorMessage error_message; s32 nrecv = recvAnyMessageType(fd, &header, &error_message, HEADER_TYPE_ERROR); - assert(nrecv != -1); + Assert(nrecv != -1); // TODO: handle not found if (nrecv == 0) return 0; @@ -157,11 +173,11 @@ authenticate(User* user, s32 fd) IntroductionMessage message; memcpy(message.author, user->Author, AUTHOR_LEN); s32 nsend = sendAnyMessage(fd, header, &message); - assert(nsend != -1); + Assert(nsend != -1); IDMessage id_message; s32 nrecv = recvAnyMessageType(fd, &header, &id_message, HEADER_TYPE_ID); - assert(nrecv != -1); + Assert(nrecv != -1); user->ID = id_message.id; return 1; } @@ -180,7 +196,7 @@ thread_reconnect(void* fds_ptr) s32 unifd, bifd; struct pollfd* fds = fds_ptr; struct timespec t = { 0, Miliseconds(300) }; // 300 miliseconds - loggingf("Trying to reconnect\n"); + LoggingF("Trying to reconnect\n"); while (1) { // timeout @@ -189,18 +205,18 @@ thread_reconnect(void* fds_ptr) bifd = get_connection(&address); if (bifd == -1) { - loggingf("errno: %d\n", errno); + LoggingF("errno: %d\n", errno); continue; } unifd = get_connection(&address); if (unifd == -1) { - loggingf("errno: %d\n", errno); + LoggingF("errno: %d\n", errno); close(bifd); continue; } - loggingf("Reconnect succeeded (%d, %d), authenticating\n", unifd, bifd); + LoggingF("Reconnect succeeded (%d, %d), authenticating\n", unifd, bifd); if (authenticate(&user, bifd) && authenticate(&user, unifd)) @@ -211,7 +227,7 @@ thread_reconnect(void* fds_ptr) close(bifd); close(unifd); - loggingf("Failed, retrying...\n"); + LoggingF("Failed, retrying...\n"); } fds[FDS_BI].fd = bifd; @@ -230,10 +246,10 @@ run_command_get_output(char *Command, char *Argv[], u8 *OutputBuffer, int Len) int CommandPipe[2]; int Error = pipe(CommandPipe); - assert(Error != -1); + Assert(Error != -1); int Pid = fork(); - assert(Pid != -1); + Assert(Pid != -1); // Run command in child if (!Pid) @@ -269,7 +285,7 @@ run_command_get_output(char *Command, char *Argv[], u8 *OutputBuffer, int Len) close(CommandPipe[1]); Result.NumRead = read(CommandPipe[0], OutputBuffer, Len); - assert(Result.NumRead != -1); + Assert(Result.NumRead != -1); return Result; } @@ -283,33 +299,43 @@ DisplayChat(Arena* ScratchArena, Arena* ClientsArena, struct pollfd* fds, wchar_t Input[], u32 InputLen) { -#define MIN_TEXT_WIDTH_FOR_WRAPPING 20 - u32 BoxHeight = GetInputBoxMinimumHeight(); - u32 MinBoxWidth = GetInputBoxMinimumWidth(); - - u32 BoxWidth = global.width - 1; - u32 InputBoxTextWidth = BoxWidth - MinBoxWidth + 2; + rect TextBox = { + 1, 0, global.width - 2, 3, + }; + u32 FreeHeight = global.height - TextBox.H; + TextBox.Y = FreeHeight; - if (InputLen >= InputBoxTextWidth && - InputBoxTextWidth > MIN_TEXT_WIDTH_FOR_WRAPPING) +#define MIN_TEXT_WIDTH_FOR_WRAPPING 20 + s32 MinBoxWidth = TEXTBOX_MIN_WIDTH; + s32 InputBoxTextWidth = TextBox.W - MinBoxWidth + 2; + bool ShouldIncreaseSize = ( + (s32)InputLen >= InputBoxTextWidth && + InputBoxTextWidth > MIN_TEXT_WIDTH_FOR_WRAPPING + ); + if (ShouldIncreaseSize) { - BoxHeight++; + TextBox.H++; } - - u32 FreeHeight = global.height - BoxHeight; - #undef MIN_TEXT_WIDTH_FOR_WRAPPING - if (global.height < BoxHeight || global.width < MinBoxWidth) + rect TextR = { + TextBox.X + 2, TextBox.Y + 1, + TextBox.W - 2*TEXTBOX_PADDING_X - 2*TEXTBOX_BORDER_WIDTH, + TextBox.H - 2*TEXTBOX_BORDER_WIDTH + }; + + if (global.height < TextBox.H || global.width < TextBox.W) { tb_hide_cursor(); return; } + bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); + global.cursor_x = TextR.X; + global.cursor_y = TextR.Y; + DrawBox(TextBox, 0); - InputBox(0, FreeHeight, BoxWidth, BoxHeight, - Input, InputLen, - True); + // InputBox(TextBox, Input, InputLen, True); // Print vertical bar s32 VerticalBarOffset = TIMESTAMP_LEN + AUTHOR_LEN + 2; @@ -335,7 +361,7 @@ DisplayChat(Arena* ScratchArena, // Used to go to the next message in MessagesArena by incrementing with the messages' size. u8* MessageAddress = MessagesArena->addr; - assert(MessageAddress != 0); + Assert(MessageAddress != 0); // Skip messages if there is not enough space to display them all u32 MessagesOffset = (MessagesNum > FreeHeight) ? MessagesNum - FreeHeight : 0; @@ -361,7 +387,7 @@ DisplayChat(Arena* ScratchArena, break; default: // unhandled message type - assert(0); + Assert(0); } } @@ -379,10 +405,10 @@ DisplayChat(Arena* ScratchArena, User* client = get_user_by_id(ClientsArena, header->id); if (!client) { - loggingf("User not known, requesting from server\n"); + LoggingF("User not known, requesting from server\n"); client = add_user_info(ClientsArena, fds[FDS_BI].fd, header->id); } - assert(client); + Assert(client); switch (header->type) { @@ -480,7 +506,7 @@ main(int argc, char** argv) } u32 arg_len = strlen(argv[1]); - assert(arg_len <= AUTHOR_LEN - 1); + Assert(arg_len <= AUTHOR_LEN - 1); memcpy(user.Author, argv[1], arg_len); user.Author[arg_len] = '\0'; @@ -506,8 +532,8 @@ main(int argc, char** argv) pthread_t thr_rec; // thread for reconnecting to server when disconnected #ifdef LOGGING - logfd = open(LOGFILE, O_RDWR | O_CREAT | O_TRUNC, 0600); - assert(logfd != -1); + LogFD = open(LOGFILE, O_RDWR | O_CREAT | O_TRUNC, 0600); + Assert(LogFD != -1); #else logfd = 2; // stderr #endif @@ -531,7 +557,7 @@ main(int argc, char** argv) // File for storing the user's ID. u32 idfile = open(ID_FILE, O_RDWR | O_CREAT, 0600); s32 nread = read(idfile, &user.id, sizeof(user.id)); - assert(nread != -1); + Assert(nread != -1); #endif /* Authentication */ { @@ -539,25 +565,25 @@ main(int argc, char** argv) bifd = get_connection(&address); if (bifd == -1) { - loggingf("errno: %d\n", errno); + LoggingF("errno: %d\n", errno); return 1; } unifd = get_connection(&address); if (unifd == -1) { - loggingf("errno: %d\n", errno); + LoggingF("errno: %d\n", errno); return 1; } - loggingf("(%d,%d)\n", bifd, unifd); + LoggingF("(%d,%d)\n", bifd, unifd); if (!authenticate(&user, bifd) || !authenticate(&user, unifd)) { - loggingf("errno: %d\n", errno); + LoggingF("errno: %d\n", errno); return 1; } else { - loggingf("Authenticated (%d,%d)\n", bifd, unifd); + LoggingF("Authenticated (%d,%d)\n", bifd, unifd); } fds[FDS_BI].fd = bifd; fds[FDS_UNI].fd = unifd; @@ -568,10 +594,10 @@ main(int argc, char** argv) write(idfile, &user.id, sizeof(user.id)); #endif - loggingf("Got ID: %lu\n", user.ID); + LoggingF("Got ID: %lu\n", user.ID); // for wide character printing - assert(setlocale(LC_ALL, "")); + Assert(setlocale(LC_ALL, "")); // init tb_init(); @@ -588,7 +614,7 @@ main(int argc, char** argv) { err = poll(fds, FDS_MAX, TIMEOUT_POLL); // ignore resize events and use them to redraw the screen - assert(err != -1 || errno == EINTR); + Assert(err != -1 || errno == EINTR); tb_clear(); @@ -597,24 +623,24 @@ main(int argc, char** argv) // got data from server HeaderMessage header; nrecv = recv(fds[FDS_UNI].fd, &header, sizeof(header), 0); - assert(nrecv != -1); + Assert(nrecv != -1); // Server disconnects if (nrecv == 0) { // close diconnected server's socket err = close(fds[FDS_UNI].fd); - assert(err == 0); + Assert(err == 0); fds[FDS_UNI].fd = -1; // ignore // start trying to reconnect in a thread err = pthread_create(&thr_rec, 0, &thread_reconnect, (void*)fds); - assert(err == 0); + Assert(err == 0); } else { if (header.version != PROTOCOL_VERSION) { - loggingf("Header received does not match version\n"); + LoggingF("Header received does not match version\n"); continue; } @@ -631,12 +657,12 @@ main(int argc, char** argv) case HEADER_TYPE_PRESENCE:; PresenceMessage* message = ArenaPush(&MessagesArena, sizeof(*message)); nrecv = recv(fds[FDS_UNI].fd, message, sizeof(*message), 0); - assert(nrecv != -1); - assert(nrecv == sizeof(*message)); + Assert(nrecv != -1); + Assert(nrecv == sizeof(*message)); MessagesNum++; break; default: - loggingf("Got unhandled message: %s\n", headerTypeString(header.type)); + LoggingF("Got unhandled message: %s\n", headerTypeString(header.type)); break; } } @@ -34,53 +34,18 @@ #define global_variable #define internal static -typedef uint8_t u8; -typedef uint16_t u16; -typedef uint32_t u32; -typedef uint64_t u64; -typedef int8_t s8; -typedef int16_t s16; -typedef int32_t s32; -typedef int64_t s64; -typedef enum { - False = 0, - True = 1 -} Bool; +#include "types.h" -// Arena Allocator -typedef struct { - void* addr; - u64 size; - u64 pos; -} Arena; -#define PushArray(arena, type, count) (type*)ArenaPush((arena), sizeof(type) * (count)) -#define PushArrayZero(arena, type, count) (type*)ArenaPushZero((arena), sizeof(type) * (count)) -#define PushStruct(arena, type) PushArray((arena), (type), 1) -#define PushStructZero(arena, type) PushArrayZero((arena), (type), 1) - -u32 wstrlen(u32* str); -void loggingf(char* format, ...); -void ArenaAlloc(Arena* arena, u64 size); -void ArenaRelease(Arena* arena); -void* ArenaPush(Arena* arena, u64 size); +void Loggingf(char* format, ...); #endif // CHATTY_H #ifdef CHATTY_IMPL -global_variable s32 logfd; - -u32 -wstrlen(u32* str) -{ - u32 i = 0; - while (str[i] != 0) - i++; - return i; -} +global_variable s32 LogFD; void -loggingf(char* format, ...) +LoggingF(char* format, ...) { char buf[LOGMESSAGE_MAX]; va_list args; @@ -96,36 +61,10 @@ loggingf(char* format, ...) u8 timestamp[LOG_LEN]; struct tm* ltime = localtime((time_t*)&t); strftime((char*)timestamp, LOG_LEN, LOG_FMT, ltime); - write(logfd, timestamp, LOG_LEN - 1); + write(LogFD, timestamp, LOG_LEN - 1); - write(logfd, buf, n); + write(LogFD, buf, n); } -// Returns arena in case of success, or 0 if it failed to alllocate the memory -void -ArenaAlloc(Arena* arena, u64 size) -{ - arena->addr = mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); - assert(arena->addr != MAP_FAILED); - arena->pos = 0; - arena->size = size; -} - -void -ArenaRelease(Arena* arena) -{ - munmap(arena->addr, arena->size); -} - -void* -ArenaPush(Arena* arena, u64 size) -{ - u8* mem; - mem = (u8*)arena->addr + arena->pos; - arena->pos += size; - assert(arena->pos <= arena->size); - return mem; -} - #endif // CHATTY_IMPL diff --git a/chatty2.c b/chatty2.c new file mode 100644 index 0000000..5a00fee --- /dev/null +++ b/chatty2.c @@ -0,0 +1,90 @@ +/* Macro's */ + +#define TB_IMPL +#include "external/termbox2.h" +#undef TB_IMPL + +#define DEBUG +#define MAX_INPUT_LEN 255 + +#define CHATTY_IMPL +#include "ui.h" +#undef CHATTY_IMPL +#include "protocol.h" + +#include <locale.h> + +#ifdef DEBUG +#define Assert(expr) \ + if (!(expr)) \ + { \ + tb_shutdown(); \ + raise(SIGTRAP); \ + } +#else +#define Assert(expr) ; +#endif + +int +main(int Argc, char *Argv[]) +{ + struct tb_event ev; + rect TextBox = {0, 0, 24, 4}; + rect TextR = { + TextBox.X + TEXTBOX_BORDER_WIDTH + TEXTBOX_PADDING_X, + TextBox.Y + TEXTBOX_BORDER_WIDTH, + TextBox.W - TEXTBOX_BORDER_WIDTH * 2 - TEXTBOX_PADDING_X * 2, + TextBox.H - TEXTBOX_BORDER_WIDTH * 2 + }; + wchar_t Input[MAX_INPUT_LEN] = {0}; + u32 InputLen = 0; + u32 InputOffset = 0; + u32 InputPos = 0; + u32 DidParseKey = 0; + + Assert(setlocale(LC_ALL, "")); + tb_init(); + global.cursor_x = TextR.X; + global.cursor_y = TextR.Y; + bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); + + while (ev.key != TB_KEY_CTRL_C) + { + tb_clear(); + + DrawBox(TextBox, 0); + TextBoxDraw(TextR, Input + InputOffset, InputLen); + + InputPos = InputOffset + (global.cursor_x - TextR.X) + (global.cursor_y - TextR.Y) * TextR.W; + Assert(InputPos <= InputLen); + + tb_present(); + + tb_poll_event(&ev); + + // TODO: Handle resize event + + // Intercept keys + if (ev.key == TB_KEY_CTRL_M) + { + tb_printf(26, 0, 0, 0, "sent."); + continue; + } + else + { + DidParseKey = TextBoxKeypress(ev, TextR, + Input, &InputLen, InputPos, &InputOffset); + } + + u32 ShouldInsert = (!DidParseKey) && (ev.ch && InputLen < MAX_INPUT_LEN); + if (ShouldInsert) + { + TextBoxInsert(Input, InputPos, InputLen++, ev.ch); + ScrollRight(TextR, &InputOffset); + } + + // tb_clear(); + } + + tb_shutdown(); +} diff --git a/gdb_scripts b/gdb_scripts new file mode 100644 index 0000000..97acf23 --- /dev/null +++ b/gdb_scripts @@ -0,0 +1,12 @@ +define tbx + tui ena + b 126 + continue + disp SearchIndex + disp PrevIndex + disp Text[TextOffset + SearchIndex] + disp Text + TextOffset + PrevIndex + disp BreakOffset + disp YOffset +end + @@ -1,7 +1,8 @@ #ifndef PROTOCOL_H #define PROTOCOL_H -#include "chatty.h" +#include "arena.h" +#include "types.h" /// Protocol // - every message has format Header + Message @@ -212,7 +213,7 @@ recvTextMessage(Arena* msgsArena, u32 fd) nrecv = recv(fd, (u8*)&message->text, text_size, 0); assert(nrecv != -1); - assert(nrecv == message->len * sizeof(*message->text)); + assert(nrecv == (s32)(message->len * sizeof(*message->text))); return message; } @@ -331,7 +332,7 @@ sendAnyMessage(u32 fd, HeaderMessage header, void* anyMessage) s32 nsend_total; s32 nsend = send(fd, &header, sizeof(header), 0); if (nsend == -1) return nsend; - loggingf("sendAnyMessage (%d)|sending "HEADER_FMT"\n", fd, HEADER_ARG(header)); + LoggingF("sendAnyMessage (%d)|sending "HEADER_FMT"\n", fd, HEADER_ARG(header)); assert(nsend == sizeof(header)); nsend_total = nsend; @@ -359,7 +360,7 @@ sendAnyMessage(u32 fd, HeaderMessage header, void* anyMessage) anyMessage = &message->text; } break; default: - loggingf("sendAnyMessage (%d)|Cannot send %s\n", fd, headerTypeString(header.type)); + LoggingF("sendAnyMessage (%d)|Cannot send %s\n", fd, headerTypeString(header.type)); return -1; } diff --git a/scroll.md b/scroll.md new file mode 100644 index 0000000..f36e774 --- /dev/null +++ b/scroll.md @@ -0,0 +1,63 @@ +# The scrolling problem: +If the text is wrapped on whitespace this means that when we scroll up we do not go +backwards by text width. It is important to update the text correctly because we want +the complete text to always look the same so that when the user can see what they expect +to see (in this case the previous text). + +# 1. Find based on skipped characters +New position could be +`InputPos - (TextWidth - Skipped)` + +This means that the place where the wrapping is done must somehow return the number of +skipped characters. + +Or we can run the wrapping algorithm on the current line to check if wrapping would +have been done and how much characters it would have skipped. + +# 2. Wrapping as is: +Searching the text backwards for whitespace works good for an existsing text. But we +have a text that we append to a lot, so it might be more interesting to add wrapping +when adding characters. + +When we add a character at the end of the line and it is not a space *wrap*. + +Problem: What if we are in the middle of the text. +1. Check the last character +2. Is it a space + y) ignore + n) search backwards for space and wrap there + +Problem: How to print +Have the wrapping algorithms on adding a character and printing be the same, which +means that if we wrapped after printing a character we should print it correctly. + +# 3. Wrapping with special characters: +To create special characters (formatting, wrapping, etc.) and preserving unicode we can +implement special characters by making our text into a special linked list structure. + +> Example +```c +typedef enum { + NodeText = 0, + NodeBold, + NodeWrap +} node_type; + +typedef stuct { + node_type Type; + wchar_t *Text; + text_node *Prev; + text_node *Next; +} text_node; + +text_node First = { NodeText, L"Hello Foo!", 0, 0 }; +text_node Second = { NodeWrap, 0, &NodeText, 0 }; +First.Next = &Second; +``` +# Notes +- We should add a special (colored?) char to show the text is wrapped because that can + be hard to tell when spaces are same as empty. +- What if we end up on a wrapping char? +- Implement 2 for wrapping +- Implement 1 for cursor movement +- Implement 3 for formatting later and instead use text start and finish diff --git a/scrollandwrapped.c b/scrollandwrapped.c new file mode 100644 index 0000000..3cd3a42 --- /dev/null +++ b/scrollandwrapped.c @@ -0,0 +1,225 @@ +#define TB_IMPL +#include "external/termbox2.h" +#undef TB_IMPL + +#define MAX_INPUT_LEN 255 +#define DEBUG + +#define CHATTY_IMPL +#include "ui.h" + +#undef Assert +#define Assert(expr) \ + if (!(expr)) \ + { \ + tb_shutdown(); \ + raise(SIGTRAP); \ + } + +void +Right(rect TextR, s32 *TextOffset) +{ + if (global.cursor_x == TextR.X + TextR.W - 1) + { + if (global.cursor_y == TextR.Y + TextR.H - 1) + { + *TextOffset += TextR.W; + } + else + { + global.cursor_y++; + } + global.cursor_x = TextR.X; + } + else + { + global.cursor_x++; + } +} + +void +Left(rect TextR, s32 *TextOffset) +{ + if (global.cursor_x == TextR.X) + { + if (global.cursor_y == TextR.Y) + { + *TextOffset -= TextR.W; + } + else + { + global.cursor_y--; + } + global.cursor_x = TextR.X + TextR.W - 1; + } + else + { + global.cursor_x--; + } + +} + +void +PrintString(s32 X, s32 Y, s32 FG, s32 BG, wchar_t *Text, s32 TextLen) +{ + for (s32 At = 0; + At < TextLen; + At++) + { +#ifdef DEBUG + tb_set_cell(X + At, Y, Text[At], TB_BLACK, TB_CYAN); + tb_present(); +#else + tb_set_cell(X + At, Y, Text[At], FG, BG); +#endif + } +} + +int +main(int Argc, char* Argv[]) +{ + wchar_t Text[MAX_INPUT_LEN] = {0}; + s32 TextLen = 0; + s32 TextPos = 0; + s32 TextOffset = 0; + rect BoxR = {0, 0, 24, 4}; + rect TextR = TEXTBOXFROMBOX(BoxR); + struct tb_event ev = {0}; + + Assert(setlocale(LC_ALL, "")); + tb_init(); + + bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); + global.cursor_x = TextR.X; + global.cursor_y = TextR.Y; + +#if 0 + { + wchar_t DummyText[] = L"This is some dummy text m" + "eant to be wrapped sever" + "al times so I can try ou" + "t how scrolling works"; + s32 Len = wcslen(DummyText); + wcsncpy(Text, DummyText, Len); + TextLen = Len; + } +#endif + + while (ev.key != TB_KEY_CTRL_C) + { + tb_clear(); + DrawBox(BoxR, 0); + +#if 1 + { + s32 SearchIndex = TextR.W; + s32 PrevIndex = 0; + s32 BreakOffset = 0; + s32 YOffset = 0; + + if (TextLen - TextOffset <= TextR.W) + { + PrintString(TextR.X, TextR.Y, 0, 0, Text + TextOffset, TextLen); + } + else + { + while (SearchIndex < TextLen - TextOffset) + { + while (Text[SearchIndex] != ' ') + { + SearchIndex--; + if (SearchIndex == PrevIndex) + { + SearchIndex += TextR.W; + break; + } + } + + BreakOffset = (PrevIndex == SearchIndex) ? TextR.W : SearchIndex - PrevIndex; + PrintString(TextR.X, TextR.Y + YOffset, 0, 0, + Text + TextOffset + PrevIndex, BreakOffset); + + YOffset++; + if (YOffset == TextR.H) + { + break; + } + + PrevIndex = SearchIndex; + SearchIndex += TextR.W; + } + + if (YOffset < TextR.H) + { + PrintString(TextR.X, TextR.Y + YOffset, 0, 0, + Text + TextOffset + PrevIndex, + TextLen - TextOffset - PrevIndex); + } + } + + + } +#endif + + tb_printf(BoxR.X, BoxR.Y, 0, 0, + "#%d/%d +%d", + TextPos, TextLen, + TextOffset); + tb_printf(BoxR.X, BoxR.Y + BoxR.H, 0, 0, + "'%lc'/'%lc'", + ev.ch ? ev.ch : L'|', Text[TextPos] ? ev.ch : L'|'); + + tb_present(); + tb_poll_event(&ev); + switch (ev.key) + { + case TB_KEY_ARROW_RIGHT: + { + if (TextPos == TextLen) break; + TextPos++; + Right(TextR, &TextOffset); + continue; + } break; + case TB_KEY_ARROW_LEFT: + { + if (TextPos == 0) break; + TextPos--; + Left(TextR, &TextOffset); + continue; + } break; + case TB_KEY_CTRL_8: + { + if (TextPos == 0) break; + TextPos--; + TextLen--; + memmove(Text + TextPos, + Text + TextPos + 1, + (MAX_INPUT_LEN - TextPos - 1)*sizeof(*Text)); + Left(TextR, &TextOffset); + continue; + } break; + } + + if (!ev.mod && ev.ch && TextLen < MAX_INPUT_LEN) + { + if (TextPos < TextLen) + { + memmove(Text + TextPos + 1, + Text + TextPos, + (MAX_INPUT_LEN - TextPos - 1)*sizeof(*Text)); + } + Text[TextPos] = ev.ch; + TextPos++; + TextLen++; + + // Advance more if wrapping will happen + if (global.cursor_x == TextR.X + TextR.W - 1 && + Text) + + Right(TextR, &TextOffset); + } + } + + tb_shutdown(); + return 0; +} @@ -1,10 +1,3 @@ -#define CHATTY_IMPL -#include "chatty.h" -#undef CHATTY_IMPL - -#include "protocol.h" - -#include <assert.h> #include <errno.h> #include <fcntl.h> #include <netinet/in.h> @@ -16,6 +9,30 @@ #include <sys/stat.h> #include <unistd.h> +/* Assertion macro */ +#ifndef Assert +#ifdef DEBUG +#define Assert(expr) if (!(expr)) { \ + raise(SIGTRAP); \ +} +#else +#define Assert(expr) if (!(expr)) { \ + raise(SIGTRAP); \ +} +#endif // DEBUG +#endif // Assert + +/* Dependencies */ +#define CHATTY_IMPL +#include "chatty.h" +#undef CHATTY_IMPL + +#define ARENA_IMPL +#include "arena.h" +#undef ARENA_IMPL +#include "protocol.h" + +/* Configuration options */ // timeout on polling #define TIMEOUT 60 * 1000 // max pending connections @@ -102,7 +119,7 @@ printTextMessage(TextMessage* message, Client* client, u8 wide) } else { u8 str[message->len]; wcstombs((char*)str, (wchar_t*)&message->text, message->len * sizeof(*message->text)); - loggingf("TextMessage: %s [%s] (%d)%s\n", timestamp, client->author, message->len, str); + LoggingF("TextMessage: %s [%s] (%d)%s\n", timestamp, client->author, message->len, str); } } @@ -135,7 +152,7 @@ sendToOthers(Client* clients, u32 nclients, Client* client, ClientFD type, Heade nsend = sendAnyMessage(fd, *header, anyMessage); assert(nsend != -1); - loggingf("sendToOthers "CLIENT_FMT"|%d<-%s %d bytes\n", CLIENT_ARG((clients[i])), fd, headerTypeString(header->type), nsend); + LoggingF("sendToOthers "CLIENT_FMT"|%d<-%s %d bytes\n", CLIENT_ARG((clients[i])), fd, headerTypeString(header->type), nsend); } } @@ -165,7 +182,7 @@ sendToAll(Client* clients, u32 nclients, ClientFD type, HeaderMessage* header, v else assert(0); assert(nsend != -1); - loggingf("sendToAll|[%s]->"CLIENT_FMT" %d bytes\n", headerTypeString(header->type), + LoggingF("sendToAll|[%s]->"CLIENT_FMT" %d bytes\n", headerTypeString(header->type), CLIENT_ARG(clients[i]), nsend); } @@ -175,7 +192,7 @@ sendToAll(Client* clients, u32 nclients, ClientFD type, HeaderMessage* header, v void disconnect(Client* client) { - loggingf("Disconnecting "CLIENT_FMT"\n", CLIENT_ARG((*client))); + LoggingF("Disconnecting "CLIENT_FMT"\n", CLIENT_ARG((*client))); if (client->unifd && client->unifd->fd != -1) { close(client->unifd->fd); @@ -215,7 +232,7 @@ authenticate(Arena* clientsArena, s32 clients_file, struct pollfd* pollfd, Heade s32 nrecv = 0; Client* client = 0; - loggingf("authenticate (%d)|" HEADER_FMT "\n", pollfd->fd, HEADER_ARG(header)); + LoggingF("authenticate (%d)|" HEADER_FMT "\n", pollfd->fd, HEADER_ARG(header)); /* Scenario 1: Search for existing client */ if (header.type == HEADER_TYPE_ID) @@ -227,7 +244,7 @@ authenticate(Arena* clientsArena, s32 clients_file, struct pollfd* pollfd, Heade client = getClientByID((Client*)clientsArena->addr, nclients, message.id); if (!client) { - loggingf("authenticate (%d)|notfound\n", pollfd->fd); + LoggingF("authenticate (%d)|notfound\n", pollfd->fd); header.type = HEADER_TYPE_ERROR; ErrorMessage error_message = ERROR_INIT(ERROR_TYPE_NOTFOUND); sendAnyMessage(pollfd->fd, header, &error_message); @@ -235,7 +252,7 @@ authenticate(Arena* clientsArena, s32 clients_file, struct pollfd* pollfd, Heade } else { - loggingf("authenticate (%d)|found [%s](%lu)\n", pollfd->fd, client->author, client->id); + LoggingF("authenticate (%d)|found [%s](%lu)\n", pollfd->fd, client->author, client->id); header.type = HEADER_TYPE_ERROR; ErrorMessage error_message = ERROR_INIT(ERROR_TYPE_SUCCESS); sendAnyMessage(pollfd->fd, header, &error_message); @@ -258,7 +275,7 @@ authenticate(Arena* clientsArena, s32 clients_file, struct pollfd* pollfd, Heade nrecv = recv(pollfd->fd, &message, sizeof(message), 0); if (nrecv != sizeof(message)) { - loggingf("authenticate (%d)|err: %d/%lu bytes\n", pollfd->fd, nrecv, sizeof(message)); + LoggingF("authenticate (%d)|err: %d/%lu bytes\n", pollfd->fd, nrecv, sizeof(message)); return 0; } @@ -279,7 +296,7 @@ authenticate(Arena* clientsArena, s32 clients_file, struct pollfd* pollfd, Heade #ifdef IMPORT_ID write(clients_file, client, sizeof(*client)); #endif - loggingf("authenticate (%d)|Added [%s](%lu)\n", pollfd->fd, client->author, client->id); + LoggingF("authenticate (%d)|Added [%s](%lu)\n", pollfd->fd, client->author, client->id); // Send ID to new client HeaderMessage header = HEADER_INIT(HEADER_TYPE_ID); @@ -292,7 +309,7 @@ authenticate(Arena* clientsArena, s32 clients_file, struct pollfd* pollfd, Heade return client; } - loggingf("authenticate (%d)|Wrong header expected %s or %s\n", pollfd->fd, + LoggingF("authenticate (%d)|Wrong header expected %s or %s\n", pollfd->fd, headerTypeString(HEADER_TYPE_INTRODUCTION), headerTypeString(HEADER_TYPE_ID)); return 0; @@ -303,15 +320,15 @@ main(int argc, char** argv) { signal(SIGPIPE, SIG_IGN); - logfd = 2; + LogFD = 2; // optional logging if (argc > 1) { if (*argv[1] == '-') if (argv[1][1] == 'l') { - logfd = open(LOGFILE, O_RDWR | O_CREAT | O_TRUNC, 0600); - assert(logfd != -1); + LogFD = open(LOGFILE, O_RDWR | O_CREAT | O_TRUNC, 0600); + assert(LogFD != -1); } } @@ -338,7 +355,7 @@ main(int argc, char** argv) err = listen(serverfd, MAX_CONNECTIONS); assert(!err); - loggingf("Listening on :%d\n", PORT); + LoggingF("Listening on :%d\n", PORT); } Arena clientsArena; @@ -374,7 +391,7 @@ main(int argc, char** argv) if (statbuf.st_size > 0) { ArenaPush(&clientsArena, statbuf.st_size); - loggingf("Imported %lu client(s)\n", statbuf.st_size / sizeof(*clients)); + LoggingF("Imported %lu client(s)\n", statbuf.st_size / sizeof(*clients)); nclients += statbuf.st_size / sizeof(*clients); // Reset pointers on imported clients @@ -385,7 +402,7 @@ main(int argc, char** argv) } } for (u32 i = 0; i < nclients - 1; i++) - loggingf("Imported: " CLIENT_FMT "\n", CLIENT_ARG(clients[i])); + LoggingF("Imported: " CLIENT_FMT "\n", CLIENT_ARG(clients[i])); #else clients_file = 0; #endif @@ -412,11 +429,11 @@ main(int argc, char** argv) if (clientfd == -1) { - loggingf("Error while accepting connection (%d)\n", clientfd); + LoggingF("Error while accepting connection (%d)\n", clientfd); continue; } else - loggingf("New connection(%d)\n", clientfd); + LoggingF("New connection(%d)\n", clientfd); // TODO: find empty space in arena (fragmentation) if (nclients + 1 == MAX_CONNECTIONS) @@ -426,14 +443,14 @@ main(int argc, char** argv) sendAnyMessage(clientfd, header, &message); if (clientfd != -1) close(clientfd); - loggingf("Max clients reached. Rejected connection\n"); + LoggingF("Max clients reached. Rejected connection\n"); } else { // no more space, allocate struct pollfd* pollfd = ArenaPush(&fdsArena, sizeof(*pollfd)); pollfd->fd = clientfd; - loggingf("Added pollfd(%d)\n", clientfd); + LoggingF("Added pollfd(%d)\n", clientfd); } } @@ -441,14 +458,14 @@ main(int argc, char** argv) { if (!(fds[conn].revents & POLLIN)) continue; if (fds[conn].fd == -1) continue; - loggingf("Message(%d)\n", fds[conn].fd); + LoggingF("Message(%d)\n", fds[conn].fd); // We received a message, try to parse the header HeaderMessage header; s32 nrecv = recv(fds[conn].fd, &header, sizeof(header), 0); if(nrecv == -1) { - loggingf("Received error from fd: %d, errno: %d\n", fds[conn].fd, errno); + LoggingF("Received error from fd: %d, errno: %d\n", fds[conn].fd, errno); }; Client* client; @@ -457,36 +474,36 @@ main(int argc, char** argv) client = getClientByFD(clients, nclients, fds[conn].fd); if (client) { - loggingf("Received %d/%lu bytes "CLIENT_FMT"\n", nrecv, sizeof(header), CLIENT_ARG((*client))); + LoggingF("Received %d/%lu bytes "CLIENT_FMT"\n", nrecv, sizeof(header), CLIENT_ARG((*client))); disconnectAndNotify(clients, nclients, client); } else { - loggingf("Got error/disconnect from unauthenticated client\n"); + LoggingF("Got error/disconnect from unauthenticated client\n"); close(fds[conn].fd); fds[conn].fd = -1; } continue; } - loggingf("Received(%d): " HEADER_FMT "\n", fds[conn].fd, HEADER_ARG(header)); + LoggingF("Received(%d): " HEADER_FMT "\n", fds[conn].fd, HEADER_ARG(header)); // Authentication if (!header.id) { - loggingf("No client for connection(%d)\n", fds[conn].fd); + LoggingF("No client for connection(%d)\n", fds[conn].fd); client = authenticate(&clientsArena, clients_file, fds + conn, header); if (!client) { - loggingf("Could not initialize client (%d)\n", fds[conn].fd); + LoggingF("Could not initialize client (%d)\n", fds[conn].fd); close(fds[conn].fd); fds[conn].fd = -1; } /* This is the first time a message is sent, because unifd is not yet set. */ else if (!client->unifd) { - loggingf("Send connected message\n"); + LoggingF("Send connected message\n"); local_persist HeaderMessage header = HEADER_INIT(HEADER_TYPE_PRESENCE); header.id = client->id; PresenceMessage message = {.type = PRESENCE_TYPE_CONNECTED}; @@ -498,7 +515,7 @@ main(int argc, char** argv) client = getClientByID(clients, nclients, header.id); if (!client) { - loggingf("No client for id %d\n", fds[conn].fd); + LoggingF("No client for id %d\n", fds[conn].fd); header.type = HEADER_TYPE_ERROR; ErrorMessage message = ERROR_INIT(ERROR_TYPE_NOTFOUND); @@ -516,7 +533,7 @@ main(int argc, char** argv) case HEADER_TYPE_TEXT: { TextMessage* text_message = recvTextMessage(&msgsArena, fds[conn].fd); - loggingf("Received(%d): ", fds[conn].fd); + LoggingF("Received(%d): ", fds[conn].fd); printTextMessage(text_message, client, 0); sendToOthers(clients, nclients, client, UNIFD, &header, text_message); @@ -547,7 +564,7 @@ main(int argc, char** argv) assert(nrecv != -1); } break; default: - loggingf("Unhandled '%s' from "CLIENT_FMT"(%d)\n", headerTypeString(header.type), + LoggingF("Unhandled '%s' from "CLIENT_FMT"(%d)\n", headerTypeString(header.type), CLIENT_ARG((*client)), fds[conn].fd); disconnectAndNotify(client, nclients, client); @@ -0,0 +1,217 @@ +// Library used for writing tests, must be included once for definitions and +// once for the `main()` function. +// +// DOCUMENTATION +// Expect(expr) +// Macro that will return early if the expression is false, calling +// `tb_shutdown()` to save the terminal. It will also print the expression +// and line on which it occurred for convenience +// +// test_functions TestFunctions +// Global variable containing the function (test). This array is +// 0-terminated. +// +// TESTFUNC() +// Macro for adding functions to TestFunctions array conveniently. +// +// tb_get_cell(x, y) +// Function +// +// EXAMPLE +// #define TEST_IMPL +// #include "test.h" +// +// bool FooTest() +// { +// Expect(0 == 1); +// return true; +// } +// +// test_functions TestFunctions[] = { +// TESTFUNC(FooTest), +// {0} +// } +// +// int main(int Argc, int Argv) +// { +// Test(Argc, Argv); +// } + +#ifndef TEST_H +#define TEST_H + +#include <stdio.h> +#include <stdbool.h> + +#include "chatty.h" + +/* Macro's */ +#define Expect(expr) if (!(expr)) \ + { \ + tb_shutdown(); \ + printf("\n %d: %s\n", __LINE__, #expr); \ + return false; \ + } + +#define TESTFUNC(func) { func, #func } + +/* Types */ +typedef struct { + bool (*Func)(void); + const char * Name; +} test_functions; + +typedef struct { + s32 X, Y, W, H; +} rect; + +/* Declarations */ +struct tb_cell tb_get_cell(s32 x, s32 y); +void RectCopy(rect Dest, rect Source); +bool RectCmp(rect Dest, rect Source); +bool TextCmp(s32 X, s32 Y, wchar_t *Text, s32 TextLen); +bool TextCmpWithColor(s32 X, s32 Y, wchar_t *Text, s32 TextLen, s32 fg, s32 bg); + +/* Global variables */ +void (*Before)(void); +void (*After)(void); + +/* Functions*/ +struct tb_cell +tb_get_cell(s32 x, s32 y) +{ + return global.back.cells[global.width * y + x]; +} + +void +RectCopy(rect Dest, rect Source) +{ + for (u32 Y = 0; Y < Source.H; Y++) + { + for (u32 X = 0; X < Source.W; X++) + { + struct tb_cell Cell = tb_get_cell(Source.X + X, Source.Y + Y); + tb_set_cell(Dest.X + X, Dest.Y + Y, Cell.ch, Cell.fg, Cell.bg); + } + } +} + +bool +RectCmp(rect Dest, rect Source) +{ + for (u32 Y = 0; Y < Source.H; Y++) + { + for (u32 X = 0; X < Source.W; X++) + { + struct tb_cell SourceCell = tb_get_cell(Source.X + X, Source.Y + Y); + struct tb_cell DestCell = tb_get_cell(Dest.X + X, Dest.Y + Y); + if (!(SourceCell.fg == DestCell.fg && + SourceCell.bg == DestCell.bg && + SourceCell.ch == DestCell.ch)) + { + return false; + } + } + } + + return true; +} + +bool +TextCmp(s32 X, s32 Y, wchar_t *Text, s32 TextLen) +{ + for (u32 At = 0; + At < TextLen; + At++) + { + struct tb_cell DestCell = tb_get_cell(X++, Y); + if (DestCell.ch != Text[At]) return false; + } + + return true; +} + +bool +TextCmpWithColor(s32 X, s32 Y, wchar_t *Text, s32 TextLen, s32 fg, s32 bg) +{ + for (u32 At = 0; + At < TextLen; + At++) + { + struct tb_cell DestCell = tb_get_cell(X++, Y); + if (DestCell.ch != Text[At] || + DestCell.fg != fg || + DestCell.bg != bg) + { + return false; + } + } + + return true; +} + +int +Test(test_functions *TestFunctions, int Argc, char *Argv[]) +{ + u32 TestFunctionsLen = 0; + while (TestFunctions[TestFunctionsLen].Func) TestFunctionsLen++; + + if (Argc > 1) + { + for (u32 Arg = 0; + Arg < Argc; + Arg++) + { + char *Function = Argv[Arg]; + + for (u32 TestFunc = 0; + TestFunc < TestFunctionsLen; + TestFunc++) + { + if(!strcmp(Function, TestFunctions[TestFunc].Name)) + { + printf("%s ", TestFunctions[TestFunc].Name); + + if (Before) Before(); + bool Ret = TestFunctions[TestFunc].Func(); + if (After) After(); + + if (Ret) + { + printf("\033[32mPASSED\033[0m\n"); + } + else + { + printf("\033[31mFAILED\033[0m\n"); \ + } + } + } + } + } + else + { + for (int At = 0; + TestFunctions[At].Func; + At++) + { + printf("%s ", TestFunctions[At].Name); + + if (Before) Before(); + bool Ret = TestFunctions[At].Func(); + if (After) After(); + + if (Ret) + { + printf("\033[32mPASSED\033[0m\n"); + } + else + { + printf("\033[31mFAILED\033[0m\n"); \ + } + } + } + + return 0; +} + +#endif // TEST_H @@ -0,0 +1,37 @@ +#define MAX_INPUT_LEN 255 + +#define TB_IMPL +#include "external/termbox2.h" + +#include "chatty.h" + +#define TEST_IMPL +#include "test.h" + +#define Assert(expr) if (!(expr)) *(u8*)0 = 0 + +bool +DrawingTest(void) +{ + struct tb_event ev = {0}; + + + + return true; +} + +int +main(int Argc, char* Argv[]) +{ + test_functions TestFunctions[] = { + TESTFUNC(DrawingTest), + { 0 } + }; + + Assert(setlocale(LC_ALL, "")); + + Before = (void(*)(void))tb_init; + After = (void(*)(void))tb_shutdown; + + return(Test(TestFunctions, Argc, Argv)); +} @@ -0,0 +1,15 @@ +#ifndef TYPES_H +#define TYPES_H + +#include <stdbool.h> +#include <stdint.h> +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int8_t s8; +typedef int16_t s16; +typedef int32_t s32; +typedef int64_t s64; + +#endif // TYPES_H @@ -1,4 +1,14 @@ -#include "chatty.h" +#ifndef UI_H +#define UI_H + +/* Macro's */ + +#include <stdint.h> +#include <stdbool.h> +#include "termbox2.h" +#include "arena.h" + +/* Types */ // Format option at a position in raw text, used when iterating to know when to toggle a color // option. @@ -18,24 +28,535 @@ typedef struct { u32 Len; } raw_result; +// Rectangle +typedef struct { + s32 X, Y, W, H; +} rect; + +// Characters to use for drawing a box +// See DrawBox() for an example +typedef struct { + wchar_t ur, ru, rd, dr, lr, ud; +} box_characters; + +/* Functions */ + +bool IsInRect(rect Rect, s32 X, s32 Y); +bool is_whitespace(u32 ch); +bool is_markdown(u32 ch); +void tb_print_wrapped(u32 X, u32 Y, u32 XLimit, u32 YLimit, u32* Text, u32 Len); +void tb_print_markdown(u32 X, u32 Y, u32 fg, u32 bg, u32* Text, u32 Len); +u32 tb_print_wrapped_with_markdown(u32 XOffset, u32 YOffset, u32 fg, u32 bg, + u32* Text, u32 Len, + u32 XLimit, u32 YLimit, + markdown_formatoptions MDFormat); +raw_result markdown_to_raw(Arena* ScratchArena, wchar_t* Text, u32 Len); +markdown_formatoptions preprocess_markdown(Arena* ScratchArena, wchar_t* Text, u32 Len); + +/* Input Box UI */ + +// assumes TEXTBOX_MAX_INPUT to be set +// +// #define TEXTBOX_MAX_INPUT 128 + +#define TEXTBOX_PADDING_X 1 +#define TEXTBOX_BORDER_WIDTH 1 +#define TEXTBOX_MIN_WIDTH TEXTBOX_PADDING_X * 2 + TEXTBOX_BORDER_WIDTH * 2 + 1; +#define TEXTBOXFROMBOX(Box) \ + { \ + .X = Box.X + TEXTBOX_BORDER_WIDTH + TEXTBOX_PADDING_X, \ + .Y = Box.Y + TEXTBOX_BORDER_WIDTH, \ + .W = Box.W - TEXTBOX_BORDER_WIDTH * 2 - TEXTBOX_PADDING_X * 2, \ + .H = Box.H - TEXTBOX_BORDER_WIDTH * 2 \ + } + +void DrawBox(rect Rect, box_characters *Chars); +void DrawTextBox(rect TextR, wchar_t *Text, u32 TextLen); +void DrawTextBoxWrapped(rect TextR, wchar_t *Text, u32 TextLen); +void TextBoxScrollLeft(rect Text, u32 *TextOffset); +void TextBoxScrollRight(rect Text, u32 *TextOffset); +void TextBoxDelete(wchar_t* Text, u64 Pos); +void TextBoxInsert(wchar_t *Input, u32 InputPos, u32 InputLen, wchar_t ch); +u32 TextBoxKeypress(struct tb_event ev, + rect TextR, wchar_t *Text, u32 *TextLenPtr, u32 TextPos, u32 *TextOffsetPtr); + +// Draw box along boundaries in Rect with optional Chars. +void +DrawBox(rect Rect, box_characters *Chars) +{ + wchar_t ur, ru, rd, dr, lr, ud; + if (!Chars) + { + ur = L'╭'; + ru = L'╯'; + rd = L'╮'; + dr = L'╰'; + lr = L'─'; + ud = L'│'; + } + else + { + ur = Chars->ur; + ru = Chars->ru; + rd = Chars->rd; + dr = Chars->dr; + lr = Chars->lr; + ud = Chars->ud; + } + + Rect.H--; + Rect.W--; + + tb_printf(Rect.X, Rect.Y, 0, 0, "%lc", ur); + for (s32 X = 1; X < Rect.W; X++) + { + tb_printf(Rect.X + X, Rect.Y, 0, 0, "%lc", lr); + } + tb_printf(Rect.X + Rect.W, Rect.Y, 0, 0, "%lc", rd); + + // Draw vertical bars + for (s32 Y = 1; Y < Rect.H; Y++) + { + tb_printf(Rect.X, Rect.Y + Y, 0, 0, "%lc", ud); + tb_printf(Rect.X + Rect.W, Rect.Y + Y, 0, 0, "%lc", ud); + } + + tb_printf(Rect.X, Rect.Y + Rect.H, 0, 0, "%lc", dr); + for (s32 X = 1; X < Rect.W; X++) + { + tb_printf(Rect.X + X, Rect.Y + Rect.H, 0, 0, "%lc", lr); + } + tb_printf(Rect.X + Rect.W, Rect.Y + Rect.H, 0, 0, "%lc", ru); +} + +// SCROLLING +// ╭──────────╮ ╭──────────╮ Going Left on the first character scrolls up. +// │ █3 4 │ => │ 1 2█ │ Cursor on end of the top line. +// │ 5 6 │ │ 3 4 │ +// ╰──────────╯ ╰──────────╯ +// +// ╭──────────╮ ╭──────────╮ Going Right on the last character scrolls down. +// │ 1 3 │ => │ 2 4 │ Puts cursor on start of the bottom line. +// │ 2 4█ │ │ █ │ +// ╰──────────╯ ╰──────────╯ +// +// ╭──────────╮ ╭──────────╮ Going Down on bottom line scrolls down. +// │ 1 3 │ => │ 2 4 │ Cursor stays on bottom line. +// │ 2 █ 4 │ │ █ │ +// ╰──────────╯ ╰──────────╯ +// +// ╭──────────╮ ╭──────────╮ Going Up on top line scrolls up. +// │ 3 █ 4 │ => │ 1 █ 2 │ Cursor stays on top line. +// │ 5 6 │ │ 3 5 │ +// ╰──────────╯ ╰──────────╯ +// +// In code this translates to changing global.cursor_{x,y} and TextOffset accordingly. + +// Scroll one character to the left +void +TextBoxScrollLeft(rect Text, u32 *TextOffset) +{ + // If text is on the first character of the box scroll up + if (global.cursor_x == Text.X && + global.cursor_y == Text.Y) + { + global.cursor_x = Text.X + Text.W - 1; + global.cursor_y = Text.Y; + + *TextOffset -= Text.W; + } + else + { + if (global.cursor_x == Text.X) + { + // Got to previous line + global.cursor_x = Text.X + Text.W - 1; + global.cursor_y--; + } + else + { + global.cursor_x--; + } + } +} + +// Scroll one character to the right +void +TextBoxScrollRight(rect Text, u32 *TextOffset) +{ + // If cursor is on the last character scroll forwards + if (global.cursor_x == Text.X + Text.W - 1 && + global.cursor_y == Text.Y + Text.H - 1) + { + global.cursor_x = Text.X; + global.cursor_y = Text.Y + Text.H - 1; + + *TextOffset += Text.W; + } + else + { + global.cursor_x++; + if (global.cursor_x == Text.X + Text.W) + { + global.cursor_x = Text.X; + global.cursor_y++; + } + } +} + + +// Delete a character in Text at Pos +void +TextBoxDelete(wchar_t* Text, u64 Pos) +{ + memmove(Text + Pos, + Text + Pos + 1, + (TEXTBOX_MAX_INPUT - Pos - 1) * sizeof(*Text)); +} + +// Insert a ev.ch in Input at InputPos +void +TextBoxInsert(wchar_t *Input, u32 InputPos, u32 InputLen, wchar_t ch) +{ + if (InputPos < InputLen) + { + memmove(Input + InputPos, + Input + InputPos - 1, + (InputLen - InputPos + 1) * sizeof(*Input)); + } + Input[InputPos] = ch; +} + +// Handle the key event ev changing Text, TextLenPtr, and TextOffsetPtr accordingly. +// InputPos is the position in the Input relating to the cursor position. +// TextR is the bounding box for the text. +// +// Returns non-zero when a key event was handled. +// +// TODO: pass by value and return struct with updated values +u32 +TextBoxKeypress(struct tb_event ev, rect TextR, + wchar_t *Text, u32 *TextLenPtr, u32 TextPos, u32 *TextOffsetPtr) +{ + u32 Result = 1; + + u32 TextLen = *TextLenPtr; + u32 TextOffset = *TextOffsetPtr; + + switch (ev.key) + { + + // Delete character backwards + case TB_KEY_CTRL_8: + // case TB_KEY_BACKSPACE2: + { + if (TextPos == 0) break; + + TextBoxDelete(Text, TextPos - 1); + TextLen--; + + TextBoxScrollLeft(TextR, &TextOffset); + + } break; + + // Delete character forwards + case TB_KEY_CTRL_D: + { + if (TextPos == TextLen) break; + TextBoxDelete(Text, TextPos); + TextLen--; + // Delete(Text, Position) + } break; + + // Delete word backwards + case TB_KEY_CTRL_W: + { + u32 At = TextPos; + // Find character to stop on + while (At && is_whitespace(Text[At - 1])) At--; + while (At && !is_whitespace(Text[At - 1])) At--; + + s32 NDelete = TextPos - At; + memmove(Text + At, Text + TextPos, (TextLen - TextPos) * sizeof(Text[At])); + TextLen -= NDelete; +#ifdef DEBUG + Text[TextLen] = 0; +#endif + // NOTE: this could be calculated at once instead + while(NDelete--) TextBoxScrollLeft(TextR, &TextOffset); + + Assert(IsInRect(TextR, global.cursor_x, global.cursor_y)); + + } break; + + // Delete until start of Text + case TB_KEY_CTRL_U: + { + memmove(Text, Text + TextPos, (TextLen - TextPos) * sizeof(*Text)); + TextLen -= TextPos; +#ifdef DEBUG + Text[TextLen] = 0; +#endif + global.cursor_x = TextR.X; + global.cursor_y = TextR.Y; + TextOffset = 0; + } break; + + // Delete until end of Text + case TB_KEY_CTRL_K: + { + TextLen = TextPos; + Text[TextPos] = 0; + } break; + + // Move to start of line + case TB_KEY_CTRL_A: global.cursor_x = TextR.X; break; + + // Move to end of line + case TB_KEY_CTRL_E: + { + if (global.cursor_x == TextR.X + TextR.W - 1) break; + + if (TextPos + TextR.W > TextLen) + { + // Put the cursor on the last character + global.cursor_x = TextR.X + (TextLen - TextOffset) % TextR.W; + } + else + { + global.cursor_x = TextR.X + TextR.W - 1; + } + } break; + + // Move backwards + case TB_KEY_CTRL_B: + case TB_KEY_ARROW_LEFT: + { + // Move forward by word + if (ev.mod == TB_MOD_CTRL) + { + u32 At = TextPos; + while(At && is_whitespace(Text[At])) At--; + while(At && !is_whitespace(Text[At])) At--; + while(TextPos - At++) TextBoxScrollLeft(TextR, &TextOffset); + } + // Move forward by character + else + { + if (TextPos == 0) break; + TextBoxScrollLeft(TextR, &TextOffset); + } + } break; + + // Move forwards + case TB_KEY_CTRL_F: + case TB_KEY_ARROW_RIGHT: + { + // Move forward by word + if (ev.mod == TB_MOD_CTRL) + { + u32 At = TextPos; + while(At < TextLen && is_whitespace(Text[At])) At++; + while(At < TextLen && !is_whitespace(Text[At])) At++; + while(At-- - TextPos) TextBoxScrollRight(TextR, &TextOffset); + } + // Move forward by character + else + { + if (TextPos == TextLen) break; + TextBoxScrollRight(TextR, &TextOffset); + } + } break; + + // Move up + case TB_KEY_CTRL_P: + case TB_KEY_ARROW_UP: + { + if (global.cursor_y == TextR.Y) + { + if (TextOffset == 0) + { + global.cursor_x = TextR.X; + + break; + } + + TextOffset -= TextR.W; + global.cursor_y = TextR.Y; + } + else + { + global.cursor_y--; + } + } break; + + // Move down + case TB_KEY_CTRL_N: + case TB_KEY_ARROW_DOWN: + { + if (TextPos + TextR.W > TextLen) + { + // Put the cursor on the last character + global.cursor_x = TextR.X + (TextLen - TextOffset) % (TextR.W); + global.cursor_y = TextR.Y + (TextLen - TextOffset) / TextR.W; + + // If cursor ended 1 line under the bottom line this means that the text + // needs to be scrolled. + if (global.cursor_y == TextR.Y + TextR.H) + { + TextOffset += TextR.W; + global.cursor_y--; + } + + break; + } + + if (global.cursor_y == TextR.Y + TextR.H - 1) + { + TextOffset += TextR.W; + } + else + { + global.cursor_y++; + } + } break; + default: + { + Result = 0; + } + } + + *TextLenPtr = TextLen; + *TextOffsetPtr = TextOffset; + + return Result; +} + +// Draws characters from Text fitting in the TextR rectangle. +// InputLen is the amount of characters in Text. +// +// NOTE: TextR is always filled, when not enough characters in Input it will uses spaces instead. +// This makes it easy to update the textbox by recalling this function. +void +DrawTextBox(rect TextR, wchar_t *Text, u32 TextLen) +{ + // Draw the text right of the cursor + // NOTE: the cursor is assumed to be in the box + Assert(IsInRect(TextR, global.cursor_x, global.cursor_y)); + s32 AtX = TextR.X, AtY = TextR.Y; + u32 At = 0; + while (AtY < TextR.Y + TextR.H) + { + if (At < TextLen) + { + tb_printf(AtX++, AtY, 0, 0, "%lc", Text[At++]); + } + else + { + tb_printf(AtX++, AtY, 0, 0, " "); + } + + if (AtX == TextR.X + TextR.W) { AtY++; AtX = TextR.X; } + } +} + +// NOTE: To ensure that the text looks the same even when scrolling it you must provide the whole text, +// wrap the whole text and the only show the portion that can fit in the Text Rectangle. + +// When line will exceed width break the word on the next line. This is done by looking backwards +// for whitespace from TextR.W width. When a whitespace is found text is wrapped on the next line. +// TODO: this does not work yet. +void +DrawTextBoxWrapped(rect TextR, wchar_t *Text, u32 TextLen) +{ + if (TextLen <= TextR.W) + { + tb_printf(TextR.X, TextR.Y, 0, 0, "%ls", Text); + tb_present(); + global.cursor_x = TextR.X + TextLen; + global.cursor_y = TextR.Y; + return; + } + + u32 SearchIndex = TextR.W; + u32 PrevIndex = 0; + u32 Y = TextR.Y; + + while (SearchIndex < TextLen) + { + while (Text[SearchIndex] != ' ') + { + SearchIndex--; + if (SearchIndex == PrevIndex) + { + SearchIndex += TextR.W; + break; + } + } + + // Wrap + wchar_t BreakChar = Text[SearchIndex]; + Text[SearchIndex] = 0; + tb_printf(TextR.X, Y, 0, 0, "%ls", Text + PrevIndex); + tb_present(); + Text[SearchIndex] = BreakChar; + + if (Y + 1 == TextR.Y + TextR.H) + { + global.cursor_y = Y; + global.cursor_x = TextR.X + (SearchIndex - PrevIndex); + return; + } + Y++; + + if (BreakChar == L' ') + { + SearchIndex++; + } + + PrevIndex = SearchIndex; + SearchIndex += TextR.W; + } + + // This happens when SearchIndex exceeds TextLen but there is still some + // text left to print. We can assume that the text will fit because otherwise it would have + // been wrapped a second time and the loop would have returned. + tb_printf(TextR.X, Y, 0, 0, "%ls", Text + PrevIndex); + // NOTE: this sets the cursor position correctly + + global.cursor_y = Y; + global.cursor_x = TextR.X + TextLen - PrevIndex; +} + +#endif // UI_H + +// Check if coordinate (X,Y) is in rect boundaries +bool +IsInRect(rect Rect, s32 X, s32 Y) +{ + if ((X >= Rect.X && X <= Rect.X + Rect.W) && + (Y >= Rect.Y && Y <= Rect.Y + Rect.H)) return true; + return false; +} + // Return True if ch is whitespace -Bool +bool is_whitespace(u32 ch) { if (ch == L' ') - return True; - return False; + return true; + return false; } // Return True if ch is a supported markdown markup character // TODO: tilde -Bool +bool is_markdown(u32 ch) { if (ch == L'_' || ch == L'*') - return True; - return False; + return true; + return false; } // Print `Text`, `Len` characters long with markdown @@ -85,8 +606,8 @@ void tb_print_wrapped(u32 X, u32 Y, u32 XLimit, u32 YLimit, u32* Text, u32 Len) { // Iterator in text - assert(XLimit > 0); - assert(YLimit > 0); + Assert(XLimit > 0); + Assert(YLimit > 0); u32 i = XLimit; u32 PrevI = 0; @@ -148,8 +669,8 @@ tb_print_wrapped_with_markdown(u32 XOffset, u32 YOffset, u32 fg, u32 bg, { XLimit -= XOffset; YLimit -= YOffset; - assert(YLimit > 0); - assert(XLimit > 0); + Assert(YLimit > 0); + Assert(XLimit > 0); u32 TextIndex = XLimit; u32 PrevTextIndex = 0; @@ -201,8 +722,8 @@ tb_print_wrapped_with_markdown(u32 XOffset, u32 YOffset, u32 fg, u32 bg, } tb_printf(X++, Y, fg, bg, "%lc", Text[TextIndex]); } - assert(WrapPositionsIndex == WrapPositionsLen); - assert(MDFormat.Len == MDFormatOptionsIndex); + Assert(WrapPositionsIndex == WrapPositionsLen); + Assert(MDFormat.Len == MDFormatOptionsIndex); return WrapPositionsLen + 1; } @@ -296,186 +817,3 @@ preprocess_markdown(Arena* ScratchArena, wchar_t* Text, u32 Len) return Result; } - -u32 InputBoxMarginX = 1; -u32 InputBoxPaddingX = 1; -#define INPUT_BOX_BORDER_WIDTH 1 -#define INPUT_BOX_MIN_TEXT_WIDTH 1 -#define INPUT_BOX_MIN_TEXT_HEIGHT 1 - -u32 -GetInputBoxMinimumWidth() -{ - return InputBoxPaddingX * 2 + - InputBoxMarginX * 2 + - INPUT_BOX_BORDER_WIDTH * 2 + - INPUT_BOX_MIN_TEXT_WIDTH; -} - -u32 -GetInputBoxMinimumHeight() -{ - return INPUT_BOX_BORDER_WIDTH * 2 + - INPUT_BOX_MIN_TEXT_HEIGHT; -} - -void -InputBox(u32 BoxX, u32 BoxY, u32 BoxWidth, u32 BoxHeight, - wchar_t *Text, u32 TextLen, - Bool Focused) -{ - // ╭───────╮ - // M│P....█P│M - // ╰───────╯ - // . -> text - // █ -> cursor - // P -> padding (symmetric) - // M -> margin (symmetric) - - u32 MarginX = InputBoxMarginX; - u32 PaddingX = InputBoxPaddingX; - u32 BorderWidth = INPUT_BOX_BORDER_WIDTH; - - // Get 0-based coordinate - BoxWidth -= 2* MarginX; - BoxHeight--; - - wchar_t ur = L'╭'; - wchar_t ru = L'╯'; - wchar_t rd = L'╮'; - wchar_t dr = L'╰'; - wchar_t lr = L'─'; - wchar_t ud = L'│'; - - // Draw Borders - { - // Draw the top bar - tb_printf(BoxX + MarginX, BoxY, 0, 0, "%lc", ur); - for (u32 X = 1; - X < BoxWidth; - X++) - { - tb_printf(BoxX + X + MarginX, BoxY, 0, 0, "%lc", lr); - } - tb_printf(BoxX + BoxWidth + MarginX, BoxY, 0, 0, "%lc", rd); - - // Draw vertical bars - for (u32 Y = 1; - Y < BoxHeight; - Y++) - { - tb_printf(BoxX + MarginX, BoxY + Y, 0, 0, "%lc", ud); - tb_printf(BoxX + BoxWidth + MarginX, BoxY + Y, 0, 0, "%lc", ud); - } - - // Draw the bottom bar - tb_printf(BoxWidth + MarginX, BoxY + BoxHeight, 0, 0, "%lc", ru); - for (u32 X = 1; - X < BoxWidth; - X++) - { - tb_printf(BoxX + X + MarginX, BoxY + BoxHeight, 0, 0, "%lc", lr); - } - tb_printf(BoxX + MarginX, BoxY + BoxHeight, 0, 0, "%lc", dr); - } - - // Draw the text - u32 TextX = BoxX + MarginX + PaddingX + 1; - u32 TextY = BoxY + 1; - u32 TextWidth = BoxWidth - TextX - MarginX + 1; - u32 TextHeight = BoxHeight - BorderWidth * 2 + 1; - u32 TextDisplaySize = TextWidth * TextHeight; - - // XOffset and YOffset are needed for setting the cursor position - u32 XOffset = 0, YOffset = 0; - u32 TextOffset = 0; - - // If there is more than one line, implement vertical wrapping otherwise scroll the text - // horizontally. - if (TextHeight > 1) - { - // If there is not enough space to fit the text scroll one line by advancing by textwidth. - if (TextLen >= TextDisplaySize) - { - // TextHeight - 1 : scroll by one line - TextOffset = (TextLen / TextWidth - (TextHeight - 1)) * TextWidth; - } - - // Print the text - while (TextOffset < TextLen) - { - for (YOffset = 0; - YOffset < TextHeight && TextOffset < TextLen; - YOffset++) - { - for (XOffset = 0; - XOffset < TextWidth && TextOffset < TextLen; - XOffset++) - { - tb_printf(TextX + XOffset, TextY + YOffset, 0, 0, "%lc", Text[TextOffset]); - TextOffset++; - } - } - } - } - else - { - // Scrooll the text horizontally - if (TextLen >= TextDisplaySize) - { - TextOffset = TextLen - TextWidth; - XOffset = TextWidth; - } - else - { - XOffset = TextLen; - } - YOffset = 1; - tb_printf(TextX, TextY, 0, 0, "%ls", Text + TextOffset); - } - -#ifdef DEBUG -#ifdef MAX_INPUT_LEN - tb_printf(BoxX + 1, BoxY, 0, 0, "%d/%d [%dx%d] %dx%d %d (%d,%d)+(%d,%d)", - TextLen, MAX_INPUT_LEN, - BoxWidth, BoxHeight, - TextWidth, TextHeight, - TextOffset, - TextX, TextY, - XOffset, YOffset); -#else - tb_printf(BoxX + 1, BoxY, 0, 0, "%d/%d [%dx%d] %dx%d %d (%d,%d)+(%d,%d)", - TextLen, TextLen, - BoxWidth, BoxHeight, - TextWidth, TextHeight, - TextOffset, - TextX, TextY, - XOffset, YOffset); - -#endif -#endif - - // Set the cursor - if (Focused) - { - if (TextLen == 0) - { - // When there is no text - global.cursor_x = TextX; - global.cursor_y = TextY; - } - else if (TextLen % TextWidth == 0 && TextHeight > 1) - { - // When at the end of width put the cursor on the next line - global.cursor_x = TextX; - global.cursor_y = TextY + YOffset; - } - else - { - // Put cursor after the text - // Minus one because of the for loop - global.cursor_x = (TextX-1) + XOffset + 1; - global.cursor_y = (TextY-1) + YOffset; - } - } -} diff --git a/ui_box.h b/ui_box.h new file mode 100644 index 0000000..c19b9dd --- /dev/null +++ b/ui_box.h @@ -0,0 +1,472 @@ +#ifndef BOX_H +#define BOX_H + +// assumes MAX_INPUT_LEN to be set +// +// #define MAX_INPUT_LEN 128 + +#define TEXTBOX_PADDING_X 1 +#define TEXTBOX_BORDER_WIDTH 1 +#define TEXTBOX_MIN_WIDTH TEXTBOX_PADDING_X * 2 + TEXTBOX_BORDER_WIDTH * 2 + 1; +#define TEXTBOXFROMBOX(Box) \ + { \ + .X = Box.X + TEXTBOX_BORDER_WIDTH + TEXTBOX_PADDING_X, \ + .Y = Box.Y + TEXTBOX_BORDER_WIDTH, \ + .W = Box.W - TEXTBOX_BORDER_WIDTH * 2 - TEXTBOX_PADDING_X * 2, \ + .H = Box.H - TEXTBOX_BORDER_WIDTH * 2 \ + } + +void DrawBox(rect Rect, box_characters *Chars); +void ScrollLeft(rect Text, u32 *TextOffset); +void ScrollRight(rect Text, u32 *TextOffset); +void TextBoxDelete(wchar_t* Text, u64 Pos); +void TextBoxInsert(wchar_t *Input, u32 InputPos, u32 InputLen, wchar_t ch); +u32 TextBoxKeypress(struct tb_event ev, rect TextR, wchar_t *Text, u32 *TextLenPtr, u32 TextPos, u32 *TextOffsetPtr); +void TextBoxDrawText(rect TextR, wchar_t *Input, u32 InputLen, u32 TextOffset); + +// Draw box along boundaries in Rect with optional Chars. +void +DrawBox(rect Rect, box_characters *Chars) +{ + wchar_t ur, ru, rd, dr, lr, ud; + if (!Chars) + { + ur = L'╭'; + ru = L'╯'; + rd = L'╮'; + dr = L'╰'; + lr = L'─'; + ud = L'│'; + } + else + { + ur = Chars->ur; + ru = Chars->ru; + rd = Chars->rd; + dr = Chars->dr; + lr = Chars->lr; + ud = Chars->ud; + } + + Rect.H--; + Rect.W--; + + tb_printf(Rect.X, Rect.Y, 0, 0, "%lc", ur); + for (s32 X = 1; X < Rect.W; X++) + { + tb_printf(Rect.X + X, Rect.Y, 0, 0, "%lc", lr); + } + tb_printf(Rect.X + Rect.W, Rect.Y, 0, 0, "%lc", rd); + + // Draw vertical bars + for (s32 Y = 1; Y < Rect.H; Y++) + { + tb_printf(Rect.X, Rect.Y + Y, 0, 0, "%lc", ud); + tb_printf(Rect.X + Rect.W, Rect.Y + Y, 0, 0, "%lc", ud); + } + + tb_printf(Rect.X, Rect.Y + Rect.H, 0, 0, "%lc", dr); + for (s32 X = 1; X < Rect.W; X++) + { + tb_printf(Rect.X + X, Rect.Y + Rect.H, 0, 0, "%lc", lr); + } + tb_printf(Rect.X + Rect.W, Rect.Y + Rect.H, 0, 0, "%lc", ru); +} + +// SCROLLING +// ╭──────────╮ ╭──────────╮ Going Left on the first character scrolls up. +// │ █3 4 │ => │ 1 2█ │ Cursor on end of the top line. +// │ 5 6 │ │ 3 4 │ +// ╰──────────╯ ╰──────────╯ +// +// ╭──────────╮ ╭──────────╮ Going Right on the last character scrolls down. +// │ 1 3 │ => │ 2 4 │ Puts cursor on start of the bottom line. +// │ 2 4█ │ │ █ │ +// ╰──────────╯ ╰──────────╯ +// +// ╭──────────╮ ╭──────────╮ Going Down on bottom line scrolls down. +// │ 1 3 │ => │ 2 4 │ Cursor stays on bottom line. +// │ 2 █ 4 │ │ █ │ +// ╰──────────╯ ╰──────────╯ +// +// ╭──────────╮ ╭──────────╮ Going Up on top line scrolls up. +// │ 3 █ 4 │ => │ 1 █ 2 │ Cursor stays on top line. +// │ 5 6 │ │ 3 5 │ +// ╰──────────╯ ╰──────────╯ +// +// In code this translates to changing global.cursor_{x,y} and TextOffset accordingly. + +// Scroll one character to the left +void +TextBoxScrollLeft(rect Text, u32 *TextOffset) +{ + // If text is on the first character of the box scroll up + if (global.cursor_x == Text.X && + global.cursor_y == Text.Y) + { + global.cursor_x = Text.X + Text.W - 1; + global.cursor_y = Text.Y; + + *TextOffset -= Text.W; + } + else + { + if (global.cursor_x == Text.X) + { + // Got to previous line + global.cursor_x = Text.X + Text.W - 1; + global.cursor_y--; + } + else + { + global.cursor_x--; + } + } +} + +// Scroll one character to the right +void +TextBoxScrollRight(rect Text, u32 *TextOffset) +{ + // If cursor is on the last character scroll forwards + if (global.cursor_x == Text.X + Text.W - 1 && + global.cursor_y == Text.Y + Text.H - 1) + { + global.cursor_x = Text.X; + global.cursor_y = Text.Y + Text.H - 1; + + *TextOffset += Text.W; + } + else + { + global.cursor_x++; + if (global.cursor_x == Text.X + Text.W) + { + global.cursor_x = Text.X; + global.cursor_y++; + } + } +} + + +// Delete a character in Text at Pos +void +TextBoxDelete(wchar_t* Text, u64 Pos) +{ + memmove(Text + Pos, + Text + Pos + 1, + (MAX_INPUT_LEN - Pos - 1) * sizeof(*Text)); +} + +// Insert a ev.ch in Input at InputPos +void +TextBoxInsert(wchar_t *Input, u32 InputPos, u32 InputLen, wchar_t ch) +{ + if (InputPos < InputLen) + { + memmove(Input + InputPos, + Input + InputPos - 1, + (InputLen - InputPos + 1) * sizeof(*Input)); + } + Input[InputPos] = ch; +} + +// Handle the key event and edit Input and updates TextLenPtr and TextOffsetPtr accordingly. +// InputPos is the position in the Input relating to the cursor position. +// TextR is the bounding box for the text. +// Returns 0 no key event was handled +u32 +TextBoxKeypress(struct tb_event ev, rect TextR, + wchar_t *Text, u32 *TextLenPtr, u32 TextPos, u32 *TextOffsetPtr) +{ + u32 Result = 1; + + u32 TextLen = *TextLenPtr; + u32 TextOffset = *TextOffsetPtr; + + switch (ev.key) + { + + // Delete character backwards + case TB_KEY_CTRL_8: + // case TB_KEY_BACKSPACE2: + { + if (TextPos == 0) break; + + TextBoxDelete(Text, TextPos - 1); + TextLen--; + + TextBoxScrollLeft(TextR, &TextOffset); + + } break; + + // Delete character forwards + case TB_KEY_CTRL_D: + { + if (TextPos == TextLen) break; + TextBoxDelete(Text, TextPos); + TextLen--; + // Delete(Text, Position) + } break; + + // Delete word backwards + case TB_KEY_CTRL_W: + { + u32 At = TextPos; + // Find character to stop on + while (At && is_whitespace(Text[At - 1])) At--; + while (At && !is_whitespace(Text[At - 1])) At--; + + s32 NDelete = TextPos - At; + memmove(Text + At, Text + TextPos, (TextLen - TextPos) * sizeof(Text[At])); + TextLen -= NDelete; +#ifdef DEBUG + Text[TextLen] = 0; +#endif + // NOTE: this could be calculated at once instead + while(NDelete--) TextBoxScrollLeft(TextR, &TextOffset); + + Assert(IsInRect(TextR, global.cursor_x, global.cursor_y)); + + } break; + + // Delete until start of Text + case TB_KEY_CTRL_U: + { + memmove(Text, Text + TextPos, (TextLen - TextPos) * sizeof(*Text)); + TextLen -= TextPos; +#ifdef DEBUG + Text[TextLen] = 0; +#endif + global.cursor_x = TextR.X; + global.cursor_y = TextR.Y; + TextOffset = 0; + } break; + + // Delete until end of Text + case TB_KEY_CTRL_K: + { + TextLen = TextPos; + Text[TextPos] = 0; + } break; + + // Move to start of line + case TB_KEY_CTRL_A: global.cursor_x = TextR.X; break; + + // Move to end of line + case TB_KEY_CTRL_E: + { + if (global.cursor_x == TextR.X + TextR.W - 1) break; + + if (TextPos + TextR.W > TextLen) + { + // Put the cursor on the last character + global.cursor_x = TextR.X + (TextLen - TextOffset) % TextR.W; + } + else + { + global.cursor_x = TextR.X + TextR.W - 1; + } + } break; + + // Move backwards + case TB_KEY_CTRL_B: + case TB_KEY_ARROW_LEFT: + { + // Move forward by word + if (ev.mod == TB_MOD_CTRL) + { + u32 At = TextPos; + while(At && is_whitespace(Text[At])) At--; + while(At && !is_whitespace(Text[At])) At--; + while(TextPos - At++) TextBoxScrollLeft(TextR, &TextOffset); + } + // Move forward by character + else + { + if (TextPos == 0) break; + TextBoxScrollLeft(TextR, &TextOffset); + } + } break; + + // Move forwards + case TB_KEY_CTRL_F: + case TB_KEY_ARROW_RIGHT: + { + // Move forward by word + if (ev.mod == TB_MOD_CTRL) + { + u32 At = TextPos; + while(At < TextLen && is_whitespace(Text[At])) At++; + while(At < TextLen && !is_whitespace(Text[At])) At++; + while(At-- - TextPos) TextBoxScrollRight(TextR, &TextOffset); + } + // Move forward by character + else + { + if (TextPos == TextLen) break; + TextBoxScrollRight(TextR, &TextOffset); + } + } break; + + // Move up + case TB_KEY_CTRL_P: + case TB_KEY_ARROW_UP: + { + if (global.cursor_y == TextR.Y) + { + if (TextOffset == 0) + { + global.cursor_x = TextR.X; + + break; + } + + TextOffset -= TextR.W; + global.cursor_y = TextR.Y; + } + else + { + global.cursor_y--; + } + } break; + + // Move down + case TB_KEY_CTRL_N: + case TB_KEY_ARROW_DOWN: + { + if (TextPos + TextR.W > TextLen) + { + // Put the cursor on the last character + global.cursor_x = TextR.X + (TextLen - TextOffset) % (TextR.W); + global.cursor_y = TextR.Y + (TextLen - TextOffset) / TextR.W; + + // If cursor ended 1 line under the bottom line this means that the text + // needs to be scrolled. + if (global.cursor_y == TextR.Y + TextR.H) + { + TextOffset += TextR.W; + global.cursor_y--; + } + + break; + } + + if (global.cursor_y == TextR.Y + TextR.H - 1) + { + TextOffset += TextR.W; + } + else + { + global.cursor_y++; + } + } break; + default: + { + Result = 0; + } + } + + *TextLenPtr = TextLen; + *TextOffsetPtr = TextOffset; + + return Result; +} + +// Draws characters in Input in the TextR rectangle. +// Skip printing TextOffset amount of characters. +// InputLen is the amount of characters in Input. +// +// NOTE: TextR is always filled, when not enough characters in Input it will uses spaces instead. +// This makes it easy to update the textbox by recalling this function. +void +TextBoxDraw(rect TextR, wchar_t *Text, u32 TextLen) +{ + // Draw the text right of the cursor + // NOTE: the cursor is assumed to be in the box + Assert(IsInRect(TextR, global.cursor_x, global.cursor_y)); + s32 AtX = TextR.X, AtY = TextR.Y; + u32 At = 0; + while (AtY < TextR.Y + TextR.H) + { + if (At < TextLen) + { + tb_printf(AtX++, AtY, 0, 0, "%lc", Text[At++]); + } + else + { + tb_printf(AtX++, AtY, 0, 0, " "); + } + + if (AtX == TextR.X + TextR.W) { AtY++; AtX = TextR.X; } + } +} + +// NOTE: To ensure that the text looks the same even when scrolling it you must provide the whole text, +// wrap the whole text and the only show the portion that can fit in the Text Rectangle. + +// When line will exceed width break the word on the next line. This is done by looking backwards +// for whitespace from TextR.W width. When a whitespace is found text is wrapped on the next line. +void +TextBoxDrawWrapped(rect TextR, wchar_t *Text, u32 TextLen) +{ + if (TextLen <= TextR.W) + { + tb_printf(TextR.X, TextR.Y, 0, 0, "%ls", Text); + tb_present(); + global.cursor_x = TextR.X + TextLen; + global.cursor_y = TextR.Y; + return; + } + + u32 SearchIndex = TextR.W; + u32 PrevIndex = 0; + u32 Y = TextR.Y; + + while (SearchIndex < TextLen) + { + while (Text[SearchIndex] != ' ') + { + SearchIndex--; + if (SearchIndex == PrevIndex) + { + SearchIndex += TextR.W; + break; + } + } + + // Wrap + wchar_t BreakChar = Text[SearchIndex]; + Text[SearchIndex] = 0; + tb_printf(TextR.X, Y, 0, 0, "%ls", Text + PrevIndex); + tb_present(); + Text[SearchIndex] = BreakChar; + + if (Y + 1 == TextR.Y + TextR.H) + { + global.cursor_y = Y; + global.cursor_x = TextR.X + (SearchIndex - PrevIndex); + return; + } + Y++; + + if (BreakChar == L' ') + { + SearchIndex++; + } + + PrevIndex = SearchIndex; + SearchIndex += TextR.W; + } + + // This happens when SearchIndex exceeds TextLen but there is still some + // text left to print. We can assume that the text will fit because otherwise it would have + // been wrapped a second time and the loop would have returned. + tb_printf(TextR.X, Y, 0, 0, "%ls", Text + PrevIndex); + // NOTE: this sets the cursor position correctly + + global.cursor_y = Y; + global.cursor_x = TextR.X + TextLen - PrevIndex; +} + +#endif // BOX_H diff --git a/ui_wrapped.c b/ui_wrapped.c new file mode 100644 index 0000000..67ddffe --- /dev/null +++ b/ui_wrapped.c @@ -0,0 +1,82 @@ +#define MAX_INPUT_LEN 255 + +#define TB_IMPL +#include "external/termbox2.h" +#undef TB_IMPL +#define CHATTY_IMPL +#include "ui.h" +#undef CHATTY_IMPL + +void +DrawBoxTest(rect Box, wchar_t *DummyText) +{ + u32 TextLen = wcslen(DummyText); + + wchar_t Text[TextLen]; + bzero(Text, sizeof(Text)); + wcsncpy(Text, DummyText, TextLen + 1); // copy n*ull terminator + + rect TextR = TEXTBOXFROMBOX(Box); + // fill the cursor for reference + for (s32 Y = 0; Y < TextR.H; Y++) + for (s32 X = 0; X < TextR.W; X++) + tb_printf(TextR.X + X, TextR.Y + Y, 0, TB_BLUE, " "); + + DrawBox(Box, 0); + tb_present(); + TextBoxDrawWrapped(TextR, Text, TextLen); + tb_printf(0, TextR.Y - 1, 0, 0, + "%d (%d, %d) ~(%d, %d)", + TextLen, + global.cursor_x, global.cursor_y, + global.cursor_x - TextR.X, global.cursor_y - TextR.Y); + tb_printf(global.cursor_x, global.cursor_y, 0, TB_RED, " "); +} + +struct tb_cell +tb_get_cell(s32 X, s32 Y) +{ + return global.back.cells[global.width * Y + X]; +} + + +int +main(int Argc, char* Argv[]) +{ + struct tb_event ev; + rect Box = {0, 0, 24, 4}; + + setlocale(LC_ALL, ""); + + tb_init(); + bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); + + /* Text that does not fit */ + DrawBoxTest(Box, L"This is some dummy text meant to be wrapped multiple times."); + Box.Y += Box.H; + + /* Text that fits */ + DrawBoxTest(Box, L"That is some."); + Box.Y += Box.H; + + /* Text ending on a space */ + DrawBoxTest(Box, L"There is something. "); + Box.Y += Box.H; + + /* Text that fits the surface */ + DrawBoxTest(Box, L"This is something. This is something."); + Box.Y += Box.H; + + /* Text that wraps once */ + DrawBoxTest(Box, L"This is something I do not."); + + rect NewBox = Box; + NewBox.X += Box.W + 2; + DrawBox(NewBox, 0); + + tb_present(); + tb_poll_event(&ev); + + tb_shutdown(); + return 0; +} diff --git a/utf8toASCII.c b/utf8toASCII.c deleted file mode 100644 index 988a69b..0000000 --- a/utf8toASCII.c +++ /dev/null @@ -1,163 +0,0 @@ -#include <stdlib.h> -#include <stdio.h> -#include <stdio.h> -#include <locale.h> -#include <assert.h> -#include <wchar.h> -#include <stdint.h> - -typedef uint8_t u8; -typedef uint16_t u16; -typedef uint32_t u32; -typedef uint64_t u64; -typedef int8_t s8; -typedef int16_t s16; -typedef int32_t s32; -typedef int64_t s64; - -static size_t -UTF8Compress(size_t InSize, wchar_t* In, size_t OutSize, u8* OutBase) -{ - wchar_t* InEnd = (wchar_t*)((u8*)In + InSize); - u8* Out = OutBase; - -#define MAX_LITERAL_COUNT 255 - u8 ASCIILiterals[MAX_LITERAL_COUNT] = {0}; - u8 ASCIILiteralsCount = 0; - wchar_t UTF8Literals[MAX_LITERAL_COUNT] = {0}; - u8 UTF8LiteralsCount = 0; - - while (In < InEnd) - { - wchar_t CurrentChar = In[0]; - - // Check consecutive ascii characters - while(CurrentChar == (u8)CurrentChar && - ASCIILiteralsCount < MAX_LITERAL_COUNT) - { - ASCIILiterals[ASCIILiteralsCount++] = (u8)CurrentChar; - CurrentChar = *++In; - } - - while(CurrentChar != (u8)CurrentChar && - UTF8LiteralsCount < MAX_LITERAL_COUNT) - { - UTF8Literals[UTF8LiteralsCount++] = CurrentChar; - CurrentChar = *++In; - } - - // Encode ASCII/UTF8 pair - *Out++ = ASCIILiteralsCount; - for (u8 ch = 0; - ch < ASCIILiteralsCount; - ch++) - { - *Out = ASCIILiterals[ch]; - Out += sizeof(ASCIILiterals[ch]); - } - ASCIILiteralsCount = 0; - - *Out++ = UTF8LiteralsCount; - for (u8 ch = 0; - ch < UTF8LiteralsCount; - ch++) - { - *(wchar_t*)Out = UTF8Literals[ch]; - Out += sizeof(UTF8Literals[ch]); - } - UTF8LiteralsCount = 0; - - } -#undef MAX_LITERAL_COUNT - assert(In == InEnd); - - return Out - OutBase; -} - -static void -PrintCompressedUTF8(u8* In, size_t InSize) -{ - u8* InEnd = In + InSize; - - while (In < InEnd) - { - u8 ASCIICount = *In++; - wprintf(L"%dA(\"", ASCIICount); - while(ASCIICount--) - { - wprintf(L"%c", *In); - In += sizeof(u8); - } - wprintf(L"\") "); - - u8 UTF8Count = *In++; - wprintf(L"%dU(\"", UTF8Count); - while(UTF8Count--) - { - wprintf(L"%lc", *(wchar_t*)In); - In += sizeof(wchar_t); - } - wprintf(L"\") "); - } - wprintf(L"\n"); - - assert(In == InEnd); -} - -static void -UTF8Decompress(size_t InSize, u8* In, size_t OutSize, wchar_t* Out) -{ - u8* InEnd = In + InSize; - - while (In < InEnd) - { - u8 ASCIICount = *In++; - while(ASCIICount--) - { - *Out++ = *In++; - } - - u8 UTF8Count = *In++; - while(UTF8Count--) - { - *Out++ = *(wchar_t*)In; - In += sizeof(wchar_t); - } - } - assert(In == InEnd); -} - -// Size is the size of the UTF8 string in bytes. "aaa" would be 12. -size_t -UTF8GetMaximumCompressedSize(size_t Size) -{ - // The largest would be if there was only one unicode point in which case we store 0 for ascii 1 - // for unicode and the raw codepoint. 1 + 1 + 4 * CodepointNum - return Size + 2; -} - -int -main(int Argc, char* Argv[]) { - assert(setlocale(LC_ALL, "") != 0); - - wchar_t* InBuf = L"text│tt│"; - size_t InSize = wcslen(InBuf) * 4; - - size_t OutSize = UTF8GetMaximumCompressedSize(InSize); - u8 OutBuf[OutSize]; - - size_t CompressedSize = UTF8Compress(InSize, InBuf, OutSize, OutBuf); - - fwprintf(stderr, L"Raw string: \"%ls\"\n", InBuf); - fwprintf(stderr, L"Compressed %lu bytes -> %lu bytes.\n", InSize, CompressedSize); - - size_t DecompressedSize = InSize; - wchar_t *DecompressedBuffer = malloc(DecompressedSize); - - UTF8Decompress(CompressedSize, OutBuf, DecompressedSize, DecompressedBuffer); - fwprintf(stderr, L"Decompressed: \"%ls\"\n", DecompressedBuffer); - - PrintCompressedUTF8(OutBuf, CompressedSize); - - return 0; -} |