Mercurial
view postdog/main.c @ 162:8ceb5d3c6bdd
Playing around with some random graphcis.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Mon, 19 Jan 2026 04:51:50 -0800 |
| parents | 87d8d3eb3491 |
| children | 058de208e640 |
line wrap: on
line source
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <sys/stat.h> #include <uv.h> #ifdef _WIN32 #include <direct.h> #include <io.h> #define mkdir(path, mode) _mkdir(path) #define access _access #define F_OK 0 #else #include <sys/stat.h> #include <dirent.h> #include <unistd.h> #endif #include "dowa/dowa.h" #include "seobeo/seobeo.h" #include "third_party/raylib/include/raylib.h" #include "third_party/raylib/include/raygui.h" #include "third_party/raylib/custom.h" #ifndef POSTDOG_PATHS #define POSTDOG_PATHS "/Users/mrjunejune/zenbu/postdog/history" #endif #define SCREEN_WIDTH 1280 #define SCREEN_HEIGHT 780 #define MAX_SCROLL_HEIGHT 10000 #define HEADER_BUFFER_LENGTH 1024 * 4 #define DEFAULT_TEXT_BUFFER_LENGTH 1024 * 4 #define URL_TEXT_BUFFER_LENGTH 1024 * 10 #define BODY_BUFFER_LENGTH 1024 * 1024 * 5 #define RESULT_BUFFER_LENGTH 1024 * 1024 * 5 #define URL_TEXT_DEFAULT "https://httpbin.org/get" #define HEADER_TEXT_DEFAULT "Content-Type: application/json" #define BODY_TEXT_DEFAULT "" #define GET_PARAM_TEXT_DEFAULT "foo bar" // ============================================================================ // TextArea Component // ============================================================================ #define TEXT_SIZE_DEFAULT 10 // used to calcualte spacing #define TEXT_AREA_LINE_HEIGHT 20 #define TEXT_AREA_PADDING 30 #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/GuiGetStyle(DEFAULT, TEXT_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, GuiGetStyle(DEFAULT, TEXT_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, GuiGetStyle(DEFAULT, TEXT_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, GuiGetStyle(DEFAULT, TEXT_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, GuiGetStyle(DEFAULT, TEXT_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 DrawRectangleRounded(bounds, 0.2, 1, is_edit_mode ? DARKGRAY : (Color){40, 40, 40, 255}); // DrawRectangleRec(bounds, is_edit_mode ? DARKGRAY : (Color){40, 40, 40, 255}); // DrawRectangleLinesEx(bounds, 1, is_edit_mode ? WHITE : GRAY); DrawRectangleRoundedLines(bounds, 0.2, 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, GuiGetStyle(DEFAULT, TEXT_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, GuiGetStyle(DEFAULT, TEXT_SIZE)); float x2 = content_x + MeasureTextRange(text, line_char_start, highlight_end, GuiGetStyle(DEFAULT, TEXT_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, GuiGetStyle(DEFAULT, TEXT_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, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_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 , GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_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, GuiGetStyle(DEFAULT, TEXT_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 { char *data; size_t size; } ResponseBuffer; typedef struct { char *filename; char *title; Rectangle rect; long time_modified; boolean deleted; } HistoryItem; typedef struct { Rectangle rectangle; char *label; bool active; } TabItem; typedef enum { TAB_HEADER = 0, TAB_BODY, TAB_GET_PARAMS, TAB_WEBSOCKET, 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; static uv_mutex_t history_mutex; static uv_loop_t *main_loop = NULL; HistoryItem *history_items = NULL; HistoryItem *new_history_items = NULL; // Global UI state char *url_input_text = NULL; char *result_text = NULL; char **input_body_array = NULL; int active_method_dropdown = 0; int active_input_tab = 0; Seobeo_WebSocket *ws = NULL; boolean WS_BREAK = FALSE; uv_thread_t websocket_thread_id; Color TEXT_COLOR = BLACK; boolean LOADING = FALSE; int CompareHistoryItemsByDate(const void *a, const void *b) { HistoryItem *itemA = (HistoryItem *)a; HistoryItem *itemB = (HistoryItem *)b; return (itemB->time_modified - itemA->time_modified); } char *PostDog_Extract_Title(const char *filename) { char full_file_path[512] = {0}; snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); FILE *file = fopen(full_file_path, "r"); if (!file) return strdup(filename); char *title = malloc(sizeof(char) * 512); if (!fgets(title, 512, file)) { fclose(file); free(title); return strdup(filename); } fclose(file); // Strip trailing newline title[strcspn(title, "\n")] = '\0'; return title; } // TODO: Make this into generic fucntion so I can use it across different thing. void PostDog_List_Directory(const char *path, HistoryItem **p_file_arr) { HistoryItem *file_arr = *p_file_arr; #ifdef _WIN32 struct _finddata_t fileinfo; intptr_t handle; char search_path[256]; sprintf(search_path, "%s\\*", path); if ((handle = _findfirst(search_path, &fileinfo)) == -1L) { printf("Directory is empty or cannot be read.\n"); } else { do { HistoryItem item = {0}; item.filename = strdup(fileinfo.name); item.title = PostDog_Extract_Title(fileinfo.name); item.time_modified = fileinfo.time_write; item.deleted = FALSE; Dowa_Array_Push(file_arr, item); } while (_findnext(handle, &fileinfo) == 0); _findclose(handle); } #else struct dirent *entry; struct stat file_stat; DIR *dp = opendir(path); if (dp == NULL) return; char full_path[256]; while ((entry = readdir(dp))) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name); if (stat(full_path, &file_stat) == 0) { HistoryItem item = {0}; item.filename = strdup(entry->d_name); item.title = PostDog_Extract_Title(entry->d_name); item.time_modified = file_stat.st_mtime; item.deleted = FALSE; Dowa_Array_Push(file_arr, item); } } closedir(dp); #endif // Update the caller's pointer in case array was reallocated *p_file_arr = file_arr; int count = Dowa_Array_Length(file_arr); if (count > 1) { qsort(file_arr, count, sizeof(HistoryItem), CompareHistoryItemsByDate); } } int PostDog_History_Load(HistoryItem **p_history_files) { if (access(POSTDOG_PATHS, F_OK) == -1) { printf("Directory '%s' not found. Creating it...\n", POSTDOG_PATHS); if (mkdir(POSTDOG_PATHS, 0777) != 0) return -1; return 0; } printf("Directory '%s' already exists.\n", POSTDOG_PATHS); PostDog_List_Directory(POSTDOG_PATHS, p_history_files); return 0; } bool InArea(Vector2 mouse_position, Rectangle area) { return ( mouse_position.x >= area.x && mouse_position.x < area.x + area.width && mouse_position.y >= area.y && mouse_position.y < area.y + area.height ); } bool Clicked(Vector2 mouse_position, Rectangle area) { return (InArea(mouse_position, area) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)); } // -------- END of UI ---- // char *PostDog_Enum_To_String(int active_enum) { switch(active_enum) { case 0: return "GET"; case 1: return "POST"; case 2: return "PUT"; case 3: return "DELETE"; } return 0; } char *PostDog_Construct_URL(char *filename, char *out_buffer, size_t buffer_size) { snprintf(out_buffer, buffer_size, "%s/%s", POSTDOG_PATHS, filename); return out_buffer; } boolean PostDog_History_CreateFile(char *filename, char* values) { char full_file_path[512] = {0}; snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); FILE *file = fopen(full_file_path, "w"); if (!file) { fprintf(stderr, "Failed to create a file: %s\n", full_file_path); return FALSE; } fwrite(values, 1, strlen(values), file); fclose(file); return TRUE; } void PostDog_Request_SaveFile(void) { const char *method = PostDog_Enum_To_String(active_method_dropdown); 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 = Dowa_Arena_Allocate(arena, strlen(method) + strlen(url_input_text) + 2); sprintf(title, "%s %s", method, url_input_text); snprintf( new_file, new_file_size, "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n", title, url_input_text, method, input_body_array[TAB_HEADER], input_body_array[TAB_BODY], input_body_array[TAB_GET_PARAMS], input_body_array[TAB_WEBSOCKET], result_text ); char *filename = Dowa_Arena_Allocate(arena, 1024); if (!filename) { perror("Error opening file"); exit(EXIT_FAILURE); } char *uuid4 = (char *)Dowa_Arena_Allocate(arena, 37); if (!uuid4) { perror("Error uuid"); exit(EXIT_FAILURE); } uv_mutex_lock(&history_mutex); int32 seed = (uint32)time(NULL) ^ counter++; Dowa_String_UUID(seed, uuid4); snprintf(filename, 1024, "%s.txt", uuid4); if (PostDog_History_CreateFile(filename, new_file)) { HistoryItem item = {0}; item.filename = strdup(filename); item.title = strdup(title); item.deleted = FALSE; Dowa_Array_Push(new_history_items, item); } uv_mutex_unlock(&history_mutex); Dowa_Arena_Free(arena); } void PostDog_Websocket_Listen(void) { while (TRUE) { if (WS_BREAK) break; Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Receive(ws); if (p_msg) { if (p_msg->opcode == SEOBEO_WS_OPCODE_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); } Seobeo_WebSocket_Message_Destroy(p_msg); } usleep(10000); printf("Listening\n"); } return; } void PostDog_Websocket_Connect(void) { ws = Seobeo_WebSocket_Connect(url_input_text); memset(result_text, 0, strlen(result_text)); } int PostDog_Websocket_Send(void) { 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"); return -1; } snprintf(result_text + strlen(result_text), RESULT_BUFFER_LENGTH - strlen(result_text), "\n%s", input_body_array[active_input_tab]); return 0; } void PostDog_Websocket_Destroy(uv_thread_t thread_id) { Seobeo_WebSocket_Destroy(ws); uv_thread_join(&thread_id); } void PostDog_Websocket_Start(void *arg) { PostDog_Websocket_Connect(); PostDog_Websocket_Listen(); } uv_thread_t PostDog_Websocket_Start_Thread() { uv_thread_t thread_id; if (uv_thread_create(&thread_id, PostDog_Websocket_Start, NULL) != 0) { perror("Failed to create thread"); memset(&thread_id, 0, sizeof(thread_id)); return 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) { case 0: { Seobeo_Client_Request_Set_Method(req, "GET"); break; } case 1: { Seobeo_Client_Request_Set_Method(req, "POST"); break; } case 2: { Seobeo_Client_Request_Set_Method(req, "PUT"); break; } case 3: { Seobeo_Client_Request_Set_Method(req, "DELETE"); break; } } { if (input_body_array[TAB_HEADER] && strlen(input_body_array[TAB_HEADER]) > 0) { char *headersCopy = strdup(input_body_array[TAB_HEADER]); char *line = strtok(headersCopy, "\n"); while (line != NULL) { while (*line == ' ' || *line == '\t') line++; if (strlen(line) > 0) Seobeo_Client_Request_Add_Header_Array(req, line); line = strtok(NULL, "\n"); } } Seobeo_Client_Request_Set_Follow_Redirects(req, TRUE, 5); // TODO: remove magic number; res = Seobeo_Client_Request_Execute(req); if (res == NULL) snprintf(result_text, RESULT_BUFFER_LENGTH, "Error: Failed to send the request\n"); else snprintf(result_text, RESULT_BUFFER_LENGTH, "HTTP Status: %d\n\n%s", res->status_code, res->body ? res->body : ""); } Seobeo_Client_Request_Destroy(req); Seobeo_Client_Response_Destroy(res); PostDog_Request_SaveFile(); return 0; } void PostDog_Http_Work(uv_work_t *req) { PostDog_Http_Request(); printf("HTTP request finished.\n"); } void PostDog_Http_Work_Done(uv_work_t *req, int status) { LOADING = FALSE; free(req); } void PostDog_Http_Thread_Request() { uv_work_t *work_req = malloc(sizeof(uv_work_t)); if (!work_req) { perror("Failed to allocate work request"); return; } LOADING = TRUE; if (uv_queue_work(main_loop, work_req, PostDog_Http_Work, PostDog_Http_Work_Done) != 0) { perror("Failed to queue work"); free(work_req); LOADING = FALSE; } } void PostDog_Update_URL(void) { // Save existing query string if present char *question_mark = strchr(url_input_text, '?'); if (question_mark) *question_mark = '\0'; int get_params_length = (int)strlen(input_body_array[TAB_GET_PARAMS]); if (get_params_length == 0) return; char *separator = "?"; Dowa_Arena *arena = Dowa_Arena_Create(1024*1024); char **lines = Dowa_String_Split(input_body_array[TAB_GET_PARAMS], "\n", get_params_length, 1, arena); for (int i = 0; i < Dowa_Array_Length(lines); i++) { char *line = lines[i]; char **key_value = Dowa_String_Split(line, " ", (int)strlen(line), 1, arena); if (Dowa_Array_Length(key_value) < 2) break; strcat(url_input_text, separator); strcat(url_input_text, key_value[0]); strcat(url_input_text, "="); for (int i = 1; i < Dowa_Array_Length(key_value); i++) { if (!key_value[i] || key_value[i][0] == '\0') break; if (i > 1) strcat(url_input_text, "%20"); strcat(url_input_text, key_value[i]); } separator = "&"; } Dowa_Arena_Free(arena); } int PostDog_String_To_MethodEnum(char *value) { if (strstr(value, "GET")) return 0; if (strstr(value, "POST")) return 1; if (strstr(value, "PUT")) return 2; if (strstr(value, "DELETE")) return 3; return 0; } void PostDog_Params_Reset(void) { url_input_text[0] = '\0'; result_text[0] = '\0'; 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) { char full_file_path[512] = {0}; snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); FILE *file = fopen(full_file_path, "r"); if (!file) return; fseek(file, 0, SEEK_END); size_t file_size = ftell(file); fseek(file, 0, SEEK_SET); Dowa_Arena *init_arena = Dowa_Arena_Create(file_size + 2); Dowa_Arena *split_arena = Dowa_Arena_Create(file_size * 2); char *file_buffer = Dowa_Arena_Allocate(init_arena, file_size+1); fread(file_buffer, 1, file_size, file); fclose(file); char **values = Dowa_String_Split(file_buffer, "---\n", file_size, 4, split_arena); for (int i = 0; i < Dowa_Array_Length(values); i++) { switch (i) { case 0: // Title - skip break; case 1: // URL snprintf(url_input_text, strlen(values[i]) + 1, "%s", values[i]); url_input_text[strcspn(url_input_text, "\n")] = '\0'; break; case 2: // Method active_method_dropdown = PostDog_String_To_MethodEnum(values[i]); break; case 3: // Headers (TAB_HEADER) case 4: // Body (TAB_BODY) case 5: // Get Params (TAB_GET_PARAMS) case 6: // Websocket (TAB_WEBSOCKET) { int map_index = i - 3; // 3->0, 4->1, 5->2, 6->3 snprintf(input_body_array[map_index], strlen(values[i]) + 1, "%s", values[i]); // Trim trailing newlines for (int j = strlen(values[i]); j > 0; j--) { if (input_body_array[map_index][j] == '\n') { input_body_array[map_index][j] = '\0'; break; } } break; } default: // Response (index 7+) snprintf(result_text, strlen(values[i]) + 1, "%s", values[i]); break; } } // Reset text area states when loading new file GuiTextAreaResetAllStates(); Dowa_Arena_Free(init_arena); Dowa_Arena_Free(split_arena); } int main() { // -- initialize libuv --// main_loop = uv_default_loop(); uv_mutex_init(&history_mutex); // -- initizlied --// InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "PostDog"); SetWindowState(FLAG_WINDOW_RESIZABLE); SetTargetFPS(60); Font customFont = LoadFontEx("postdog/Roboto-Regular.ttf", 20, 0, 0); GuiSetFont(customFont); GuiSetStyle(DEFAULT, TEXT_SIZE, 15); Image logo_original = LoadImage("postdog/logo_bigger.png"); ImageResize(&logo_original, 200, 600); SetWindowIcon(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 * 4); // -- Starting pos ---// Rectangle full_screen = { 0 }; Rectangle history_sidebar_rect = { 0 }; Dowa_Array_Reserve(history_items, 10); Dowa_Array_Reserve(new_history_items, 10); PostDog_History_Load(&history_items); int32 *history_deleted_items = NULL; Rectangle url_area_rect = { 0 }; Rectangle url_input_bounds_rect = { 0 }; bool url_input_edit = false; Rectangle url_text_bounds_rect = { 0 }; Rectangle url_enter_button_rect = { 0 }; Rectangle method_dropdown_rect = { 0 }; // Initialize global UI state url_input_text = (char *)malloc(sizeof(char) * URL_TEXT_BUFFER_LENGTH); snprintf(url_input_text, URL_TEXT_BUFFER_LENGTH, URL_TEXT_DEFAULT); Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * HEADER_BUFFER_LENGTH)); Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * BODY_BUFFER_LENGTH)); Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); snprintf(input_body_array[TAB_HEADER], HEADER_BUFFER_LENGTH, HEADER_TEXT_DEFAULT); snprintf(input_body_array[TAB_BODY], HEADER_BUFFER_LENGTH, BODY_TEXT_DEFAULT); snprintf(input_body_array[TAB_GET_PARAMS], HEADER_BUFFER_LENGTH, GET_PARAM_TEXT_DEFAULT); result_text = (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH); result_text[0] = '\0'; bool method_edit = false; int sendRequest; // -- input --// Rectangle input_area_rect = { 0 }; Rectangle input_tab_rect = { 0 }; Rectangle input_tab_item_rect = { 0 }; Rectangle input_body_rect = { 0 }; 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? int prev_input_tab = 0; // Scroll offsets float history_scroll_offset = 0; while (!WindowShouldClose()) { // Process libuv events (non-blocking) uv_run(main_loop, UV_RUN_NOWAIT); int screen_width = GetScreenWidth(); int screen_height = GetScreenHeight(); if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyDown(KEY_EQUAL)) GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetStyle(DEFAULT, TEXT_SIZE) + 1); if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyDown(KEY_MINUS)) GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetStyle(DEFAULT, TEXT_SIZE) - 1); Rectangle screen_rect = { 0, 0, screen_width, screen_height }; // -- Side bar --// history_sidebar_rect = LeftColumn(screen_rect, 0.20, padding); Rectangle content_area_rect = RightColumn(screen_rect, history_sidebar_rect, padding); Rectangle logo_area_rect = (Rectangle){ .x = history_sidebar_rect.x, .y = history_sidebar_rect.y, .width = history_sidebar_rect.width, .height = 300 }; DrawRectangleSelectiveRounded(history_sidebar_rect, 10, 10, ORANGE, TRUE, FALSE, FALSE, TRUE); DrawRectangleSelectiveRounded(content_area_rect, 10, 10, WHITE, FALSE, TRUE, TRUE, FALSE); Rectangle history_list_area_rect = Below(logo_area_rect, 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; int32 new_history_items_length = Dowa_Array_Length(new_history_items); int32 history_item_length = Dowa_Array_Length(history_items); int32 total = new_history_items_length + history_item_length; float item_height = history_list_area_rect.height * 0.10; int32 number_of_skipped_items = 0; for (int i = 0; i < total; i++) { 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) { number_of_skipped_items++; continue; } curr_history_items->rect = (Rectangle){ .x = history_list_area_rect.x, .y = history_list_area_rect.y + (padding + item_height) * (i - number_of_skipped_items) + history_scroll_offset, .width = history_list_area_rect.width, .height = item_height }; } // --- URL --- // url_area_rect = (Rectangle){ .x = content_area_rect.x, .y = content_area_rect.y, .width = content_area_rect.width, .height = content_area_rect.height * 0.1 }; float url_control_y = url_area_rect.y + (url_area_rect.height - TEXT_SIZE * 2) / 2; url_text_bounds_rect = (Rectangle){ .x = url_area_rect.x + padding, .y = url_control_y, .width = 10 * (TEXT_SIZE / 2), .height = TEXT_SIZE * 2 }; url_input_bounds_rect = RightOf(url_text_bounds_rect, padding); url_input_bounds_rect.width = url_area_rect.width * 0.7; url_input_bounds_rect.height = TEXT_SIZE * 2.5; url_enter_button_rect = RightOf(url_input_bounds_rect, padding); url_enter_button_rect.width = url_area_rect.width * 0.1; url_enter_button_rect.height = TEXT_SIZE * 2; method_dropdown_rect = RightOf(url_enter_button_rect, padding); method_dropdown_rect.width = url_area_rect.width * 0.1; method_dropdown_rect.height = TEXT_SIZE * 2; // -- Body -- // Rectangle body_area_rect = Below(url_area_rect, 0); body_area_rect.height = content_area_rect.height - url_area_rect.height; input_area_rect = HorizontalSplit(body_area_rect, 0.5); result_area_rect = RightOf(input_area_rect, 0); result_area_rect.width = body_area_rect.width - input_area_rect.width; input_tab_rect = (Rectangle){ .x = input_area_rect.x + padding, .y = input_area_rect.y + padding, .width = input_area_rect.width - (2 * padding), .height = input_area_rect.height * 0.1 }; input_tab_item_rect = input_tab_rect; input_tab_item_rect.width = input_tab_rect.width / 4; input_body_rect = Below(input_tab_rect, 0); input_body_rect.width = input_tab_rect.width; input_body_rect.height = input_area_rect.height - input_tab_rect.height - (2 * padding); // -- Result -- / result_body_rect = (Rectangle){ .x = result_area_rect.x + padding, .y = input_body_rect.y, .width = result_area_rect.width - (2 * padding), .height = input_body_rect.height }; Vector2 mouse_position = GetMousePosition(); float mouse_wheel = GetMouseWheelMove(); // Reset text area state when tab changes if (prev_input_tab != active_input_tab) { prev_input_tab = active_input_tab; } // Handle scroll wheel for history if (InArea(mouse_position, history_list_area_rect) && mouse_wheel != 0) { history_scroll_offset += mouse_wheel * 30; // 30 pixels per wheel tick // Clamp scroll offset float max_scroll = (total * (item_height + padding)) - history_list_area_rect.height; if (history_scroll_offset > 0) history_scroll_offset = 0; if (history_scroll_offset < -max_scroll && max_scroll > 0) history_scroll_offset = -max_scroll; } // Reset SetMouseCursor(MOUSE_CURSOR_DEFAULT); // 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(); // --- Begin Drawing --- // BeginDrawing(); ClearBackground(GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR))); // DrawRectangleRec(history_sidebar_rect, Fade(GRAY, 0.1f)); DrawRectangleRounded(history_sidebar_rect, 0.5, 1, Fade(BLUE, 0.1f)); // DrawRectangleRec(logo_area_rect, Fade(BLUE, 0.2f)); Rectangle logo_image_rect = AddPadding(logo_area_rect, padding); // Fit logo to area while maintaining aspect ratio float logo_size = logo_image_rect.height < logo_image_rect.width ? logo_image_rect.height : logo_image_rect.width; Rectangle dest_rect = { .x = logo_image_rect.x + (logo_image_rect.width - logo_size) / 2, // Center horizontally .y = logo_image_rect.y, .width = logo_size, .height = logo_size }; Rectangle source_rect = { 0, 0, logo_texture.width, logo_texture.height }; DrawTexturePro(logo_texture, source_rect, dest_rect, (Vector2){0, 0}, 0.0f, WHITE); 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[new_history_items_length - i - 1] : &history_items[i - new_history_items_length]; if (curr_history_items->deleted) continue; float diff = curr_history_items->rect.height*0.6; 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; Rectangle icon_area = Below(filename_area_rect, 0); icon_area.height = diff; // DrawRectangleRec(filename_area_rect, Fade(BLUE, 0.1f)); // DrawRectangleRec(icon_area, Fade(YELLOW, 0.1f)); // DrawRectangleRec(AddPadding(filename_area_rect, 5), Fade(BLUE, 0.1f)); Rectangle icon_area_left_column = LeftColumn(icon_area, 0.5, 0); Rectangle icon_area_right_column = RightColumn(icon_area, icon_area_left_column, 0); Rectangle temp = AddPadding(filename_area_rect, padding); // ADD this back // GuiDrawText(curr_history_items->title, temp, 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")) { char delete_path[512]; if (!remove(PostDog_Construct_URL(curr_history_items->filename, delete_path, sizeof(delete_path)))) curr_history_items->deleted = TRUE; else fprintf(stderr, "Wasn't able to delete file: %s \n", curr_history_items->filename); } } EndScissorMode(); if (total > 0) { float content_height = total * (item_height + padding); if (content_height > history_list_area_rect.height) { float scrollbar_height = (history_list_area_rect.height / content_height) * history_list_area_rect.height; float scrollbar_y = history_list_area_rect.y - (history_scroll_offset / content_height) * history_list_area_rect.height; Rectangle scrollbar_rect = { history_list_area_rect.x + history_list_area_rect.width - 5, scrollbar_y, 5, scrollbar_height }; DrawRectangleRec(scrollbar_rect, Fade(WHITE, 0.5f)); } } // URL area Rect DrawRectangleRec(url_area_rect, Fade(BLACK, 0.1f)); if (GuiTextArea(21, url_input_bounds_rect, url_input_text, DEFAULT_TEXT_BUFFER_LENGTH, url_input_edit, FALSE, g_text_area_arena)) url_input_edit = !url_input_edit; if (url_input_edit) { if (IsKeyPressed(KEY_ENTER)) { PostDog_Http_Thread_Request(); url_input_edit = !url_input_edit; } } if (GuiButtonRounded(url_enter_button_rect, "New Request", 0.2, 1)) PostDog_Http_Thread_Request(); DrawRectangleSelectiveRounded(body_area_rect, 10, 10, ORANGE, TRUE, FALSE, FALSE, TRUE); DrawRectangleSelectiveRounded(result_area_rect, 10, 10, WHITE, FALSE, TRUE, TRUE, FALSE); // Input Tabs Rect DrawRectangleRec(input_area_rect, Fade(BLUE, 0.1f)); DrawRectangleRec(input_tab_rect, Fade(DARKBLUE, 0.1f)); GuiSetStyle(TOGGLE, GROUP_PADDING, 0); 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; 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))) { if (!ws) websocket_thread_id = PostDog_Websocket_Start_Thread(); usleep(10000); PostDog_Websocket_Send(); } } GuiToggleGroup(input_tab_item_rect, "Header;Body;Get Param;Websocket", &active_input_tab); PostDog_Update_URL(); // 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_toggle) result_body_edit_mode = !result_body_edit_mode; if (GuiDropdownBoxRounded(url_text_bounds_rect, "GET;POST;PUT;DELETE", &active_method_dropdown, method_edit, 0.2, 1)) method_edit = !method_edit; EndDrawing(); } GuiTextAreaResetAllStates(); Dowa_Arena_Free(g_text_area_arena); // Cleanup libuv uv_mutex_destroy(&history_mutex); uv_loop_close(main_loop); CloseWindow(); return 0; }