aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaymaekers Luca <raymaekers.luca@gmail.com>2024-11-23 03:32:40 +0100
committerRaymaekers Luca <raymaekers.luca@gmail.com>2024-11-23 03:32:40 +0100
commitd01caf679f6f819a501687278d75be110646d6ed (patch)
tree5a165ea71e09b9a0e2c7621f233640061eb0e12c
parentb9725521023fec9364888ffd473042b707ab1be5 (diff)
Created InputBox widget
-rw-r--r--chatty.c165
-rw-r--r--ui.h (renamed from ui.c)134
2 files changed, 185 insertions, 114 deletions
diff --git a/chatty.c b/chatty.c
index 6631ebf..9dcc2b0 100644
--- a/chatty.c
+++ b/chatty.c
@@ -1,5 +1,5 @@
#define TB_IMPL
-#include "termbox2.h"
+#include "external/termbox2.h"
#include <arpa/inet.h>
#include <assert.h>
@@ -12,7 +12,7 @@
#define TIMEOUT_POLL 60 * 1000
// time to reconnect in seconds
#define TIMEOUT_RECONNECT 1
-#define INPUT_LIMIT 512
+#define MAX_INPUT_LEN 512
// Filepath where user ID is stored
#define ID_FILE "_id"
// Filepath where logged
@@ -26,7 +26,7 @@
#include "chatty.h"
#include "protocol.h"
-#include "ui.c"
+#include "ui.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)
@@ -273,6 +273,7 @@ run_command_get_output(char *Command, char *Argv[], u8 *OutputBuffer, int Len)
return Result;
}
+
// home screen, the first screen the user sees
// it displays a prompt with the user input of input_len wide characters
// and the received messages from msgsArena
@@ -280,28 +281,18 @@ void
screen_home(Arena* ScratchArena,
Arena* MessagesArena, u32 MessagesNum,
Arena* ClientsArena, struct pollfd* fds,
- u32 Input[], u32 InputLen)
+ wchar_t Input[], u32 InputLen)
{
- // config options
- const s32 box_max_len = 80;
- const s32 box_x = 0, box_y = global.height - 3, box_pad_x = 1, box_mar_x = 1, box_bwith = 1, box_height = 3;
- const u32 prompt_x = box_x + box_pad_x + box_mar_x + box_bwith + InputLen;
-
- // the minimum height required is the hight for the box prompt
- // the minimum width required is that one character should fit in the box prompt
- if (global.height < box_height ||
- global.width < (box_x + box_mar_x * 2 + box_pad_x * 2 + box_bwith * 2 + 1))
+ u32 BoxHeight = 3;
+ u32 BoxWidth = global.width - 1;
+
+ if (global.height < BoxHeight ||
+ global.width < 8)
{
- // + 1 for cursor
tb_hide_cursor();
return;
}
- else
- {
- // show cursor
- // TODO: show cursor as block character instead of using the real cursor
- bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]);
- }
+
// Print messages in msgsArena, if there are too many to display, start printing from an offset.
// Looks like this:
@@ -312,7 +303,7 @@ screen_home(Arena* ScratchArena,
{
s32 VerticalBarOffset = TIMESTAMP_LEN + AUTHOR_LEN + 2;
- u32 FreeHeight = global.height - box_height;
+ u32 FreeHeight = global.height - BoxHeight;
if (FreeHeight <= 0)
goto draw_prompt;
@@ -395,9 +386,9 @@ screen_home(Arena* ScratchArena,
// Only display when there is enough space
if (global.width > VerticalBarOffset + 2)
{
- raw_result RawText = markdown_to_raw(ScratchArena, (u32*)&message->text, message->len);
+ raw_result RawText = markdown_to_raw(ScratchArena, (wchar_t*)&message->text, message->len);
markdown_formatoptions MDFormat = preprocess_markdown(ScratchArena,
- (u32*)&message->text,
+ (wchar_t*)&message->text,
message->len);
u32 timesWrapped = tb_print_wrapped_with_markdown(VerticalBarOffset + 2, MessageY, fg, 0,
@@ -455,67 +446,11 @@ screen_home(Arena* ScratchArena,
tb_print(VerticalBarOffset, Y, 0, 0, "│");
draw_prompt:
- // Draw prompt box which is a box made out of
- // should look like this: ╭───────╮
- // │ text█ │
- // ╰───────╯
- // the text is padded to the left and right by box_pad_x
- // the middle/inner part is opaque
- // TODO: wrapping when the text is bigger & alternated with scrolling when there is not
- // enough space.
- {
- u32 box_len = 0;
- if (global.width >= box_max_len + 2 * box_mar_x)
- box_len = box_max_len;
- else
- box_len = global.width - box_mar_x * 2;
-
- // +2 for corners and null terminator
- u32 box_up[box_len + 1];
- u32 box_in[box_len + 1];
- u32 box_down[box_len + 1];
- u32 lr = L'─', ur = L'╭', rd = L'╮', dr = L'╰', ru = L'╯', ud = L'│';
-
- // top bar
- box_up[0] = ur;
- fillstr(box_up + 1, lr, box_len - 1);
- box_up[box_len - 1] = rd;
- box_up[box_len] = 0;
- // inner part
- fillstr(box_in + 1, L' ', box_len - 1);
- box_in[0] = ud;
- box_in[box_len - 1] = ud;
- box_in[box_len] = 0;
- // bottom bar
- box_down[0] = dr;
- fillstr(box_down + 1, lr, box_len - 1);
- box_down[box_len - 1] = ru;
- box_down[box_len] = 0;
-
- tb_printf(box_x + box_mar_x, box_y, 0, 0, "%ls", box_up);
- tb_printf(box_x + box_mar_x, box_y + 1, 0, 0, "%ls", box_in);
- tb_printf(box_x + box_mar_x, box_y + 2, 0, 0, "%ls", box_down);
-
- global.cursor_y = box_y + 1;
-
- // NOTE: wrapping would be better.
- // Scroll the text when it exceeds the prompt's box length
- u32 freesp = box_len - box_pad_x * 2 - box_bwith * 2;
- if (freesp <= 0)
- return;
-
- if (InputLen > freesp)
- {
- u32* text_offs = Input + (InputLen - freesp);
- tb_printf(box_x + box_mar_x + box_pad_x + box_bwith, box_y + 1, 0, 0, "%ls", text_offs);
- global.cursor_x = box_x + box_pad_x + box_mar_x + box_bwith + freesp;
- }
- else
- {
- global.cursor_x = prompt_x;
- tb_printf(box_x + box_mar_x + box_pad_x + box_bwith, box_y + 1, 0, 0, "%ls", Input);
- }
- }
+ InputBox(0, FreeHeight, BoxWidth, BoxHeight,
+ Input, InputLen,
+ True);
+ bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]);
+
if (fds[FDS_UNI].fd == -1 || fds[FDS_BI].fd == -1)
{
@@ -544,8 +479,8 @@ main(int argc, char** argv)
u32 MessagesNum = 0; // Number of messages in msgsArena
s32 nrecv = 0; // number of bytes received
- u32 Input[INPUT_LIMIT] = {0}; // input buffer
- u32 InputLen = 0; // number of characters in input
+ wchar_t Input[MAX_INPUT_LEN] = {0}; // input buffer
+ u32 InputIndex = 0; // number of characters in input
Arena ScratchArena;
Arena MessagesArena;
@@ -632,7 +567,10 @@ main(int argc, char** argv)
tb_init();
tb_get_fds(&fds[FDS_TTY].fd, &fds[FDS_RESIZE].fd);
- screen_home(&ScratchArena, &MessagesArena, MessagesNum, &ClientsArena, fds, Input, InputLen);
+ screen_home(&ScratchArena,
+ &MessagesArena, MessagesNum,
+ &ClientsArena, fds,
+ Input, InputIndex);
tb_present();
// main loop
@@ -703,24 +641,24 @@ main(int argc, char** argv)
{
case TB_KEY_CTRL_W:
// delete consecutive whitespace
- while (InputLen)
+ while (InputIndex)
{
- if (Input[InputLen - 1] == L' ')
+ if (Input[InputIndex - 1] == L' ')
{
- Input[InputLen - 1] = 0;
- InputLen--;
+ Input[InputIndex - 1] = 0;
+ InputIndex--;
continue;
}
break;
}
// delete until whitespace
- while (InputLen)
+ while (InputIndex)
{
- if (Input[InputLen - 1] == L' ')
+ if (Input[InputIndex - 1] == L' ')
break;
// erase
- Input[InputLen - 1] = 0;
- InputLen--;
+ Input[InputIndex - 1] = 0;
+ InputIndex--;
}
break;
case TB_KEY_CTRL_Z:
@@ -732,7 +670,7 @@ main(int argc, char** argv)
} break;
case TB_KEY_CTRL_Y: // Paste clipboard contents to input
{
- u32 OutputBufferLen = INPUT_LIMIT - InputLen;
+ u32 OutputBufferLen = MAX_INPUT_LEN - InputIndex;
if (OutputBufferLen <= 0) break;
u8 OutputBuffer[OutputBufferLen];
@@ -758,28 +696,31 @@ main(int argc, char** argv)
{
// convert u8 to u32
u32 ch = OutputBuffer[BufferIndex];
- Input[InputLen] = ch;
- InputLen++;
+ Input[InputIndex] = ch;
+ InputIndex++;
}
} break;
case TB_KEY_CTRL_I:
{
for (u32 i = 0;
- i < TAB_WIDTH && InputLen < INPUT_LIMIT - 1;
+ i < TAB_WIDTH && InputIndex < MAX_INPUT_LEN - 1;
i++)
{
- Input[InputLen] = L' ';
- InputLen++;
+ Input[InputIndex] = L' ';
+ InputIndex++;
}
} break;
+ case TB_KEY_BACKSPACE2:
+ if (InputIndex) InputIndex--;
+ break;
case TB_KEY_CTRL_D:
case TB_KEY_CTRL_C:
quit = 1;
break;
case TB_KEY_CTRL_M: // send message
{
- raw_result RawText = markdown_to_raw(0, Input, InputLen);
+ raw_result RawText = markdown_to_raw(0, Input, InputIndex);
if (RawText.Len == 0)
// do not send empty message
@@ -789,8 +730,8 @@ main(int argc, char** argv)
break;
// null terminate
- Input[InputLen] = 0;
- InputLen++;
+ Input[InputIndex] = 0;
+ InputIndex++;
// Save header
HeaderMessage* header = ArenaPush(&MessagesArena, sizeof(*header));
@@ -801,9 +742,9 @@ main(int argc, char** argv)
// Save message
TextMessage* sendmsg = ArenaPush(&MessagesArena, TEXTMESSAGE_SIZE);
sendmsg->timestamp = time(0);
- sendmsg->len = InputLen;
+ sendmsg->len = InputIndex;
- u32 text_size = InputLen * sizeof(*Input);
+ u32 text_size = InputIndex * sizeof(*Input);
ArenaPush(&MessagesArena, text_size);
memcpy(&sendmsg->text, Input, text_size);
@@ -813,20 +754,20 @@ main(int argc, char** argv)
// also clear input
} // fallthrough
case TB_KEY_CTRL_U: // clear input
- bzero(Input, InputLen * sizeof(*Input));
- InputLen = 0;
+ bzero(Input, InputIndex * sizeof(*Input));
+ InputIndex = 0;
break;
default:
if (ev.ch == 0)
break;
// TODO: show error
- if (InputLen == INPUT_LIMIT - 1) // last byte reserved for \0
+ if (InputIndex == MAX_INPUT_LEN - 1) // last byte reserved for \0
break;
// append key to input buffer
- Input[InputLen] = ev.ch;
- InputLen++;
+ Input[InputIndex] = ev.ch;
+ InputIndex++;
}
if (quit)
break;
@@ -839,7 +780,7 @@ main(int argc, char** argv)
tb_poll_event(&ev);
}
- screen_home(&ScratchArena, &MessagesArena, MessagesNum, &ClientsArena, fds, Input, InputLen);
+ screen_home(&ScratchArena, &MessagesArena, MessagesNum, &ClientsArena, fds, Input, InputIndex);
tb_present();
}
diff --git a/ui.c b/ui.h
index 8d7cab4..ff98598 100644
--- a/ui.c
+++ b/ui.h
@@ -211,7 +211,7 @@ tb_print_wrapped_with_markdown(u32 XOffset, u32 YOffset, u32 fg, u32 bg,
// Len should be characters + null terminator
// Copies the null terminator as well
raw_result
-markdown_to_raw(Arena* ScratchArena, u32* Text, u32 Len)
+markdown_to_raw(Arena* ScratchArena, wchar_t* Text, u32 Len)
{
raw_result Result = {0};
if (ScratchArena)
@@ -239,7 +239,7 @@ markdown_to_raw(Arena* ScratchArena, u32* Text, u32 Len)
// Use Scratcharena to make allocations on that buffer, The Maximimum space needed is Len, eg. when
// the string is only markup characters.
markdown_formatoptions
-preprocess_markdown(Arena* ScratchArena, u32* Text, u32 Len)
+preprocess_markdown(Arena* ScratchArena, wchar_t* Text, u32 Len)
{
markdown_formatoptions Result = {0};
Result.Options = (format_option*)((u8*)ScratchArena->addr + ScratchArena->pos);
@@ -294,3 +294,133 @@ preprocess_markdown(Arena* ScratchArena, u32* Text, u32 Len)
return Result;
}
+
+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)
+
+ // Configuration options
+ u32 MarginX = 1;
+ u32 PaddingX = 1;
+ u32 MinTextSpace = 3;
+ u32 BorderWidth = 1;
+
+ // TODO: return early if there is not enough space
+ if (BoxHeight < 3)
+ return;
+ if (BoxWidth < MarginX * 2 + PaddingX * 2 + MinTextSpace)
+ return;
+
+ // 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;
+
+ u32 At = 0;
+ // 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
+ At = (TextLen / TextWidth - (TextHeight - 1)) * TextWidth;
+ }
+
+#ifdef DEBUG
+ tb_printf(BoxX + 1, BoxY, 0, 0, "%d/%d %dx%d %d", TextLen, MAX_INPUT_LEN, TextWidth, TextHeight, At);
+#endif
+
+ // Keep if needed for cursor position
+ u32 XOffset = 0, YOffset = 0;
+ while (At < TextLen)
+ {
+ for (YOffset = 0;
+ YOffset < TextHeight && At < TextLen;
+ YOffset++)
+ {
+ for (XOffset = 0;
+ XOffset < TextWidth && At < TextLen;
+ XOffset++)
+ {
+ tb_printf(TextX + XOffset, TextY + YOffset, 0, 0, "%lc", Text[At]);
+ At++;
+ }
+ }
+ }
+
+ // 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)
+ {
+ // 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;
+ }
+ }
+}