# HG changeset patch # User June Park # Date 1768409793 28800 # Node ID 05cf9467a1c3435178cbd535e38cf114deab0200 # Parent 1c0878eb17debdfca274bb75afbefe67bad66c96 [Postdog] Updated to use text area that can handle like html text area. diff -r 1c0878eb17de -r 05cf9467a1c3 postdog/main.c --- a/postdog/main.c Wed Jan 14 07:59:19 2026 -0800 +++ b/postdog/main.c Wed Jan 14 08:56:33 2026 -0800 @@ -39,6 +39,996 @@ #define BODY_BUFFER_LENGTH 1024 * 1024 * 5 #define RESULT_BUFFER_LENGTH 1024 * 1024 * 5 +// ============================================================================ +// TextArea Component +// ============================================================================ + +#define TEXT_SIZE_DEFAULT 10 // used to calcualte spacing +#define TEXT_AREA_FONT_SIZE 16 +#define TEXT_AREA_LINE_HEIGHT 20 +#define TEXT_AREA_PADDING 8 +#define TEXT_AREA_CURSOR_WIDTH 2 +#define TEXT_AREA_MAX_UNDO_STATES 64 +#define TEXT_AREA_MAX_INSTANCES 8 + +typedef struct { + char *text; + int cursor_pos; + int selection_start; + int selection_end; +} TextAreaUndoEntry; + +typedef struct { + int id; // Unique ID for this text area + int cursor_pos; // Current cursor position in text + int selection_start; // Selection start (-1 if no selection) + int selection_end; // Selection end (-1 if no selection) + float scroll_offset_y; // Vertical scroll offset + float scroll_offset_x; // Horizontal scroll offset (for non-wrap mode) + boolean is_selecting; // Currently dragging to select + boolean is_initialized; // State has been initialized + + // Undo history + TextAreaUndoEntry *undo_stack; // Dowa array of undo entries + int undo_index; // Current position in undo stack + + // Internal tracking + double last_blink_time; + boolean cursor_visible; +} TextAreaState; + +static TextAreaState g_text_area_states[TEXT_AREA_MAX_INSTANCES] = {0}; +static int g_text_area_state_count = 0; +static char *g_clipboard_text = NULL; +static Dowa_Arena *g_text_area_arena = NULL; + +// Helper functions +static int TA_Min_Int(int a, int b) { return a < b ? a : b; } +static int TA_Max_Int(int a, int b) { return a > b ? a : b; } +static float TA_Min_Float(float a, float b) { return a < b ? a : b; } +static float TA_Max_Float(float a, float b) { return a > b ? a : b; } + +static TextAreaState* GetTextAreaState(int id) { + for (int i = 0; i < g_text_area_state_count; i++) { + if (g_text_area_states[i].id == id && g_text_area_states[i].is_initialized) { + return &g_text_area_states[i]; + } + } + return NULL; +} + +static TextAreaState* CreateTextAreaState(int id) { + if (g_text_area_state_count >= TEXT_AREA_MAX_INSTANCES) { + return &g_text_area_states[0]; // Reuse first slot + } + + TextAreaState *state = &g_text_area_states[g_text_area_state_count++]; + memset(state, 0, sizeof(TextAreaState)); + state->id = id; + state->selection_start = -1; + state->selection_end = -1; + state->undo_index = -1; + state->cursor_visible = TRUE; + state->is_initialized = TRUE; + return state; +} + +static void GetLineAndColumn(const char *text, int pos, int *out_line, int *out_column) { + int line = 0; + int column = 0; + for (int i = 0; i < pos && text[i] != '\0'; i++) { + if (text[i] == '\n') { + line++; + column = 0; + } else { + column++; + } + } + *out_line = line; + *out_column = column; +} + +static int GetPosFromLineColumn(const char *text, int line, int column) { + int current_line = 0; + int current_col = 0; + int i = 0; + + while (text[i] != '\0') { + if (current_line == line && current_col == column) { + return i; + } + if (text[i] == '\n') { + if (current_line == line) { + return i; + } + current_line++; + current_col = 0; + } else { + current_col++; + } + i++; + } + return i; +} + +static int GetLineStart(const char *text, int pos) { + int i = pos; + while (i > 0 && text[i - 1] != '\n') { + i--; + } + return i; +} + +static int GetLineEnd(const char *text, int pos) { + int i = pos; + int len = strlen(text); + while (i < len && text[i] != '\n') { + i++; + } + return i; +} + +static int CountLines(const char *text) { + int count = 1; + for (int i = 0; text[i] != '\0'; i++) { + if (text[i] == '\n') count++; + } + return count; +} + +static int MeasureTextRange(const char *text, int start, int end, int font_size) { + if (start >= end) return 0; + + char temp[1024]; + int len = TA_Min_Int(end - start, 1023); + strncpy(temp, text + start, len); + temp[len] = '\0'; + + return MeasureTextEx(GuiGetFont(), temp, font_size, TEXT_SIZE_DEFAULT/TEXT_AREA_FONT_SIZE).x; +} + +static int GetCharIndexFromPos(const char *text, Rectangle bounds, Vector2 pos, + boolean wrap, float scroll_y, int font_size, int line_height) { + if (!text || strlen(text) == 0) return 0; + + float content_x = bounds.x + TEXT_AREA_PADDING; + float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y; + float content_width = bounds.width - TEXT_AREA_PADDING * 2; + + int text_len = strlen(text); + + float click_line_y = (pos.y - content_y) / line_height; + int target_visual_line = (int)click_line_y; + if (target_visual_line < 0) target_visual_line = 0; + + int current_visual_line = 0; + int i = 0; + int line_char_start = 0; + + while (i <= text_len) { + boolean is_newline = (i < text_len && text[i] == '\n'); + + if (wrap && i > line_char_start) { + int line_width = MeasureTextRange(text, line_char_start, i, font_size); + if (line_width > content_width && i > line_char_start + 1) { + int wrap_pos = i - 1; + for (int j = i - 1; j > line_char_start; j--) { + if (text[j] == ' ') { + wrap_pos = j; + break; + } + } + + if (current_visual_line == target_visual_line) { + float click_x = pos.x - content_x; + int best_pos = line_char_start; + float best_dist = 99999; + + for (int k = line_char_start; k <= wrap_pos; k++) { + int char_x = MeasureTextRange(text, line_char_start, k, font_size); + float dist = (float)(click_x - char_x); + if (dist < 0) dist = -dist; + if (dist < best_dist) { + best_dist = dist; + best_pos = k; + } + } + return best_pos; + } + + current_visual_line++; + line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; + i = line_char_start; + continue; + } + } + + if (is_newline || i == text_len) { + if (current_visual_line == target_visual_line || i == text_len) { + float click_x = pos.x - content_x; + int line_end = i; + int best_pos = line_char_start; + float best_dist = 99999; + + for (int k = line_char_start; k <= line_end; k++) { + int char_x = MeasureTextRange(text, line_char_start, k, font_size); + float dist = (float)(click_x - char_x); + if (dist < 0) dist = -dist; + if (dist < best_dist) { + best_dist = dist; + best_pos = k; + } + } + return TA_Min_Int(best_pos, text_len); + } + + current_visual_line++; + line_char_start = i + 1; + } + + i++; + } + + return text_len; +} + +static Vector2 GetCursorScreenPos(const char *text, int cursor_pos, Rectangle bounds, + boolean wrap, float scroll_y, int font_size, int line_height) { + float content_x = bounds.x + TEXT_AREA_PADDING; + float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y; + float content_width = bounds.width - TEXT_AREA_PADDING * 2; + + if (!text || cursor_pos == 0) { + return (Vector2){content_x, content_y}; + } + + int text_len = strlen(text); + cursor_pos = TA_Min_Int(cursor_pos, text_len); + + int visual_line = 0; + int line_char_start = 0; + + for (int i = 0; i <= cursor_pos; i++) { + if (i == cursor_pos) { + float x = content_x + MeasureTextRange(text, line_char_start, cursor_pos, font_size); + float y = content_y + visual_line * line_height; + return (Vector2){x, y}; + } + + if (text[i] == '\n') { + visual_line++; + line_char_start = i + 1; + } else if (wrap) { + int line_width = MeasureTextRange(text, line_char_start, i + 1, font_size); + if (line_width > content_width && i > line_char_start) { + int wrap_pos = i; + for (int j = i; j > line_char_start; j--) { + if (text[j] == ' ') { + wrap_pos = j; + break; + } + } + + if (cursor_pos <= wrap_pos) { + float x = content_x + MeasureTextRange(text, line_char_start, cursor_pos, font_size); + float y = content_y + visual_line * line_height; + return (Vector2){x, y}; + } + + visual_line++; + line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; + } + } + } + + float x = content_x + MeasureTextRange(text, line_char_start, cursor_pos, font_size); + float y = content_y + visual_line * line_height; + return (Vector2){x, y}; +} + +static float GetContentHeight(const char *text, Rectangle bounds, boolean wrap, + int font_size, int line_height) { + if (!text || strlen(text) == 0) return line_height; + + float content_width = bounds.width - TEXT_AREA_PADDING * 2; + int visual_lines = 1; + int line_char_start = 0; + int text_len = strlen(text); + + for (int i = 0; i <= text_len; i++) { + if (i == text_len || text[i] == '\n') { + visual_lines++; + line_char_start = i + 1; + } else if (wrap) { + int line_width = MeasureTextRange(text, line_char_start, i + 1, font_size); + if (line_width > content_width && i > line_char_start) { + int wrap_pos = i; + for (int j = i; j > line_char_start; j--) { + if (text[j] == ' ') { + wrap_pos = j; + break; + } + } + visual_lines++; + line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; + } + } + } + + return visual_lines * line_height; +} + +static void PushUndoState(TextAreaState *state, const char *text, Dowa_Arena *arena) { + TextAreaUndoEntry entry; + entry.text = Dowa_String_Copy_Arena((char*)text, arena); + entry.cursor_pos = state->cursor_pos; + entry.selection_start = state->selection_start; + entry.selection_end = state->selection_end; + + if (state->undo_index < (int)Dowa_Array_Length(state->undo_stack) - 1) { + dowa__header(state->undo_stack)->length = state->undo_index + 1; + } + + Dowa_Array_Push_Arena(state->undo_stack, entry, arena); + state->undo_index = Dowa_Array_Length(state->undo_stack) - 1; + + if (Dowa_Array_Length(state->undo_stack) > TEXT_AREA_MAX_UNDO_STATES) { + for (int i = 0; i < (int)Dowa_Array_Length(state->undo_stack) - 1; i++) { + state->undo_stack[i] = state->undo_stack[i + 1]; + } + dowa__header(state->undo_stack)->length--; + state->undo_index--; + } +} + +static boolean PerformUndo(TextAreaState *state, char *text, int text_size) { + if (state->undo_index <= 0) return FALSE; + + state->undo_index--; + TextAreaUndoEntry *entry = &state->undo_stack[state->undo_index]; + + strncpy(text, entry->text, text_size - 1); + text[text_size - 1] = '\0'; + state->cursor_pos = entry->cursor_pos; + state->selection_start = entry->selection_start; + state->selection_end = entry->selection_end; + + return TRUE; +} + +static boolean PerformRedo(TextAreaState *state, char *text, int text_size) { + if (state->undo_index >= (int)Dowa_Array_Length(state->undo_stack) - 1) return FALSE; + + state->undo_index++; + TextAreaUndoEntry *entry = &state->undo_stack[state->undo_index]; + + strncpy(text, entry->text, text_size - 1); + text[text_size - 1] = '\0'; + state->cursor_pos = entry->cursor_pos; + state->selection_start = entry->selection_start; + state->selection_end = entry->selection_end; + + return TRUE; +} + +static void InsertTextAtCursor(char *text, int text_size, int cursor_pos, + const char *insert_text, int *new_cursor_pos) { + int text_len = strlen(text); + int insert_len = strlen(insert_text); + + if (text_len + insert_len >= text_size - 1) { + insert_len = text_size - 1 - text_len; + if (insert_len <= 0) return; + } + + memmove(text + cursor_pos + insert_len, + text + cursor_pos, + text_len - cursor_pos + 1); + + memcpy(text + cursor_pos, insert_text, insert_len); + + *new_cursor_pos = cursor_pos + insert_len; +} + +static void DeleteTextRange(char *text, int start, int end) { + if (start >= end) return; + int text_len = strlen(text); + if (start < 0) start = 0; + if (end > text_len) end = text_len; + + memmove(text + start, text + end, text_len - end + 1); +} + +static char* GetSelectedText(const char *text, int sel_start, int sel_end, Dowa_Arena *arena) { + if (sel_start < 0 || sel_end < 0 || sel_start >= sel_end) return NULL; + + int len = sel_end - sel_start; + char *result = Dowa_Arena_Allocate(arena, len + 1); + strncpy(result, text + sel_start, len); + result[len] = '\0'; + return result; +} + +boolean GuiTextArea(int id, Rectangle bounds, char *text, int text_size, boolean is_edit_mode, + boolean should_text_wrap, Dowa_Arena *arena) { + boolean should_toggle_edit_mode = FALSE; + + // Get or create state for this text area + TextAreaState *state = GetTextAreaState(id); + if (!state) { + state = CreateTextAreaState(id); + state->cursor_pos = strlen(text); + state->last_blink_time = GetTime(); + PushUndoState(state, text, arena); + } + + int text_len = strlen(text); + Vector2 mouse_pos = GetMousePosition(); + boolean mouse_in_bounds = CheckCollisionPointRec(mouse_pos, bounds); + + // Handle click to enter/exit edit mode + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + if (mouse_in_bounds && !is_edit_mode) { + should_toggle_edit_mode = TRUE; + } else if (!mouse_in_bounds && is_edit_mode) { + should_toggle_edit_mode = TRUE; + } + } + + // Content area + float content_height = GetContentHeight(text, bounds, should_text_wrap, + TEXT_AREA_FONT_SIZE, TEXT_AREA_LINE_HEIGHT); + float visible_height = bounds.height - TEXT_AREA_PADDING * 2; + float max_scroll = TA_Max_Float(0, content_height - visible_height); + + // Handle scrolling + float wheel = GetMouseWheelMove(); + if (mouse_in_bounds && wheel != 0) { + state->scroll_offset_y -= wheel * TEXT_AREA_LINE_HEIGHT * 3; + state->scroll_offset_y = TA_Max_Float(0, TA_Min_Float(state->scroll_offset_y, max_scroll)); + } + + if (is_edit_mode) { + boolean ctrl_pressed = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) || + IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER); + boolean shift_pressed = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); + boolean text_changed = FALSE; + + double current_time = GetTime(); + if (current_time - state->last_blink_time > 0.5) { + state->cursor_visible = !state->cursor_visible; + state->last_blink_time = current_time; + } + + // Mouse Selection + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && mouse_in_bounds) { + int click_pos = GetCharIndexFromPos(text, bounds, mouse_pos, should_text_wrap, + state->scroll_offset_y, TEXT_AREA_FONT_SIZE, + TEXT_AREA_LINE_HEIGHT); + state->cursor_pos = click_pos; + state->selection_start = -1; + state->selection_end = -1; + state->is_selecting = TRUE; + state->cursor_visible = TRUE; + state->last_blink_time = current_time; + } + + if (state->is_selecting && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { + int drag_pos = GetCharIndexFromPos(text, bounds, mouse_pos, should_text_wrap, + state->scroll_offset_y, TEXT_AREA_FONT_SIZE, + TEXT_AREA_LINE_HEIGHT); + if (drag_pos != state->cursor_pos) { + if (state->selection_start < 0) { + state->selection_start = state->cursor_pos; + } + state->selection_end = drag_pos; + } + } + + if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { + state->is_selecting = FALSE; + if (state->selection_start >= 0 && state->selection_end >= 0) { + if (state->selection_start > state->selection_end) { + int temp = state->selection_start; + state->selection_start = state->selection_end; + state->selection_end = temp; + } + if (state->selection_start == state->selection_end) { + state->selection_start = -1; + state->selection_end = -1; + } + } + } + + // Ctrl+A: Select All + if (ctrl_pressed && IsKeyPressed(KEY_A)) { + state->selection_start = 0; + state->selection_end = text_len; + state->cursor_pos = text_len; + } + + // Ctrl+C: Copy + if (ctrl_pressed && IsKeyPressed(KEY_C)) { + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + char *selected = GetSelectedText(text, sel_min, sel_max, arena); + if (selected) { + if (g_clipboard_text) free(g_clipboard_text); + g_clipboard_text = strdup(selected); + SetClipboardText(g_clipboard_text); + } + } + } + + // Ctrl+X: Cut + if (ctrl_pressed && IsKeyPressed(KEY_X)) { + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + char *selected = GetSelectedText(text, sel_min, sel_max, arena); + if (selected) { + if (g_clipboard_text) free(g_clipboard_text); + g_clipboard_text = strdup(selected); + SetClipboardText(g_clipboard_text); + } + + PushUndoState(state, text, arena); + DeleteTextRange(text, sel_min, sel_max); + state->cursor_pos = sel_min; + state->selection_start = -1; + state->selection_end = -1; + text_changed = TRUE; + } + } + + // Ctrl+V: Paste + if (ctrl_pressed && IsKeyPressed(KEY_V)) { + const char *clipboard = GetClipboardText(); + if (clipboard && strlen(clipboard) > 0) { + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + PushUndoState(state, text, arena); + DeleteTextRange(text, sel_min, sel_max); + state->cursor_pos = sel_min; + state->selection_start = -1; + state->selection_end = -1; + } else { + PushUndoState(state, text, arena); + } + + int new_cursor; + InsertTextAtCursor(text, text_size, state->cursor_pos, clipboard, &new_cursor); + state->cursor_pos = new_cursor; + text_changed = TRUE; + } + } + + // Ctrl+Z: Undo / Ctrl+Shift+Z: Redo + if (ctrl_pressed && IsKeyPressed(KEY_Z)) { + if (shift_pressed) { + PerformRedo(state, text, text_size); + } else { + PerformUndo(state, text, text_size); + } + } + + // Arrow Keys Navigation + if (IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) { + if (state->selection_start >= 0 && !shift_pressed) { + state->cursor_pos = TA_Min_Int(state->selection_start, state->selection_end); + state->selection_start = -1; + state->selection_end = -1; + } else if (state->cursor_pos > 0) { + if (shift_pressed) { + if (state->selection_start < 0) { + state->selection_start = state->cursor_pos; + state->selection_end = state->cursor_pos; + } + state->cursor_pos--; + state->selection_end = state->cursor_pos; + } else { + state->cursor_pos--; + } + } + state->cursor_visible = TRUE; + state->last_blink_time = current_time; + } + + if (IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) { + if (state->selection_start >= 0 && !shift_pressed) { + state->cursor_pos = TA_Max_Int(state->selection_start, state->selection_end); + state->selection_start = -1; + state->selection_end = -1; + } else if (state->cursor_pos < text_len) { + if (shift_pressed) { + if (state->selection_start < 0) { + state->selection_start = state->cursor_pos; + state->selection_end = state->cursor_pos; + } + state->cursor_pos++; + state->selection_end = state->cursor_pos; + } else { + state->cursor_pos++; + } + } + state->cursor_visible = TRUE; + state->last_blink_time = current_time; + } + + if (IsKeyPressed(KEY_UP) || IsKeyPressedRepeat(KEY_UP)) { + int line, col; + GetLineAndColumn(text, state->cursor_pos, &line, &col); + if (line > 0) { + int new_pos = GetPosFromLineColumn(text, line - 1, col); + if (shift_pressed) { + if (state->selection_start < 0) { + state->selection_start = state->cursor_pos; + state->selection_end = state->cursor_pos; + } + state->selection_end = new_pos; + } else { + state->selection_start = -1; + state->selection_end = -1; + } + state->cursor_pos = new_pos; + } + state->cursor_visible = TRUE; + state->last_blink_time = current_time; + } + + if (IsKeyPressed(KEY_DOWN) || IsKeyPressedRepeat(KEY_DOWN)) { + int line, col; + GetLineAndColumn(text, state->cursor_pos, &line, &col); + int total_lines = CountLines(text); + if (line < total_lines - 1) { + int new_pos = GetPosFromLineColumn(text, line + 1, col); + if (shift_pressed) { + if (state->selection_start < 0) { + state->selection_start = state->cursor_pos; + state->selection_end = state->cursor_pos; + } + state->selection_end = new_pos; + } else { + state->selection_start = -1; + state->selection_end = -1; + } + state->cursor_pos = new_pos; + } + state->cursor_visible = TRUE; + state->last_blink_time = current_time; + } + + // Home/End keys + if (IsKeyPressed(KEY_HOME)) { + int line_start = GetLineStart(text, state->cursor_pos); + if (shift_pressed) { + if (state->selection_start < 0) { + state->selection_start = state->cursor_pos; + state->selection_end = state->cursor_pos; + } + state->selection_end = line_start; + } else { + state->selection_start = -1; + state->selection_end = -1; + } + state->cursor_pos = line_start; + } + + if (IsKeyPressed(KEY_END)) { + int line_end = GetLineEnd(text, state->cursor_pos); + if (shift_pressed) { + if (state->selection_start < 0) { + state->selection_start = state->cursor_pos; + state->selection_end = state->cursor_pos; + } + state->selection_end = line_end; + } else { + state->selection_start = -1; + state->selection_end = -1; + } + state->cursor_pos = line_end; + } + + // Text Input + if (!ctrl_pressed) { + int key = GetCharPressed(); + while (key > 0) { + if (key >= 32 && key <= 126) { + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + PushUndoState(state, text, arena); + DeleteTextRange(text, sel_min, sel_max); + state->cursor_pos = sel_min; + state->selection_start = -1; + state->selection_end = -1; + text_changed = TRUE; + } else if (!text_changed) { + PushUndoState(state, text, arena); + } + + char insert_str[2] = {(char)key, '\0'}; + int new_cursor; + InsertTextAtCursor(text, text_size, state->cursor_pos, insert_str, &new_cursor); + state->cursor_pos = new_cursor; + text_changed = TRUE; + } + key = GetCharPressed(); + } + } + + // Enter key + if (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER)) { + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + PushUndoState(state, text, arena); + DeleteTextRange(text, sel_min, sel_max); + state->cursor_pos = sel_min; + state->selection_start = -1; + state->selection_end = -1; + } else { + PushUndoState(state, text, arena); + } + + int new_cursor; + InsertTextAtCursor(text, text_size, state->cursor_pos, "\n", &new_cursor); + state->cursor_pos = new_cursor; + text_changed = TRUE; + } + + // Backspace + if (IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) { + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + PushUndoState(state, text, arena); + DeleteTextRange(text, sel_min, sel_max); + state->cursor_pos = sel_min; + state->selection_start = -1; + state->selection_end = -1; + text_changed = TRUE; + } else if (state->cursor_pos > 0) { + PushUndoState(state, text, arena); + DeleteTextRange(text, state->cursor_pos - 1, state->cursor_pos); + state->cursor_pos--; + text_changed = TRUE; + } + } + + // Delete key + if (IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) { + text_len = strlen(text); + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + PushUndoState(state, text, arena); + DeleteTextRange(text, sel_min, sel_max); + state->cursor_pos = sel_min; + state->selection_start = -1; + state->selection_end = -1; + text_changed = TRUE; + } else if (state->cursor_pos < text_len) { + PushUndoState(state, text, arena); + DeleteTextRange(text, state->cursor_pos, state->cursor_pos + 1); + text_changed = TRUE; + } + } + + // Tab key + if (IsKeyPressed(KEY_TAB)) { + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + PushUndoState(state, text, arena); + DeleteTextRange(text, sel_min, sel_max); + state->cursor_pos = sel_min; + state->selection_start = -1; + state->selection_end = -1; + } else { + PushUndoState(state, text, arena); + } + + int new_cursor; + InsertTextAtCursor(text, text_size, state->cursor_pos, " ", &new_cursor); + state->cursor_pos = new_cursor; + text_changed = TRUE; + } + + // Auto-scroll to keep cursor visible + Vector2 cursor_screen = GetCursorScreenPos(text, state->cursor_pos, bounds, + should_text_wrap, state->scroll_offset_y, + TEXT_AREA_FONT_SIZE, TEXT_AREA_LINE_HEIGHT); + + float visible_top = bounds.y + TEXT_AREA_PADDING; + float visible_bottom = bounds.y + bounds.height - TEXT_AREA_PADDING - TEXT_AREA_LINE_HEIGHT; + + if (cursor_screen.y < visible_top) { + state->scroll_offset_y -= visible_top - cursor_screen.y; + } else if (cursor_screen.y > visible_bottom) { + state->scroll_offset_y += cursor_screen.y - visible_bottom; + } + + state->scroll_offset_y = TA_Max_Float(0, TA_Min_Float(state->scroll_offset_y, max_scroll)); + } + + // Drawing + DrawRectangleRec(bounds, is_edit_mode ? DARKGRAY : (Color){40, 40, 40, 255}); + DrawRectangleLinesEx(bounds, 1, is_edit_mode ? WHITE : GRAY); + + BeginScissorMode((int)bounds.x, (int)bounds.y, (int)bounds.width, (int)bounds.height); + + float content_x = bounds.x + TEXT_AREA_PADDING; + float content_y = bounds.y + TEXT_AREA_PADDING - state->scroll_offset_y; + float content_width = bounds.width - TEXT_AREA_PADDING * 2; + + text_len = strlen(text); + + // Draw selection highlight + if (state->selection_start >= 0 && state->selection_end >= 0 && + state->selection_start != state->selection_end) { + int sel_min = TA_Min_Int(state->selection_start, state->selection_end); + int sel_max = TA_Max_Int(state->selection_start, state->selection_end); + + int line_char_start = 0; + int visual_line = 0; + + for (int i = 0; i <= text_len; i++) { + boolean is_end = (i == text_len); + boolean is_newline = (!is_end && text[i] == '\n'); + boolean should_draw_line = is_end || is_newline; + + if (should_text_wrap && !is_end && !is_newline) { + int line_width = MeasureTextRange(text, line_char_start, i + 1, TEXT_AREA_FONT_SIZE); + if (line_width > content_width && i > line_char_start) { + should_draw_line = TRUE; + } + } + + if (should_draw_line) { + int line_end = i; + + if (sel_min < line_end && sel_max > line_char_start) { + int highlight_start = TA_Max_Int(sel_min, line_char_start); + int highlight_end = TA_Min_Int(sel_max, line_end); + + float x1 = content_x + MeasureTextRange(text, line_char_start, highlight_start, TEXT_AREA_FONT_SIZE); + float x2 = content_x + MeasureTextRange(text, line_char_start, highlight_end, TEXT_AREA_FONT_SIZE); + float y = content_y + visual_line * TEXT_AREA_LINE_HEIGHT; + + DrawRectangle((int)x1, (int)y, (int)(x2 - x1), TEXT_AREA_LINE_HEIGHT, + Fade(SKYBLUE, 0.5f)); + } + + visual_line++; + line_char_start = i + 1; + } + } + } + + // Draw text with wrapping support + if (should_text_wrap) { + int line_char_start = 0; + int visual_line = 0; + + for (int i = 0; i <= text_len; i++) { + boolean is_end = (i == text_len); + boolean is_newline = (!is_end && text[i] == '\n'); + boolean should_draw_line = is_end || is_newline; + + if (!is_end && !is_newline) { + int line_width = MeasureTextRange(text, line_char_start, i + 1, TEXT_AREA_FONT_SIZE); + if (line_width > content_width && i > line_char_start) { + should_draw_line = TRUE; + } + } + + if (should_draw_line && i > line_char_start) { + char line_buffer[1024]; + int line_len = TA_Min_Int(i - line_char_start, 1023); + strncpy(line_buffer, text + line_char_start, line_len); + line_buffer[line_len] = '\0'; + + Vector2 draw_text_vector = { + .x = content_x, + .y = content_y + visual_line * TEXT_AREA_LINE_HEIGHT + }; + DrawTextEx(GuiGetFont(), line_buffer, draw_text_vector, + TEXT_AREA_FONT_SIZE, TEXT_SIZE_DEFAULT/TEXT_AREA_FONT_SIZE, WHITE); + + visual_line++; + line_char_start = is_newline ? i + 1 : i; + } else if (is_newline) { + visual_line++; + line_char_start = i + 1; + } + } + } else { + int line_start = 0; + int visual_line = 0; + + for (int i = 0; i <= text_len; i++) { + if (i == text_len || text[i] == '\n') { + if (i > line_start) { + char line_buffer[1024]; + int line_len = TA_Min_Int(i - line_start, 1023); + strncpy(line_buffer, text + line_start, line_len); + line_buffer[line_len] = '\0'; + + Vector2 draw_text_vector = { + .x = content_x, + .y = content_y + visual_line * TEXT_AREA_LINE_HEIGHT + }; + DrawTextEx(GuiGetFont(), line_buffer, draw_text_vector , + TEXT_AREA_FONT_SIZE, TEXT_SIZE_DEFAULT/TEXT_AREA_FONT_SIZE, WHITE); + } + visual_line++; + line_start = i + 1; + } + } + } + + // Draw cursor + if (is_edit_mode && state->cursor_visible) { + Vector2 cursor_pos = GetCursorScreenPos(text, state->cursor_pos, bounds, + should_text_wrap, state->scroll_offset_y, + TEXT_AREA_FONT_SIZE, TEXT_AREA_LINE_HEIGHT); + + DrawRectangle((int)cursor_pos.x, (int)cursor_pos.y, + TEXT_AREA_CURSOR_WIDTH, TEXT_AREA_LINE_HEIGHT, WHITE); + } + + EndScissorMode(); + + // Draw scrollbar if needed + if (max_scroll > 0) { + float scrollbar_height = (visible_height / content_height) * visible_height; + float scrollbar_y = bounds.y + TEXT_AREA_PADDING + + (state->scroll_offset_y / max_scroll) * (visible_height - scrollbar_height); + + DrawRectangle((int)(bounds.x + bounds.width - 8), (int)scrollbar_y, + 6, (int)scrollbar_height, Fade(WHITE, 0.3f)); + } + + return should_toggle_edit_mode; +} + +void GuiTextAreaResetState(int id) { + TextAreaState *state = GetTextAreaState(id); + if (state) { + if (state->undo_stack) { + Dowa_Array_Free(state->undo_stack); + state->undo_stack = NULL; + } + state->is_initialized = FALSE; + } +} + +void GuiTextAreaResetAllStates(void) { + for (int i = 0; i < g_text_area_state_count; i++) { + if (g_text_area_states[i].undo_stack) { + Dowa_Array_Free(g_text_area_states[i].undo_stack); + g_text_area_states[i].undo_stack = NULL; + } + g_text_area_states[i].is_initialized = FALSE; + } + g_text_area_state_count = 0; +} + +// ============================================================================ +// End TextArea Component +// ============================================================================ + typedef Dowa_KV(char*, char*) INPUT_HASHMAP; typedef struct { @@ -48,7 +1038,7 @@ typedef struct { char *filename; - char *title; + char *title; Rectangle rect; long time_modified; boolean deleted; @@ -68,6 +1058,13 @@ TAB_LENGTH } PostDog_Tab_Enum; +// Text area IDs +#define TEXT_AREA_ID_INPUT_HEADER 1 +#define TEXT_AREA_ID_INPUT_BODY 2 +#define TEXT_AREA_ID_INPUT_PARAMS 3 +#define TEXT_AREA_ID_INPUT_WS 4 +#define TEXT_AREA_ID_RESULT 5 + static uint32 counter = 0; HistoryItem *history_items = NULL; HistoryItem *new_history_items = NULL; @@ -80,7 +1077,9 @@ int active_input_tab = 0; Seobeo_WebSocket *ws = NULL; boolean WS_BREAK = FALSE; +pthread_t websocket_thread_id; Color TEXT_COLOR = BLACK; +boolean LOADING = FALSE; int CompareHistoryItemsByDate(const void *a, const void *b) { HistoryItem *itemA = (HistoryItem *)a; @@ -187,6 +1186,8 @@ return (InArea(mouse_position, area) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)); } +// -------- END of UI ---- // + char *PostDog_Enum_To_String(int active_enum) { switch(active_enum) @@ -226,7 +1227,7 @@ size_t new_file_size = 1024 * 1024; Dowa_Arena *arena = Dowa_Arena_Create(1024 * 1024 * 2); char *new_file = Dowa_Arena_Allocate(arena, 1024 * 1024); - char *title = malloc(strlen(method) + strlen(url_input_text) + 2); + char *title = Dowa_Arena_Allocate(arena, strlen(method) + strlen(url_input_text) + 2); sprintf(title, "%s %s", method, url_input_text); snprintf( new_file, @@ -259,15 +1260,15 @@ if (!filename) { perror("Error opening file"); - exit(EXIT_FAILURE); + exit(EXIT_FAILURE); } char *uuid4 = (char *)Dowa_Arena_Allocate(arena, 37); if (!uuid4) { perror("Error uuid"); - exit(EXIT_FAILURE); + exit(EXIT_FAILURE); } - + int32 seed = (uint32)time(NULL) ^ counter++; Dowa_String_UUID(seed, uuid4); snprintf(filename, 1024, "%s.txt", uuid4); @@ -275,24 +1276,15 @@ HistoryItem item = {0}; item.filename = strdup(filename); - item.title = title; + item.title = strdup(title); item.deleted = FALSE; + Dowa_Array_Push(new_history_items, item); - Dowa_Arena_Free(arena); } -int PostDog_Websocket_Send(void) +void PostDog_Websocket_Listen(void) { - if (!ws) - ws = Seobeo_WebSocket_Connect(url_input_text); - - printf("URL %s\n", url_input_text); - if (Seobeo_WebSocket_Send_Text(ws, input_body_array[active_input_tab]) < 0) - printf("Failed to send message\n"); - - printf("Receiving responses...\n"); - int received = 0; while (TRUE) { if (WS_BREAK) break; @@ -302,50 +1294,72 @@ { if (p_msg->opcode == SEOBEO_WS_OPCODE_TEXT) { - printf("Response %d: %.*s\n", received + 1, (int)p_msg->length, (char*)p_msg->data); - snprintf(result_text + strlen(result_text), RESULT_BUFFER_LENGTH - strlen(result_text), + printf("Response: %.*s\n", (int)p_msg->length, (char*)p_msg->data); + snprintf(result_text + strlen(result_text), RESULT_BUFFER_LENGTH - strlen(result_text), "\n%s", (char*)p_msg->data); - received++; } Seobeo_WebSocket_Message_Destroy(p_msg); } usleep(10000); + printf("Listening\n"); } - printf("Received %d/%d messages\n", received, 3); - Seobeo_WebSocket_Destroy(ws); + return; +} + +void PostDog_Websocket_Connect(void) +{ + ws = Seobeo_WebSocket_Connect(url_input_text); + memset(result_text, 0, strlen(result_text)); } -void *PostDog_Websocket_Thread(void *arg) +int PostDog_Websocket_Send(void) { - PostDog_Websocket_Send(); - printf("Websocket request finished.\n"); + if (Seobeo_WebSocket_Send_Text(ws, input_body_array[active_input_tab]) < 0) + snprintf(result_text + strlen(result_text), RESULT_BUFFER_LENGTH - strlen(result_text), + "Failed to send message\n"); + else + snprintf(result_text + strlen(result_text), RESULT_BUFFER_LENGTH - strlen(result_text), + "\n%s", input_body_array[active_input_tab]); +} + +void PostDog_Websocket_Destroy(pthread_t thread_id) +{ + Seobeo_WebSocket_Destroy(ws); + pthread_detach(thread_id); +} + +void *PostDog_Websocket_Start(void *arg) +{ + PostDog_Websocket_Connect(); + PostDog_Websocket_Listen(); return NULL; } -void PostDog_Websocket_Thread_Send() +pthread_t PostDog_Websocket_Start_Thread() { pthread_t thread_id; - - if (pthread_create(&thread_id, NULL, PostDog_Websocket_Thread, NULL) != 0) + + if (pthread_create(&thread_id, NULL, PostDog_Websocket_Start, NULL) != 0) { perror("Failed to create thread"); - return; + return 0; } - pthread_detach(thread_id); + + return thread_id; } int PostDog_Http_Request(void) { Seobeo_Client_Request *req = Seobeo_Client_Request_Create(url_input_text); Seobeo_Client_Response *res; - switch (active_method_dropdown) + switch (active_method_dropdown) { case 0: { Seobeo_Client_Request_Set_Method(req, "GET"); break; } - case 1: + case 1: { Seobeo_Client_Request_Set_Method(req, "POST"); break; @@ -375,7 +1389,7 @@ line = strtok(NULL, "\n"); } - } + } Seobeo_Client_Request_Set_Follow_Redirects(req, TRUE, 5); // TODO: remove magic number; res = Seobeo_Client_Request_Execute(req); @@ -391,6 +1405,26 @@ return 0; } +void *PostDog_Http_Thread(void *arg) +{ + PostDog_Http_Request(); + printf("HTTP request finished.\n"); + LOADING = FALSE; + return NULL; +} + +void PostDog_Http_Thread_Request() +{ + pthread_t thread_id; + LOADING = TRUE; + if (pthread_create(&thread_id, NULL, PostDog_Http_Thread, NULL) != 0) + { + perror("Failed to create thread"); + return; + } + pthread_detach(thread_id); +} + void PostDog_Update_URL(void) { // Save existing query string if present @@ -450,6 +1484,9 @@ active_method_dropdown = 0; for (int i = 0; i < Dowa_Array_Length(input_body_array); i++) input_body_array[i][0] = '\0'; + + // Reset text area states when clearing + GuiTextAreaResetAllStates(); } void PostDog_Load_File(const char *filename) @@ -513,6 +1550,9 @@ } } + // Reset text area states when loading new file + GuiTextAreaResetAllStates(); + Dowa_Arena_Free(init_arena); Dowa_Arena_Free(split_arena); } @@ -623,9 +1663,12 @@ Image logo_original = LoadImage("postdog/epi_all_colors.png"); ImageResize(&logo_original, 60, 60); SetWindowIcon(logo_original); - Texture2D logo_texture = LoadTextureFromImage(logo_original); + Texture2D logo_texture = LoadTextureFromImage(logo_original); UnloadImage(logo_original); + // Arena for text area undo states + g_text_area_arena = Dowa_Arena_Create(1024 * 1024 * 8); // 8MB for undo states + // -- Starting pos ---// Rectangle history_sidebar_rect = { 0 }; Dowa_Array_Reserve(history_items, 10); @@ -654,6 +1697,7 @@ snprintf(input_body_array[TAB_BODY], HEADER_BUFFER_LENGTH, ""); result_text = (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH); + result_text[0] = '\0'; bool method_edit = false; @@ -664,11 +1708,12 @@ Rectangle input_tab_rect = { 0 }; Rectangle input_tab_item_rect = { 0 }; Rectangle input_body_rect = { 0 }; - bool input_body_bool = false; + bool input_body_edit_mode = false; // -- result --// Rectangle result_area_rect = { 0 }; Rectangle result_body_rect = { 0 }; + bool result_body_edit_mode = false; // General styling. float padding = 10; // TODO make it % based? @@ -676,8 +1721,6 @@ // Scroll offsets float history_scroll_offset = 0; - float input_body_scroll_offset = 0; - float result_body_scroll_offset = 0; while (!WindowShouldClose()) { @@ -699,11 +1742,11 @@ .x = history_sidebar_rect.x, .y = history_sidebar_rect.y, .width = history_sidebar_rect.width, - .height = 80 + .height = 80 }; Rectangle history_list_area_rect = Below(logo_area_rect, padding); - history_list_area_rect.x += padding; + history_list_area_rect.x += padding; history_list_area_rect.width = history_sidebar_rect.width - (2 * padding); history_list_area_rect.height = history_sidebar_rect.height - logo_area_rect.height - padding; @@ -716,7 +1759,7 @@ for (int i = 0; i < total; i++) { HistoryItem *curr_history_items = i < new_history_items_length ? - &new_history_items[i - new_history_items_length - 1] : &history_items[i - new_history_items_length]; + &new_history_items[new_history_items_length - i - 1] : &history_items[i - new_history_items_length]; if (curr_history_items->deleted) { @@ -794,10 +1837,9 @@ Vector2 mouse_position = GetMousePosition(); float mouse_wheel = GetMouseWheelMove(); - // Reset input body scroll when tab changes + // Reset text area state when tab changes if (prev_input_tab != active_input_tab) { - input_body_scroll_offset = 0; prev_input_tab = active_input_tab; } @@ -810,18 +1852,22 @@ if (history_scroll_offset < -max_scroll && max_scroll > 0) history_scroll_offset = -max_scroll; } - // Handle scroll wheel for input body - if (InArea(mouse_position, input_body_rect) && mouse_wheel != 0) { - input_body_scroll_offset += mouse_wheel * 30; - if (input_body_scroll_offset > 0) input_body_scroll_offset = 0; - } + // Reset + SetMouseCursor(MOUSE_CURSOR_DEFAULT); - // Handle scroll wheel for result body - if (InArea(mouse_position, result_body_rect) && mouse_wheel != 0) { - result_body_scroll_offset += mouse_wheel * 30; - if (result_body_scroll_offset > 0) result_body_scroll_offset = 0; - } + // TODO: Move all for loop rect up here so it does not flicker. + if ( + InArea(mouse_position, result_area_rect) || + InArea(mouse_position, input_tab_rect) || + InArea(mouse_position, url_input_bounds_rect) || + InArea(mouse_position, url_enter_button_rect) || + InArea(mouse_position, method_dropdown_rect) || + InArea(mouse_position, logo_area_rect) + ) + SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); + if (Clicked(mouse_position, logo_area_rect)) + PostDog_Params_Reset(); BeginDrawing(); ClearBackground(GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR))); @@ -845,8 +1891,8 @@ BeginScissorMode(history_list_area_rect.x, history_list_area_rect.y, history_list_area_rect.width, history_list_area_rect.height); for (int i = 0; i < total; i++) { - HistoryItem *curr_history_items = i < new_history_items_length ? - &new_history_items[i - new_history_items_length - 1] : &history_items[i - new_history_items_length]; + HistoryItem *curr_history_items = i < new_history_items_length ? + &new_history_items[new_history_items_length - i - 1] : &history_items[i - new_history_items_length]; if (curr_history_items->deleted) continue; @@ -855,7 +1901,7 @@ DrawRectangleRounded(curr_history_items->rect, 0.5, 1, Fade(BLACK, 0.1f)); Rectangle filename_area_rect = curr_history_items->rect; - filename_area_rect.height -= diff; + filename_area_rect.height -= diff; Rectangle icon_area = Below(filename_area_rect, 0); icon_area.height = diff; @@ -866,7 +1912,14 @@ Rectangle icon_area_left_column = LeftColumn(icon_area, 0.5, 0); Rectangle icon_area_right_column = RightColumn(icon_area, icon_area_left_column, 0); - GuiDrawText(curr_history_items->title, AddPaddingHorizontal(curr_history_items->rect, padding), TEXT_ALIGN_MIDDLE, BLACK); + filename_area_rect.y += 2*padding; + GuiDrawText(curr_history_items->title, AddPadding(filename_area_rect, padding), TEXT_ALIGN_CENTER, BLACK); + if ( + InArea(mouse_position, icon_area_left_column) || + InArea(mouse_position, icon_area_right_column) + ) + SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); + if (GuiButton(AddPadding(icon_area_left_column, padding), "view")) PostDog_Load_File(curr_history_items->filename); if (GuiButton(AddPadding(icon_area_right_column, padding), "delete")) @@ -897,14 +1950,25 @@ } // URL area Rect - GuiDrawText("URL: ", url_text_bounds_rect, TEXT_ALIGN_CENTER, BLACK); + GuiDrawText("URL: ", url_text_bounds_rect, TEXT_ALIGN_CENTER, BLACK); DrawRectangleRec(url_area_rect, Fade(BLACK, 0.1f)); + if (GuiTextBox(url_input_bounds_rect, url_input_text, DEFAULT_TEXT_BUFFER_LENGTH, url_input_edit)) url_input_edit = !url_input_edit; + if (url_input_edit) + { + if (IsKeyPressed(KEY_ENTER)) + { + PostDog_Http_Thread_Request(); + url_input_edit = !url_input_edit; + } + } + sendRequest = GuiButton(url_enter_button_rect, "ENTER"); if (sendRequest) - PostDog_Http_Request(); + PostDog_Http_Thread_Request(); + if (GuiDropdownBox(method_dropdown_rect, "GET;POST;PUT;DELETE", &active_method_dropdown, method_edit)) method_edit = !method_edit; @@ -912,87 +1976,54 @@ DrawRectangleRec(input_area_rect, Fade(BLUE, 0.1f)); DrawRectangleRec(input_tab_rect, Fade(DARKBLUE, 0.1f)); GuiSetStyle(TOGGLE, GROUP_PADDING, 0); - GuiDrawRectangle(input_body_rect, 1, GetColor(GuiGetStyle(TEXTBOX, BORDER)), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED))); + + int text_area_id = TEXT_AREA_ID_INPUT_HEADER + active_input_tab; + if (GuiTextArea(text_area_id, input_body_rect, input_body_array[active_input_tab], + BODY_BUFFER_LENGTH, input_body_edit_mode, TRUE, g_text_area_arena)) + input_body_edit_mode = !input_body_edit_mode; - BeginScissorMode(input_body_rect.x, input_body_rect.y, input_body_rect.width, input_body_rect.height); - Rectangle scrolled_input_rect = AddPadding(input_body_rect, padding * 2); - scrolled_input_rect.y += input_body_scroll_offset; - scrolled_input_rect.height = MAX_SCROLL_HEIGHT; - if (active_input_tab != TAB_WEBSOCKET) - { - WS_BREAK = TRUE; - if (JUNE_GuiTextBox(scrolled_input_rect, input_body_array[active_input_tab], DEFAULT_TEXT_BUFFER_LENGTH, input_body_bool)) - input_body_bool = !input_body_bool; - } - else + if (active_input_tab != TAB_WEBSOCKET) + { + WS_BREAK = TRUE; + if (websocket_thread_id) + PostDog_Websocket_Destroy(websocket_thread_id); + } + else + { + WS_BREAK = FALSE; + Rectangle temp = { + .x = input_body_rect.x + input_body_rect.width - (3 * padding) - 100, + .y = input_body_rect.y + input_body_rect.height - (3 * padding) - 20, + .width = 100, + .height = 40 + }; + if (GuiButton(temp, "Send") || (input_body_edit_mode && IsKeyDown(KEY_LEFT_SHIFT) && IsKeyDown(KEY_ENTER))) { - boolean temp = true; - WS_BREAK = FALSE; - if (GuiTextInputBox( - input_body_rect, - "send message", ws != NULL ? "connected" : "start messaging", - "send", input_body_array[active_input_tab], BODY_BUFFER_LENGTH, &temp) == 1) - PostDog_Websocket_Thread_Send(); + if (!ws) + websocket_thread_id = PostDog_Websocket_Start_Thread(); + usleep(10000); + PostDog_Websocket_Send(); } - EndScissorMode(); - - if (input_body_scroll_offset < 0) { - float scrollbar_height = 10; - float scrollbar_y = input_body_rect.y - (input_body_scroll_offset / MAX_SCROLL_HEIGHT) * (input_body_rect.height - scrollbar_height); - Rectangle scrollbar_rect = { - input_body_rect.x + input_body_rect.width - 5, - scrollbar_y, - 5, - scrollbar_height - }; - DrawRectangleRec(scrollbar_rect, Fade(BLUE, 0.5f)); } + GuiToggleGroup(input_tab_item_rect, "Header;Body;Get Param;Websocket", &active_input_tab); - PostDog_Update_URL(); - // Result Rect - DrawRectangleRec(result_area_rect, Fade(GREEN, 0.1f)); - DrawRectangleRec(result_body_rect, Fade(DARKGREEN, 0.1f)); - - // Create scrollable result body with offset - BeginScissorMode(result_body_rect.x, result_body_rect.y, result_body_rect.width, result_body_rect.height); - Rectangle scrolled_result = result_body_rect; - scrolled_result.y += result_body_scroll_offset; - scrolled_result.height = MAX_SCROLL_HEIGHT; - GuiTextBoxMulti(scrolled_result, result_text, RESULT_BUFFER_LENGTH, false); - EndScissorMode(); + // TODO: Add animations. + DrawRectangleRec(result_area_rect, LOADING ? Fade(RED, 0.1f) : Fade(GREEN, 0.1f)); + boolean result_toggle = GuiTextArea(TEXT_AREA_ID_RESULT, result_body_rect, result_text, + RESULT_BUFFER_LENGTH, result_body_edit_mode, TRUE, g_text_area_arena); - if (result_body_scroll_offset < 0) - { - float scrollbar_height = 10; - float scrollbar_y = result_body_rect.y - (result_body_scroll_offset / MAX_SCROLL_HEIGHT) * (result_body_rect.height - scrollbar_height); - Rectangle scrollbar_rect = { - result_body_rect.x + result_body_rect.width - 5, - scrollbar_y, - 5, - scrollbar_height - }; - DrawRectangleRec(scrollbar_rect, Fade(GREEN, 0.5f)); - } + GuiToggleGroup(input_tab_item_rect, "Header;Body;Get Param;Websocket", &active_input_tab); + if (result_toggle) + result_body_edit_mode = !result_body_edit_mode; - if (url_input_edit && (IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C)) - { - SetClipboardText(url_input_text); - } - else if (input_body_bool && (IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C)) - { - SetClipboardText(input_body_array[active_input_tab]); - } - else if (InArea(mouse_position, result_body_rect)) - { - DrawRectangleRec(result_body_rect, Fade(GREEN, 0.3f)); - if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C)) - SetClipboardText(result_text); - } EndDrawing(); } + + GuiTextAreaResetAllStates(); + Dowa_Arena_Free(g_text_area_arena); CloseWindow(); return 0; } diff -r 1c0878eb17de -r 05cf9467a1c3 tags --- a/tags Wed Jan 14 07:59:19 2026 -0800 +++ b/tags Wed Jan 14 08:56:33 2026 -0800 @@ -2197,6 +2197,8 @@ GuiWindowFileDialog postdog/gui_window_file_dialog.h /^void GuiWindowFileDialog(GuiWindowFileDialogState *state);$/;" p typeref:typename:void GuiWindowFileDialogState postdog/gui_window_file_dialog.h /^} GuiWindowFileDialogState;$/;" t typeref:struct:__anoncb6fd9740108 HASH_KEY_NUMBER dowa/dowa.h /^#define HASH_KEY_NUMBER /;" d +HAS_SELECTION third_party/raylib/include/raygui.h /^ #define HAS_SELECTION(/;" d +HAS_SELECTION third_party/raylib/include/raygui.h /^ #undef HAS_SELECTION$/;" d HTTP_BAD_REQUEST seobeo/seobeo.h /^#define HTTP_BAD_REQUEST /;" d HTTP_CREATED seobeo/seobeo.h /^#define HTTP_CREATED /;" d HTTP_FORBIDDEN seobeo/seobeo.h /^#define HTTP_FORBIDDEN /;" d @@ -2893,8 +2895,11 @@ JUNE_DrawRectangleLinesNoBottom third_party/raylib/include/raygui.h /^RAYGUIAPI void JUNE_DrawRectangleLinesNoBottom(Rectangle rect, float thickness, Color color);$/;" p typeref:typename:RAYGUIAPI void JUNE_DrawRectangleLinesNoBottom third_party/raylib/include/raygui.h /^void JUNE_DrawRectangleLinesNoBottom(Rectangle rect, float thickness, Color color) {$/;" f typeref:typename:void JUNE_GuiButton third_party/raylib/include/raygui.h /^int JUNE_GuiButton(Rectangle bounds, const char *text, int borderWidth, Color borderColor)$/;" f typeref:typename:int -JUNE_GuiTextBox third_party/raylib/include/raygui.h /^int JUNE_GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode)$/;" f typeref:typename:int +JUNE_GuiTextBoxEx third_party/raylib/include/raygui.h /^JUNE_TextBoxResult JUNE_GuiTextBoxEx(Rectangle bounds, char *text, int textSize, bool editMode)$/;" f typeref:typename:JUNE_TextBoxResult JUNE_HELPER_INTERNAL dowa/dowa_internal.h /^#define JUNE_HELPER_INTERNAL$/;" d +JUNE_MAX_VISUAL_LINES third_party/raylib/include/raygui.h /^ #define JUNE_MAX_VISUAL_LINES /;" d +JUNE_TextBoxResult third_party/raylib/include/raygui.h /^typedef struct JUNE_TextBoxResult {$/;" s +JUNE_TextBoxResult third_party/raylib/include/raygui.h /^} JUNE_TextBoxResult;$/;" t typeref:struct:JUNE_TextBoxResult KEY_A third_party/raylib/include/raylib.h /^ KEY_A = 65, \/\/ Key: A | a$/;" e enum:__anonc03ae25c0403 KEY_A third_party/raylib/raylib-5.5_linux_amd64/include/raylib.h /^ KEY_A = 65, \/\/ Key: A | a$/;" e enum:__anon9d42b9dd0403 KEY_A third_party/raylib/raylib-5.5_macos/include/raylib.h /^ KEY_A = 65, \/\/ Key: A | a$/;" e enum:__anon255619050403 @@ -3588,6 +3593,7 @@ MAGENTA third_party/raylib/raylib-5.5_linux_amd64/include/raylib.h /^#define MAGENTA /;" d MAGENTA third_party/raylib/raylib-5.5_macos/include/raylib.h /^#define MAGENTA /;" d MAGENTA third_party/raylib/raylib-5.5_win64/include/raylib.h /^#define MAGENTA /;" d +MARKDOWN_TO_HTML_H markdown_converter/markdown_to_html.h /^#define MARKDOWN_TO_HTML_H$/;" d MAROON third_party/raylib/include/raylib.h /^#define MAROON /;" d MAROON third_party/raylib/raylib-5.5_linux_amd64/include/raylib.h /^#define MAROON /;" d MAROON third_party/raylib/raylib-5.5_macos/include/raylib.h /^#define MAROON /;" d @@ -3654,6 +3660,9 @@ MAX_INT_16 seobeo/seobeo_internal.h /^#define MAX_INT_16 /;" d MAX_LINE_BUFFER_SIZE third_party/raylib/include/raygui.h /^ #define MAX_LINE_BUFFER_SIZE /;" d MAX_RESPONSE_SIZE mrjunejune/test/test.h /^#define MAX_RESPONSE_SIZE /;" d +MDAPI markdown_converter/markdown_to_html.h /^ #define MDAPI /;" d +MDAPI markdown_converter/markdown_to_html.h /^ #define MDAPI /;" d +MDAPI markdown_converter/markdown_to_html.h /^ #define MDAPI /;" d MIN third_party/raylib/include/rlgl.h /^ #define MIN(/;" d MIN third_party/raylib/raylib-5.5_linux_amd64/include/rlgl.h /^ #define MIN(/;" d MIN third_party/raylib/raylib-5.5_macos/include/rlgl.h /^ #define MIN(/;" d @@ -5435,6 +5444,10 @@ SCROLL_SLIDER_PADDING third_party/raylib/include/raygui.h /^ SCROLL_SLIDER_PADDING, \/\/ ScrollBar slider internal padding$/;" e enum:__anonbfe71a2a0c03 SCROLL_SLIDER_SIZE third_party/raylib/include/raygui.h /^ SCROLL_SLIDER_SIZE, \/\/ ScrollBar slider size$/;" e enum:__anonbfe71a2a0c03 SCROLL_SPEED third_party/raylib/include/raygui.h /^ SCROLL_SPEED, \/\/ ScrollBar scrolling speed$/;" e enum:__anonbfe71a2a0c03 +SELECTION_MAX third_party/raylib/include/raygui.h /^ #define SELECTION_MAX(/;" d +SELECTION_MAX third_party/raylib/include/raygui.h /^ #undef SELECTION_MAX$/;" d +SELECTION_MIN third_party/raylib/include/raygui.h /^ #define SELECTION_MIN(/;" d +SELECTION_MIN third_party/raylib/include/raygui.h /^ #undef SELECTION_MIN$/;" d SEOBEO_DEBUG seobeo/seobeo.h /^ SEOBEO_DEBUG,$/;" e enum:__anonc55223040103 SEOBEO_ERROR seobeo/seobeo.h /^ SEOBEO_ERROR,$/;" e enum:__anonc55223040103 SEOBEO_INFO seobeo/seobeo.h /^ SEOBEO_INFO = 0,$/;" e enum:__anonc55223040103 @@ -7549,6 +7562,7 @@ elementCount third_party/raylib/raylib-5.5_linux_amd64/include/rlgl.h /^ int elementCount; \/\/ Number of elements in the buffer (QUADS)$/;" m struct:rlVertexBuffer typeref:typename:int elementCount third_party/raylib/raylib-5.5_macos/include/rlgl.h /^ int elementCount; \/\/ Number of elements in the buffer (QUADS)$/;" m struct:rlVertexBuffer typeref:typename:int elementCount third_party/raylib/raylib-5.5_win64/include/rlgl.h /^ int elementCount; \/\/ Number of elements in the buffer (QUADS)$/;" m struct:rlVertexBuffer typeref:typename:int +emscripten.h markdown_converter/markdown_to_html.h /^ #include /;" h errno.h seobeo/seobeo.h /^#include /;" h events third_party/raylib/include/raylib.h /^ AutomationEvent *events; \/\/ Events entries$/;" m struct:AutomationEventList typeref:typename:AutomationEvent * events third_party/raylib/raylib-5.5_linux_amd64/include/raylib.h /^ AutomationEvent *events; \/\/ Events entries$/;" m struct:AutomationEventList typeref:typename:AutomationEvent * @@ -8089,6 +8103,9 @@ maps third_party/raylib/raylib-5.5_linux_amd64/include/raylib.h /^ MaterialMap *maps; \/\/ Material maps array (MAX_MATERIAL_MAPS)$/;" m struct:Material typeref:typename:MaterialMap * maps third_party/raylib/raylib-5.5_macos/include/raylib.h /^ MaterialMap *maps; \/\/ Material maps array (MAX_MATERIAL_MAPS)$/;" m struct:Material typeref:typename:MaterialMap * maps third_party/raylib/raylib-5.5_win64/include/raylib.h /^ MaterialMap *maps; \/\/ Material maps array (MAX_MATERIAL_MAPS)$/;" m struct:Material typeref:typename:MaterialMap * +markdown_free markdown_converter/markdown_to_html.h /^MDAPI void markdown_free(char *html);$/;" p typeref:typename:MDAPI void +markdown_get_length markdown_converter/markdown_to_html.h /^MDAPI size_t markdown_get_length(const char *html);$/;" p typeref:typename:MDAPI size_t +markdown_to_html markdown_converter/markdown_to_html.h /^MDAPI char *markdown_to_html(const char *markdown);$/;" p typeref:typename:MDAPI char * materialCount third_party/raylib/include/raylib.h /^ int materialCount; \/\/ Number of materials$/;" m struct:Model typeref:typename:int materialCount third_party/raylib/raylib-5.5_linux_amd64/include/raylib.h /^ int materialCount; \/\/ Number of materials$/;" m struct:Model typeref:typename:int materialCount third_party/raylib/raylib-5.5_macos/include/raylib.h /^ int materialCount; \/\/ Number of materials$/;" m struct:Model typeref:typename:int @@ -8309,6 +8326,7 @@ recs third_party/raylib/raylib-5.5_win64/include/raylib.h /^ Rectangle *recs; \/\/ Rectangles in texture for the glyphs$/;" m struct:Font typeref:typename:Rectangle * redirect_url seobeo/seobeo_internal.h /^ char *redirect_url;$/;" m struct:__anon7a4da8400708 typeref:typename:char * remaining dowa/stb_ds.h /^ size_t remaining;$/;" m struct:stbds_string_arena typeref:typename:size_t +result third_party/raylib/include/raygui.h /^ int result; \/\/ Original return value (0 = no change, 1 = mode changed)$/;" m struct:JUNE_TextBoxResult typeref:typename:int right third_party/raylib/include/raylib.h /^ int right; \/\/ Right border offset$/;" m struct:NPatchInfo typeref:typename:int right third_party/raylib/raylib-5.5_linux_amd64/include/raylib.h /^ int right; \/\/ Right border offset$/;" m struct:NPatchInfo typeref:typename:int right third_party/raylib/raylib-5.5_macos/include/raylib.h /^ int right; \/\/ Right border offset$/;" m struct:NPatchInfo typeref:typename:int @@ -9847,6 +9865,8 @@ scaleIn third_party/raylib/raylib-5.5_macos/include/raylib.h /^ float scaleIn[2]; \/\/ VR distortion scale in$/;" m struct:VrStereoConfig typeref:typename:float[2] scaleIn third_party/raylib/raylib-5.5_win64/include/raylib.h /^ float scaleIn[2]; \/\/ VR distortion scale in$/;" m struct:VrStereoConfig typeref:typename:float[2] seed dowa/stb_ds.h /^ size_t seed;$/;" m struct:__anon7f1219f40408 typeref:typename:size_t +selectionEnd third_party/raylib/include/raygui.h /^ int selectionEnd; \/\/ End index of selection (-1 if no selection)$/;" m struct:JUNE_TextBoxResult typeref:typename:int +selectionStart third_party/raylib/include/raygui.h /^ int selectionStart; \/\/ Start index of selection (-1 if no selection)$/;" m struct:JUNE_TextBoxResult typeref:typename:int seobeo/seobeo.h mrjunejune/test/test.h /^#include "seobeo\/seobeo.h"/;" h seobeo/seobeo.h seobeo/snapshot_creator.h /^#include "seobeo\/seobeo.h"/;" h seobeo/seobeo_internal.h seobeo/seobeo.h /^#include "seobeo\/seobeo_internal.h"/;" h @@ -10053,6 +10073,7 @@ stdbool.h third_party/raylib/raylib-5.5_win64/include/rlgl.h /^ #include /;" h stddef.h dowa/dowa.h /^#include /;" h stddef.h dowa/stb_ds.h /^#include /;" h +stddef.h markdown_converter/markdown_to_html.h /^#include /;" h stdio.h dowa/dowa.h /^#include /;" h stdio.h dowa/stb_ds.h /^#include /;" h stdio.h mrjunejune/test/test.h /^#include /;" h @@ -10183,6 +10204,9 @@ texcoordy third_party/raylib/raylib-5.5_macos/include/rlgl.h /^ float texcoordx, texcoordy; \/\/ Current active texture coordinate (added on glV/;" m struct:rlglData::__anon96c0c2130d08 typeref:typename:float texcoordy third_party/raylib/raylib-5.5_win64/include/rlgl.h /^ float texcoordx, texcoordy; \/\/ Current active texture coordinate (added on glV/;" m struct:rlglData::__anon9e7796b80d08 typeref:typename:float textBoxCursorIndex third_party/raylib/include/raygui.h /^static int textBoxCursorIndex = 0; \/\/ Cursor index, shared by all GuiTextBox*()$/;" v typeref:typename:int +textBoxSelecting third_party/raylib/include/raygui.h /^static bool textBoxSelecting = false; \/\/ Currently selecting with mouse$/;" v typeref:typename:bool +textBoxSelectionEnd third_party/raylib/include/raygui.h /^static int textBoxSelectionEnd = -1; \/\/ Selection end index (-1 if no selection)$/;" v typeref:typename:int +textBoxSelectionStart third_party/raylib/include/raygui.h /^static int textBoxSelectionStart = -1; \/\/ Selection start index (-1 if no selection)$/;" v typeref:typename:int text_copy seobeo/seobeo_internal.h /^ void *text_copy;$/;" m struct:__anon7a4da8400208 typeref:typename:void * texture third_party/raylib/include/raygui.h /^ Texture2D texture; \/\/ Texture atlas containing the glyphs$/;" m struct:Font typeref:typename:Texture2D texture third_party/raylib/include/raylib.h /^ Texture texture; \/\/ Color buffer attachment texture$/;" m struct:RenderTexture typeref:typename:Texture