Mercurial
comparison postdog/main.c @ 173:827c6ac504cd hg-web
Merged in default here.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 19 Jan 2026 18:59:10 -0800 |
| parents | 0face9898d04 |
| children |
comparison
equal
deleted
inserted
replaced
| 151:c033667da5f9 | 173:827c6ac504cd |
|---|---|
| 1 #include <stdio.h> | 1 #include <stdio.h> |
| 2 #include <stdlib.h> | 2 #include <stdlib.h> |
| 3 #include <string.h> | 3 #include <string.h> |
| 4 #include <time.h> | 4 #include <time.h> |
| 5 #include <sys/stat.h> | 5 #include <sys/stat.h> |
| 6 #include <uv.h> | |
| 7 | |
| 8 #ifdef _WIN32 | |
| 9 #include <direct.h> | |
| 10 #include <io.h> | |
| 11 #define mkdir(path, mode) _mkdir(path) | |
| 12 #define access _access | |
| 13 #define F_OK 0 | |
| 14 #else | |
| 15 #include <sys/stat.h> | |
| 16 #include <dirent.h> | |
| 17 #include <unistd.h> | |
| 18 #endif | |
| 19 | |
| 20 | |
| 6 #include "dowa/dowa.h" | 21 #include "dowa/dowa.h" |
| 7 | 22 #include "seobeo/seobeo.h" |
| 8 #include <curl/curl.h> | |
| 9 #include "third_party/raylib/include/raylib.h" | 23 #include "third_party/raylib/include/raylib.h" |
| 10 #define RAYGUI_IMPLEMENTATION | |
| 11 #include "third_party/raylib/include/raygui.h" | 24 #include "third_party/raylib/include/raygui.h" |
| 25 #include "third_party/raylib/custom.h" | |
| 12 | 26 |
| 13 #ifndef POSTDOG_PATHS | 27 #ifndef POSTDOG_PATHS |
| 14 #define POSTDOG_PATHS "/Users/mrjunejune/zenbu/postdog/history" | 28 #define POSTDOG_PATHS "/home/june/zenbu/postdog/history" |
| 15 #endif | 29 #endif |
| 16 | 30 |
| 17 #define SCREEN_WIDTH 1280 | 31 #define SCREEN_WIDTH 1280 |
| 18 #define SCREEN_HEIGHT 780 | 32 #define SCREEN_HEIGHT 780 |
| 19 #define MAX_SCROLL_HEIGHT 10000 | 33 #define MAX_SCROLL_HEIGHT 10000 |
| 22 #define DEFAULT_TEXT_BUFFER_LENGTH 1024 * 4 | 36 #define DEFAULT_TEXT_BUFFER_LENGTH 1024 * 4 |
| 23 #define URL_TEXT_BUFFER_LENGTH 1024 * 10 | 37 #define URL_TEXT_BUFFER_LENGTH 1024 * 10 |
| 24 #define BODY_BUFFER_LENGTH 1024 * 1024 * 5 | 38 #define BODY_BUFFER_LENGTH 1024 * 1024 * 5 |
| 25 #define RESULT_BUFFER_LENGTH 1024 * 1024 * 5 | 39 #define RESULT_BUFFER_LENGTH 1024 * 1024 * 5 |
| 26 | 40 |
| 27 | 41 // #define URL_TEXT_DEFAULT "https://httpbin.org/get" |
| 28 #ifdef _WIN32 | 42 #define URL_TEXT_DEFAULT "wss://mrjunejune.com/echo" |
| 29 #include <direct.h> | 43 #define HEADER_TEXT_DEFAULT "Content-Type: application/json" |
| 30 #include <io.h> | 44 #define BODY_TEXT_DEFAULT "" |
| 31 #define mkdir(path, mode) _mkdir(path) | 45 #define GET_PARAM_TEXT_DEFAULT "foo bar" |
| 32 #define access _access | 46 |
| 33 #define F_OK 0 | 47 // ============================================================================ |
| 34 #else | 48 // TextArea Component |
| 35 #include <sys/stat.h> | 49 // ============================================================================ |
| 36 #include <dirent.h> | 50 |
| 37 #include <unistd.h> | 51 #define TEXT_SIZE_DEFAULT 20 // used to calcualte spacing |
| 38 #endif | 52 #define TEXT_AREA_LINE_HEIGHT GuiGetStyle(DEFAULT, TEXT_SIZE) |
| 53 #define TEXT_AREA_PADDING 30 | |
| 54 #define TEXT_AREA_CURSOR_WIDTH 2 | |
| 55 #define TEXT_AREA_MAX_UNDO_STATES 64 | |
| 56 #define TEXT_AREA_MAX_INSTANCES 8 | |
| 57 | |
| 58 typedef struct { | |
| 59 char *text; | |
| 60 int cursor_pos; | |
| 61 int selection_start; | |
| 62 int selection_end; | |
| 63 } TextAreaUndoEntry; | |
| 64 | |
| 65 // Cached line info for fast rendering | |
| 66 typedef struct { | |
| 67 int start_pos; // Character position where this line starts | |
| 68 int end_pos; // Character position where this line ends (exclusive) | |
| 69 int char_count; // Number of characters in this line | |
| 70 } LineInfo; | |
| 71 | |
| 72 typedef struct { | |
| 73 int id; // Unique ID for this text area | |
| 74 int cursor_pos; // Current cursor position in text | |
| 75 int selection_start; // Selection start (-1 if no selection) | |
| 76 int selection_end; // Selection end (-1 if no selection) | |
| 77 float scroll_offset_y; // Vertical scroll offset | |
| 78 float scroll_offset_x; // Horizontal scroll offset (for non-wrap mode) | |
| 79 boolean is_selecting; // Currently dragging to select | |
| 80 boolean is_initialized; // State has been initialized | |
| 81 | |
| 82 // Undo history | |
| 83 TextAreaUndoEntry *undo_stack; // Dowa array of undo entries | |
| 84 int undo_index; // Current position in undo stack | |
| 85 | |
| 86 // Internal tracking | |
| 87 double last_blink_time; | |
| 88 boolean cursor_visible; | |
| 89 | |
| 90 // Line cache for fast rendering | |
| 91 LineInfo *line_cache; // Dowa array of line info | |
| 92 int cached_text_len; // Text length when cache was built | |
| 93 int cached_text_hash; // Simple hash to detect text changes | |
| 94 float cached_content_width; // Content width when cache was built | |
| 95 boolean cache_valid; // Whether cache is valid | |
| 96 } TextAreaState; | |
| 97 | |
| 98 static TextAreaState g_text_area_states[TEXT_AREA_MAX_INSTANCES] = {0}; | |
| 99 static int g_text_area_state_count = 0; | |
| 100 static char *g_clipboard_text = NULL; | |
| 101 static Dowa_Arena *g_text_area_arena = NULL; | |
| 102 | |
| 103 // Helper functions | |
| 104 static int TA_Min_Int(int a, int b) { return a < b ? a : b; } | |
| 105 static int TA_Max_Int(int a, int b) { return a > b ? a : b; } | |
| 106 static float TA_Min_Float(float a, float b) { return a < b ? a : b; } | |
| 107 static float TA_Max_Float(float a, float b) { return a > b ? a : b; } | |
| 108 | |
| 109 // Sanitize text for display - replace tabs with spaces, remove other non-printable chars | |
| 110 static void SanitizeTextForDisplay(char *text) { | |
| 111 if (!text) return; | |
| 112 char *read = text; | |
| 113 char *write = text; | |
| 114 while (*read) { | |
| 115 if (*read == '\t') { | |
| 116 // Replace tab with 2 spaces | |
| 117 *write++ = ' '; | |
| 118 *write++ = ' '; | |
| 119 } else if (*read == '\n' || *read == '\r') { | |
| 120 // Keep newlines and carriage returns | |
| 121 *write++ = *read; | |
| 122 } else if ((unsigned char)*read >= 32 && (unsigned char)*read < 127) { | |
| 123 // Keep printable ASCII | |
| 124 *write++ = *read; | |
| 125 } else if ((unsigned char)*read >= 128) { | |
| 126 // Keep UTF-8 characters (high bit set) | |
| 127 *write++ = *read; | |
| 128 } | |
| 129 // Skip other non-printable characters | |
| 130 read++; | |
| 131 } | |
| 132 *write = '\0'; | |
| 133 } | |
| 134 | |
| 135 static TextAreaState* GetTextAreaState(int id) { | |
| 136 for (int i = 0; i < g_text_area_state_count; i++) { | |
| 137 if (g_text_area_states[i].id == id && g_text_area_states[i].is_initialized) { | |
| 138 return &g_text_area_states[i]; | |
| 139 } | |
| 140 } | |
| 141 return NULL; | |
| 142 } | |
| 143 | |
| 144 static TextAreaState* CreateTextAreaState(int id) { | |
| 145 // First, check if there's an existing slot with this ID (possibly reset) | |
| 146 for (int i = 0; i < g_text_area_state_count; i++) { | |
| 147 if (g_text_area_states[i].id == id) { | |
| 148 TextAreaState *state = &g_text_area_states[i]; | |
| 149 // Reuse this slot - clear it but keep the id | |
| 150 memset(state, 0, sizeof(TextAreaState)); | |
| 151 state->id = id; | |
| 152 state->selection_start = -1; | |
| 153 state->selection_end = -1; | |
| 154 state->undo_index = -1; | |
| 155 state->cursor_visible = TRUE; | |
| 156 state->is_initialized = TRUE; | |
| 157 return state; | |
| 158 } | |
| 159 } | |
| 160 | |
| 161 // No existing slot, create a new one | |
| 162 if (g_text_area_state_count >= TEXT_AREA_MAX_INSTANCES) { | |
| 163 return &g_text_area_states[0]; // Reuse first slot as fallback | |
| 164 } | |
| 165 | |
| 166 TextAreaState *state = &g_text_area_states[g_text_area_state_count++]; | |
| 167 memset(state, 0, sizeof(TextAreaState)); | |
| 168 state->id = id; | |
| 169 state->selection_start = -1; | |
| 170 state->selection_end = -1; | |
| 171 state->undo_index = -1; | |
| 172 state->cursor_visible = TRUE; | |
| 173 state->is_initialized = TRUE; | |
| 174 return state; | |
| 175 } | |
| 176 | |
| 177 static void GetLineAndColumn(const char *text, int pos, int *out_line, int *out_column) { | |
| 178 int line = 0; | |
| 179 int column = 0; | |
| 180 for (int i = 0; i < pos && text[i] != '\0'; i++) { | |
| 181 if (text[i] == '\n') { | |
| 182 line++; | |
| 183 column = 0; | |
| 184 } else { | |
| 185 column++; | |
| 186 } | |
| 187 } | |
| 188 *out_line = line; | |
| 189 *out_column = column; | |
| 190 } | |
| 191 | |
| 192 static int GetPosFromLineColumn(const char *text, int line, int column) { | |
| 193 int current_line = 0; | |
| 194 int current_col = 0; | |
| 195 int i = 0; | |
| 196 | |
| 197 while (text[i] != '\0') { | |
| 198 if (current_line == line && current_col == column) { | |
| 199 return i; | |
| 200 } | |
| 201 if (text[i] == '\n') { | |
| 202 if (current_line == line) { | |
| 203 return i; | |
| 204 } | |
| 205 current_line++; | |
| 206 current_col = 0; | |
| 207 } else { | |
| 208 current_col++; | |
| 209 } | |
| 210 i++; | |
| 211 } | |
| 212 return i; | |
| 213 } | |
| 214 | |
| 215 static int GetLineStart(const char *text, int pos) { | |
| 216 int i = pos; | |
| 217 while (i > 0 && text[i - 1] != '\n') { | |
| 218 i--; | |
| 219 } | |
| 220 return i; | |
| 221 } | |
| 222 | |
| 223 static int GetLineEnd(const char *text, int pos) { | |
| 224 int i = pos; | |
| 225 int len = strlen(text); | |
| 226 while (i < len && text[i] != '\n') { | |
| 227 i++; | |
| 228 } | |
| 229 return i; | |
| 230 } | |
| 231 | |
| 232 static int CountLines(const char *text) { | |
| 233 int count = 1; | |
| 234 for (int i = 0; text[i] != '\0'; i++) { | |
| 235 if (text[i] == '\n') count++; | |
| 236 } | |
| 237 return count; | |
| 238 } | |
| 239 | |
| 240 // Simple hash to detect text changes (djb2) | |
| 241 static int SimpleTextHash(const char *text, int len) { | |
| 242 int hash = 5381; | |
| 243 int sample_count = len < 100 ? len : 100; // Sample first 100 chars | |
| 244 for (int i = 0; i < sample_count; i++) { | |
| 245 hash = ((hash << 5) + hash) + text[i]; | |
| 246 } | |
| 247 // Also sample some chars from middle and end | |
| 248 if (len > 200) { | |
| 249 for (int i = len/2; i < len/2 + 50 && i < len; i++) { | |
| 250 hash = ((hash << 5) + hash) + text[i]; | |
| 251 } | |
| 252 } | |
| 253 return hash; | |
| 254 } | |
| 255 | |
| 256 // Build line cache - O(n) once, then O(1) for rendering | |
| 257 static void BuildLineCache(TextAreaState *state, const char *text, float content_width, | |
| 258 int font_size, boolean wrap, Dowa_Arena *arena) { | |
| 259 int text_len = strlen(text); | |
| 260 | |
| 261 // Check if cache is still valid | |
| 262 int new_hash = SimpleTextHash(text, text_len); | |
| 263 if (state->cache_valid && | |
| 264 state->cached_text_len == text_len && | |
| 265 state->cached_text_hash == new_hash && | |
| 266 state->cached_content_width == content_width) { | |
| 267 return; // Cache is valid, no need to rebuild | |
| 268 } | |
| 269 | |
| 270 // Clear old cache | |
| 271 if (state->line_cache) { | |
| 272 Dowa_Array_Free(state->line_cache); | |
| 273 state->line_cache = NULL; | |
| 274 } | |
| 275 | |
| 276 // Approximate character width (for non-monospace, use average) | |
| 277 float avg_char_width = font_size * 0.6f; // Reasonable approximation for most fonts | |
| 278 int max_chars_per_line = (int)(content_width / avg_char_width); | |
| 279 if (max_chars_per_line < 1) max_chars_per_line = 1; | |
| 280 | |
| 281 Dowa_Array_Reserve(state->line_cache, text_len / max_chars_per_line + 10); | |
| 282 | |
| 283 int line_start = 0; | |
| 284 int i = 0; | |
| 285 | |
| 286 while (i <= text_len) { | |
| 287 boolean is_end = (i == text_len); | |
| 288 boolean is_newline = (!is_end && text[i] == '\n'); | |
| 289 int chars_in_line = i - line_start; | |
| 290 | |
| 291 // Check if we need to wrap (based on character count approximation) | |
| 292 boolean should_wrap = wrap && !is_end && !is_newline && | |
| 293 chars_in_line >= max_chars_per_line; | |
| 294 | |
| 295 if (is_end || is_newline || should_wrap) { | |
| 296 LineInfo line = { | |
| 297 .start_pos = line_start, | |
| 298 .end_pos = i, | |
| 299 .char_count = chars_in_line | |
| 300 }; | |
| 301 Dowa_Array_Push(state->line_cache, line); | |
| 302 | |
| 303 if (is_newline) { | |
| 304 line_start = i + 1; | |
| 305 } else if (should_wrap) { | |
| 306 // Try to find a space to wrap at | |
| 307 int wrap_pos = i; | |
| 308 for (int j = i - 1; j > line_start && j > i - 20; j--) { | |
| 309 if (text[j] == ' ') { | |
| 310 wrap_pos = j; | |
| 311 break; | |
| 312 } | |
| 313 } | |
| 314 // Update the last line entry with correct end | |
| 315 state->line_cache[Dowa_Array_Length(state->line_cache) - 1].end_pos = wrap_pos; | |
| 316 state->line_cache[Dowa_Array_Length(state->line_cache) - 1].char_count = wrap_pos - line_start; | |
| 317 line_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; | |
| 318 i = line_start - 1; // Will be incremented at end of loop | |
| 319 } else { | |
| 320 line_start = i + 1; | |
| 321 } | |
| 322 } | |
| 323 i++; | |
| 324 } | |
| 325 | |
| 326 // Ensure at least one line exists | |
| 327 if (Dowa_Array_Length(state->line_cache) == 0) { | |
| 328 LineInfo empty_line = { .start_pos = 0, .end_pos = 0, .char_count = 0 }; | |
| 329 Dowa_Array_Push(state->line_cache, empty_line); | |
| 330 } | |
| 331 | |
| 332 // Update cache metadata | |
| 333 state->cached_text_len = text_len; | |
| 334 state->cached_text_hash = new_hash; | |
| 335 state->cached_content_width = content_width; | |
| 336 state->cache_valid = TRUE; | |
| 337 } | |
| 338 | |
| 339 static int MeasureTextRange(const char *text, int start, int end, int font_size) { | |
| 340 if (start >= end) return 0; | |
| 341 | |
| 342 char temp[1024]; | |
| 343 int len = TA_Min_Int(end - start, 1023); | |
| 344 strncpy(temp, text + start, len); | |
| 345 temp[len] = '\0'; | |
| 346 | |
| 347 return MeasureTextEx(GuiGetFont(), temp, font_size, TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE)).x; | |
| 348 } | |
| 349 | |
| 350 static int GetCharIndexFromPos(const char *text, Rectangle bounds, Vector2 pos, | |
| 351 boolean wrap, float scroll_y, int font_size, int line_height) { | |
| 352 if (!text || strlen(text) == 0) return 0; | |
| 353 | |
| 354 float content_x = bounds.x + TEXT_AREA_PADDING; | |
| 355 float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y; | |
| 356 float content_width = bounds.width - TEXT_AREA_PADDING * 2; | |
| 357 | |
| 358 int text_len = strlen(text); | |
| 359 | |
| 360 float click_line_y = (pos.y - content_y) / line_height; | |
| 361 int target_visual_line = (int)click_line_y; | |
| 362 if (target_visual_line < 0) target_visual_line = 0; | |
| 363 | |
| 364 int current_visual_line = 0; | |
| 365 int i = 0; | |
| 366 int line_char_start = 0; | |
| 367 | |
| 368 while (i <= text_len) { | |
| 369 boolean is_newline = (i < text_len && text[i] == '\n'); | |
| 370 | |
| 371 if (wrap && i > line_char_start) { | |
| 372 int line_width = MeasureTextRange(text, line_char_start, i, font_size); | |
| 373 if (line_width > content_width && i > line_char_start + 1) { | |
| 374 int wrap_pos = i - 1; | |
| 375 for (int j = i - 1; j > line_char_start; j--) { | |
| 376 if (text[j] == ' ') { | |
| 377 wrap_pos = j; | |
| 378 break; | |
| 379 } | |
| 380 } | |
| 381 | |
| 382 if (current_visual_line == target_visual_line) { | |
| 383 float click_x = pos.x - content_x; | |
| 384 int best_pos = line_char_start; | |
| 385 float best_dist = 99999; | |
| 386 | |
| 387 for (int k = line_char_start; k <= wrap_pos; k++) { | |
| 388 int char_x = MeasureTextRange(text, line_char_start, k, font_size); | |
| 389 float dist = (float)(click_x - char_x); | |
| 390 if (dist < 0) dist = -dist; | |
| 391 if (dist < best_dist) { | |
| 392 best_dist = dist; | |
| 393 best_pos = k; | |
| 394 } | |
| 395 } | |
| 396 return best_pos; | |
| 397 } | |
| 398 | |
| 399 current_visual_line++; | |
| 400 line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; | |
| 401 i = line_char_start; | |
| 402 continue; | |
| 403 } | |
| 404 } | |
| 405 | |
| 406 if (is_newline || i == text_len) { | |
| 407 if (current_visual_line == target_visual_line || i == text_len) { | |
| 408 float click_x = pos.x - content_x; | |
| 409 int line_end = i; | |
| 410 int best_pos = line_char_start; | |
| 411 float best_dist = 99999; | |
| 412 | |
| 413 for (int k = line_char_start; k <= line_end; k++) { | |
| 414 int char_x = MeasureTextRange(text, line_char_start, k, font_size); | |
| 415 float dist = (float)(click_x - char_x); | |
| 416 if (dist < 0) dist = -dist; | |
| 417 if (dist < best_dist) { | |
| 418 best_dist = dist; | |
| 419 best_pos = k; | |
| 420 } | |
| 421 } | |
| 422 return TA_Min_Int(best_pos, text_len); | |
| 423 } | |
| 424 | |
| 425 current_visual_line++; | |
| 426 line_char_start = i + 1; | |
| 427 } | |
| 428 | |
| 429 i++; | |
| 430 } | |
| 431 | |
| 432 return text_len; | |
| 433 } | |
| 434 | |
| 435 // Get character index from mouse position using line cache | |
| 436 static int GetCharIndexFromPosWithCache(const char *text, Rectangle bounds, Vector2 pos, | |
| 437 float scroll_y, int font_size, int line_height, | |
| 438 LineInfo *line_cache) { | |
| 439 if (!text || strlen(text) == 0) return 0; | |
| 440 if (!line_cache || Dowa_Array_Length(line_cache) == 0) return 0; | |
| 441 | |
| 442 float content_x = bounds.x + TEXT_AREA_PADDING; | |
| 443 float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y; | |
| 444 | |
| 445 int text_len = strlen(text); | |
| 446 int line_count = Dowa_Array_Length(line_cache); | |
| 447 | |
| 448 // Find which visual line was clicked | |
| 449 float click_line_y = (pos.y - content_y) / line_height; | |
| 450 int target_line = (int)click_line_y; | |
| 451 if (target_line < 0) target_line = 0; | |
| 452 if (target_line >= line_count) target_line = line_count - 1; | |
| 453 if (target_line < 0) return 0; // Safety check | |
| 454 | |
| 455 LineInfo *line = &line_cache[target_line]; | |
| 456 | |
| 457 // Find character position within the line | |
| 458 float click_x = pos.x - content_x; | |
| 459 if (click_x < 0) return line->start_pos; | |
| 460 | |
| 461 // Linear search for the character closest to click position | |
| 462 int best_pos = line->start_pos; | |
| 463 float best_dist = 99999; | |
| 464 | |
| 465 for (int k = line->start_pos; k <= line->end_pos; k++) { | |
| 466 int char_x = MeasureTextRange(text, line->start_pos, k, font_size); | |
| 467 float dist = click_x - char_x; | |
| 468 if (dist < 0) dist = -dist; | |
| 469 | |
| 470 if (dist < best_dist) { | |
| 471 best_dist = dist; | |
| 472 best_pos = k; | |
| 473 } | |
| 474 } | |
| 475 | |
| 476 return TA_Min_Int(best_pos, text_len); | |
| 477 } | |
| 478 | |
| 479 // Get cursor position using line cache (fast O(log n) or O(n) worst case) | |
| 480 static Vector2 GetCursorScreenPosFromCache(const char *text, int cursor_pos, Rectangle bounds, | |
| 481 float scroll_y, int font_size, int line_height, | |
| 482 LineInfo *line_cache) { | |
| 483 float content_x = bounds.x + TEXT_AREA_PADDING; | |
| 484 float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y; | |
| 485 | |
| 486 if (!text || cursor_pos == 0) { | |
| 487 return (Vector2){content_x, content_y}; | |
| 488 } | |
| 489 if (!line_cache || Dowa_Array_Length(line_cache) == 0) { | |
| 490 return (Vector2){content_x, content_y}; | |
| 491 } | |
| 492 | |
| 493 int text_len = strlen(text); | |
| 494 cursor_pos = TA_Min_Int(cursor_pos, text_len); | |
| 495 | |
| 496 // Find which line contains the cursor | |
| 497 int line_count = Dowa_Array_Length(line_cache); | |
| 498 for (int i = 0; i < line_count; i++) { | |
| 499 LineInfo *line = &line_cache[i]; | |
| 500 | |
| 501 // Cursor is within this line or at the end of this line | |
| 502 if (cursor_pos >= line->start_pos && cursor_pos <= line->end_pos) { | |
| 503 float x = content_x + MeasureTextRange(text, line->start_pos, cursor_pos, font_size); | |
| 504 float y = content_y + i * line_height; | |
| 505 return (Vector2){x, y}; | |
| 506 } | |
| 507 } | |
| 508 | |
| 509 // Fallback: cursor at the end | |
| 510 int last_line = line_count > 0 ? line_count - 1 : 0; | |
| 511 LineInfo *line = &line_cache[last_line]; | |
| 512 float x = content_x + MeasureTextRange(text, line->start_pos, cursor_pos, font_size); | |
| 513 float y = content_y + last_line * line_height; | |
| 514 return (Vector2){x, y}; | |
| 515 } | |
| 516 | |
| 517 static float GetContentHeight(const char *text, Rectangle bounds, boolean wrap, | |
| 518 int font_size, int line_height) { | |
| 519 if (!text || strlen(text) == 0) return line_height; | |
| 520 | |
| 521 float content_width = bounds.width - TEXT_AREA_PADDING * 2; | |
| 522 int visual_lines = 1; | |
| 523 int line_char_start = 0; | |
| 524 int text_len = strlen(text); | |
| 525 | |
| 526 for (int i = 0; i <= text_len; i++) { | |
| 527 if (i == text_len || text[i] == '\n') { | |
| 528 visual_lines++; | |
| 529 line_char_start = i + 1; | |
| 530 } else if (wrap) { | |
| 531 int line_width = MeasureTextRange(text, line_char_start, i + 1, font_size); | |
| 532 if (line_width > content_width && i > line_char_start) { | |
| 533 int wrap_pos = i; | |
| 534 for (int j = i; j > line_char_start; j--) { | |
| 535 if (text[j] == ' ') { | |
| 536 wrap_pos = j; | |
| 537 break; | |
| 538 } | |
| 539 } | |
| 540 visual_lines++; | |
| 541 line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; | |
| 542 } | |
| 543 } | |
| 544 } | |
| 545 | |
| 546 return visual_lines * line_height; | |
| 547 } | |
| 548 | |
| 549 static void PushUndoState(TextAreaState *state, const char *text, Dowa_Arena *arena) { | |
| 550 TextAreaUndoEntry entry; | |
| 551 entry.text = Dowa_String_Copy_Arena((char*)text, arena); | |
| 552 entry.cursor_pos = state->cursor_pos; | |
| 553 entry.selection_start = state->selection_start; | |
| 554 entry.selection_end = state->selection_end; | |
| 555 | |
| 556 if (state->undo_index < (int)Dowa_Array_Length(state->undo_stack) - 1) { | |
| 557 dowa__header(state->undo_stack)->length = state->undo_index + 1; | |
| 558 } | |
| 559 | |
| 560 Dowa_Array_Push_Arena(state->undo_stack, entry, arena); | |
| 561 state->undo_index = Dowa_Array_Length(state->undo_stack) - 1; | |
| 562 | |
| 563 if (Dowa_Array_Length(state->undo_stack) > TEXT_AREA_MAX_UNDO_STATES) { | |
| 564 for (int i = 0; i < (int)Dowa_Array_Length(state->undo_stack) - 1; i++) { | |
| 565 state->undo_stack[i] = state->undo_stack[i + 1]; | |
| 566 } | |
| 567 dowa__header(state->undo_stack)->length--; | |
| 568 state->undo_index--; | |
| 569 } | |
| 570 } | |
| 571 | |
| 572 static boolean PerformUndo(TextAreaState *state, char *text, int text_size) { | |
| 573 if (state->undo_index <= 0) return FALSE; | |
| 574 | |
| 575 state->undo_index--; | |
| 576 TextAreaUndoEntry *entry = &state->undo_stack[state->undo_index]; | |
| 577 | |
| 578 strncpy(text, entry->text, text_size - 1); | |
| 579 text[text_size - 1] = '\0'; | |
| 580 state->cursor_pos = entry->cursor_pos; | |
| 581 state->selection_start = entry->selection_start; | |
| 582 state->selection_end = entry->selection_end; | |
| 583 | |
| 584 return TRUE; | |
| 585 } | |
| 586 | |
| 587 static boolean PerformRedo(TextAreaState *state, char *text, int text_size) { | |
| 588 if (state->undo_index >= (int)Dowa_Array_Length(state->undo_stack) - 1) return FALSE; | |
| 589 | |
| 590 state->undo_index++; | |
| 591 TextAreaUndoEntry *entry = &state->undo_stack[state->undo_index]; | |
| 592 | |
| 593 strncpy(text, entry->text, text_size - 1); | |
| 594 text[text_size - 1] = '\0'; | |
| 595 state->cursor_pos = entry->cursor_pos; | |
| 596 state->selection_start = entry->selection_start; | |
| 597 state->selection_end = entry->selection_end; | |
| 598 | |
| 599 return TRUE; | |
| 600 } | |
| 601 | |
| 602 static void InsertTextAtCursor(char *text, int text_size, int cursor_pos, | |
| 603 const char *insert_text, int *new_cursor_pos) { | |
| 604 int text_len = strlen(text); | |
| 605 int insert_len = strlen(insert_text); | |
| 606 | |
| 607 if (text_len + insert_len >= text_size - 1) { | |
| 608 insert_len = text_size - 1 - text_len; | |
| 609 if (insert_len <= 0) return; | |
| 610 } | |
| 611 | |
| 612 memmove(text + cursor_pos + insert_len, | |
| 613 text + cursor_pos, | |
| 614 text_len - cursor_pos + 1); | |
| 615 | |
| 616 memcpy(text + cursor_pos, insert_text, insert_len); | |
| 617 | |
| 618 *new_cursor_pos = cursor_pos + insert_len; | |
| 619 } | |
| 620 | |
| 621 static void DeleteTextRange(char *text, int start, int end) { | |
| 622 if (start >= end) return; | |
| 623 int text_len = strlen(text); | |
| 624 if (start < 0) start = 0; | |
| 625 if (end > text_len) end = text_len; | |
| 626 | |
| 627 memmove(text + start, text + end, text_len - end + 1); | |
| 628 } | |
| 629 | |
| 630 static char* GetSelectedText(const char *text, int sel_start, int sel_end, Dowa_Arena *arena) { | |
| 631 if (sel_start < 0 || sel_end < 0 || sel_start >= sel_end) return NULL; | |
| 632 | |
| 633 int len = sel_end - sel_start; | |
| 634 char *result = Dowa_Arena_Allocate(arena, len + 1); | |
| 635 strncpy(result, text + sel_start, len); | |
| 636 result[len] = '\0'; | |
| 637 return result; | |
| 638 } | |
| 639 | |
| 640 boolean GuiTextArea(int id, Rectangle bounds, char *text, int text_size, boolean is_edit_mode, | |
| 641 boolean should_text_wrap, boolean readonly, Dowa_Arena *arena) { | |
| 642 boolean should_toggle_edit_mode = FALSE; | |
| 643 | |
| 644 // Get or create state for this text area | |
| 645 TextAreaState *state = GetTextAreaState(id); | |
| 646 if (!state) { | |
| 647 state = CreateTextAreaState(id); | |
| 648 state->cursor_pos = readonly ? 0 : strlen(text); // Readonly starts at beginning | |
| 649 state->last_blink_time = GetTime(); | |
| 650 if (!readonly) PushUndoState(state, text, arena); | |
| 651 } | |
| 652 | |
| 653 int text_len = strlen(text); | |
| 654 Vector2 mouse_pos = GetMousePosition(); | |
| 655 boolean mouse_in_bounds = CheckCollisionPointRec(mouse_pos, bounds); | |
| 656 | |
| 657 // Handle click to enter/exit edit mode (readonly still allows selection mode) | |
| 658 if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { | |
| 659 if (mouse_in_bounds && !is_edit_mode) { | |
| 660 should_toggle_edit_mode = TRUE; | |
| 661 } else if (!mouse_in_bounds && is_edit_mode) { | |
| 662 should_toggle_edit_mode = TRUE; | |
| 663 } | |
| 664 } | |
| 665 | |
| 666 // Content area - build/update line cache | |
| 667 float content_width = bounds.width - TEXT_AREA_PADDING * 2; | |
| 668 BuildLineCache(state, text, content_width, GuiGetStyle(DEFAULT, TEXT_SIZE), should_text_wrap, arena); | |
| 669 | |
| 670 // Calculate content height from cache (O(1)) | |
| 671 int total_lines = Dowa_Array_Length(state->line_cache); | |
| 672 float content_height = total_lines * TEXT_AREA_LINE_HEIGHT; | |
| 673 float visible_height = bounds.height - TEXT_AREA_PADDING * 2; | |
| 674 float max_scroll = TA_Max_Float(0, content_height - visible_height); | |
| 675 | |
| 676 // Handle scrolling | |
| 677 float wheel = GetMouseWheelMove(); | |
| 678 if (mouse_in_bounds && wheel != 0) { | |
| 679 state->scroll_offset_y -= wheel * TEXT_AREA_LINE_HEIGHT * 3; | |
| 680 state->scroll_offset_y = TA_Max_Float(0, TA_Min_Float(state->scroll_offset_y, max_scroll)); | |
| 681 } | |
| 682 | |
| 683 if (is_edit_mode) { | |
| 684 boolean ctrl_pressed = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) || | |
| 685 IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER); | |
| 686 boolean shift_pressed = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); | |
| 687 boolean text_changed = FALSE; | |
| 688 | |
| 689 double current_time = GetTime(); | |
| 690 if (current_time - state->last_blink_time > 0.5) { | |
| 691 state->cursor_visible = !state->cursor_visible; | |
| 692 state->last_blink_time = current_time; | |
| 693 } | |
| 694 | |
| 695 // Mouse Selection (use line cache for accurate position) | |
| 696 if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && mouse_in_bounds) { | |
| 697 int click_pos = GetCharIndexFromPosWithCache(text, bounds, mouse_pos, | |
| 698 state->scroll_offset_y, GuiGetStyle(DEFAULT, TEXT_SIZE), | |
| 699 TEXT_AREA_LINE_HEIGHT, state->line_cache); | |
| 700 state->cursor_pos = click_pos; | |
| 701 state->selection_start = -1; | |
| 702 state->selection_end = -1; | |
| 703 state->is_selecting = TRUE; | |
| 704 state->cursor_visible = TRUE; | |
| 705 state->last_blink_time = current_time; | |
| 706 } | |
| 707 | |
| 708 if (state->is_selecting && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { | |
| 709 int drag_pos = GetCharIndexFromPosWithCache(text, bounds, mouse_pos, | |
| 710 state->scroll_offset_y, GuiGetStyle(DEFAULT, TEXT_SIZE), | |
| 711 TEXT_AREA_LINE_HEIGHT, state->line_cache); | |
| 712 if (drag_pos != state->cursor_pos) { | |
| 713 if (state->selection_start < 0) { | |
| 714 state->selection_start = state->cursor_pos; | |
| 715 } | |
| 716 state->selection_end = drag_pos; | |
| 717 } | |
| 718 } | |
| 719 | |
| 720 if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { | |
| 721 state->is_selecting = FALSE; | |
| 722 if (state->selection_start >= 0 && state->selection_end >= 0) { | |
| 723 if (state->selection_start > state->selection_end) { | |
| 724 int temp = state->selection_start; | |
| 725 state->selection_start = state->selection_end; | |
| 726 state->selection_end = temp; | |
| 727 } | |
| 728 if (state->selection_start == state->selection_end) { | |
| 729 state->selection_start = -1; | |
| 730 state->selection_end = -1; | |
| 731 } | |
| 732 } | |
| 733 } | |
| 734 | |
| 735 // Ctrl+A: Select All | |
| 736 if (ctrl_pressed && IsKeyPressed(KEY_A)) { | |
| 737 state->selection_start = 0; | |
| 738 state->selection_end = text_len; | |
| 739 state->cursor_pos = text_len; | |
| 740 } | |
| 741 | |
| 742 // Ctrl+C: Copy | |
| 743 if (ctrl_pressed && IsKeyPressed(KEY_C)) { | |
| 744 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 745 state->selection_start != state->selection_end) { | |
| 746 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 747 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 748 char *selected = GetSelectedText(text, sel_min, sel_max, arena); | |
| 749 if (selected) { | |
| 750 if (g_clipboard_text) free(g_clipboard_text); | |
| 751 g_clipboard_text = strdup(selected); | |
| 752 SetClipboardText(g_clipboard_text); | |
| 753 } | |
| 754 } | |
| 755 } | |
| 756 | |
| 757 // Ctrl+X: Cut (not allowed in readonly mode) | |
| 758 if (!readonly && ctrl_pressed && IsKeyPressed(KEY_X)) { | |
| 759 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 760 state->selection_start != state->selection_end) { | |
| 761 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 762 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 763 char *selected = GetSelectedText(text, sel_min, sel_max, arena); | |
| 764 if (selected) { | |
| 765 if (g_clipboard_text) free(g_clipboard_text); | |
| 766 g_clipboard_text = strdup(selected); | |
| 767 SetClipboardText(g_clipboard_text); | |
| 768 } | |
| 769 | |
| 770 PushUndoState(state, text, arena); | |
| 771 DeleteTextRange(text, sel_min, sel_max); | |
| 772 state->cursor_pos = sel_min; | |
| 773 state->selection_start = -1; | |
| 774 state->selection_end = -1; | |
| 775 text_changed = TRUE; | |
| 776 } | |
| 777 } | |
| 778 | |
| 779 // Ctrl+V: Paste (not allowed in readonly mode) | |
| 780 if (!readonly && ctrl_pressed && IsKeyPressed(KEY_V)) { | |
| 781 const char *clipboard = GetClipboardText(); | |
| 782 if (clipboard && strlen(clipboard) > 0) { | |
| 783 // Skip leading whitespace (spaces, tabs, newlines) | |
| 784 while (*clipboard && (*clipboard == ' ' || *clipboard == '\t' || | |
| 785 *clipboard == '\n' || *clipboard == '\r')) { | |
| 786 clipboard++; | |
| 787 } | |
| 788 | |
| 789 if (strlen(clipboard) > 0) { | |
| 790 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 791 state->selection_start != state->selection_end) { | |
| 792 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 793 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 794 PushUndoState(state, text, arena); | |
| 795 DeleteTextRange(text, sel_min, sel_max); | |
| 796 state->cursor_pos = sel_min; | |
| 797 state->selection_start = -1; | |
| 798 state->selection_end = -1; | |
| 799 } else { | |
| 800 PushUndoState(state, text, arena); | |
| 801 } | |
| 802 | |
| 803 int new_cursor; | |
| 804 InsertTextAtCursor(text, text_size, state->cursor_pos, clipboard, &new_cursor); | |
| 805 state->cursor_pos = new_cursor; | |
| 806 text_changed = TRUE; | |
| 807 } | |
| 808 } | |
| 809 } | |
| 810 | |
| 811 // Ctrl+Z: Undo / Ctrl+Shift+Z: Redo (not allowed in readonly mode) | |
| 812 if (!readonly && ctrl_pressed && IsKeyPressed(KEY_Z)) { | |
| 813 if (shift_pressed) { | |
| 814 PerformRedo(state, text, text_size); | |
| 815 } else { | |
| 816 PerformUndo(state, text, text_size); | |
| 817 } | |
| 818 } | |
| 819 | |
| 820 // Arrow Keys Navigation | |
| 821 if (IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) { | |
| 822 if (state->selection_start >= 0 && !shift_pressed) { | |
| 823 state->cursor_pos = TA_Min_Int(state->selection_start, state->selection_end); | |
| 824 state->selection_start = -1; | |
| 825 state->selection_end = -1; | |
| 826 } else if (state->cursor_pos > 0) { | |
| 827 if (shift_pressed) { | |
| 828 if (state->selection_start < 0) { | |
| 829 state->selection_start = state->cursor_pos; | |
| 830 state->selection_end = state->cursor_pos; | |
| 831 } | |
| 832 state->cursor_pos--; | |
| 833 state->selection_end = state->cursor_pos; | |
| 834 } else { | |
| 835 state->cursor_pos--; | |
| 836 } | |
| 837 } | |
| 838 state->cursor_visible = TRUE; | |
| 839 state->last_blink_time = current_time; | |
| 840 } | |
| 841 | |
| 842 if (IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) { | |
| 843 if (state->selection_start >= 0 && !shift_pressed) { | |
| 844 state->cursor_pos = TA_Max_Int(state->selection_start, state->selection_end); | |
| 845 state->selection_start = -1; | |
| 846 state->selection_end = -1; | |
| 847 } else if (state->cursor_pos < text_len) { | |
| 848 if (shift_pressed) { | |
| 849 if (state->selection_start < 0) { | |
| 850 state->selection_start = state->cursor_pos; | |
| 851 state->selection_end = state->cursor_pos; | |
| 852 } | |
| 853 state->cursor_pos++; | |
| 854 state->selection_end = state->cursor_pos; | |
| 855 } else { | |
| 856 state->cursor_pos++; | |
| 857 } | |
| 858 } | |
| 859 state->cursor_visible = TRUE; | |
| 860 state->last_blink_time = current_time; | |
| 861 } | |
| 862 | |
| 863 if (IsKeyPressed(KEY_UP) || IsKeyPressedRepeat(KEY_UP)) { | |
| 864 int line, col; | |
| 865 GetLineAndColumn(text, state->cursor_pos, &line, &col); | |
| 866 if (line > 0) { | |
| 867 int new_pos = GetPosFromLineColumn(text, line - 1, col); | |
| 868 if (shift_pressed) { | |
| 869 if (state->selection_start < 0) { | |
| 870 state->selection_start = state->cursor_pos; | |
| 871 state->selection_end = state->cursor_pos; | |
| 872 } | |
| 873 state->selection_end = new_pos; | |
| 874 } else { | |
| 875 state->selection_start = -1; | |
| 876 state->selection_end = -1; | |
| 877 } | |
| 878 state->cursor_pos = new_pos; | |
| 879 } | |
| 880 state->cursor_visible = TRUE; | |
| 881 state->last_blink_time = current_time; | |
| 882 } | |
| 883 | |
| 884 if (IsKeyPressed(KEY_DOWN) || IsKeyPressedRepeat(KEY_DOWN)) { | |
| 885 int line, col; | |
| 886 GetLineAndColumn(text, state->cursor_pos, &line, &col); | |
| 887 int total_lines = CountLines(text); | |
| 888 if (line < total_lines - 1) { | |
| 889 int new_pos = GetPosFromLineColumn(text, line + 1, col); | |
| 890 if (shift_pressed) { | |
| 891 if (state->selection_start < 0) { | |
| 892 state->selection_start = state->cursor_pos; | |
| 893 state->selection_end = state->cursor_pos; | |
| 894 } | |
| 895 state->selection_end = new_pos; | |
| 896 } else { | |
| 897 state->selection_start = -1; | |
| 898 state->selection_end = -1; | |
| 899 } | |
| 900 state->cursor_pos = new_pos; | |
| 901 } | |
| 902 state->cursor_visible = TRUE; | |
| 903 state->last_blink_time = current_time; | |
| 904 } | |
| 905 | |
| 906 // Home/End keys | |
| 907 if (IsKeyPressed(KEY_HOME)) { | |
| 908 int line_start = GetLineStart(text, state->cursor_pos); | |
| 909 if (shift_pressed) { | |
| 910 if (state->selection_start < 0) { | |
| 911 state->selection_start = state->cursor_pos; | |
| 912 state->selection_end = state->cursor_pos; | |
| 913 } | |
| 914 state->selection_end = line_start; | |
| 915 } else { | |
| 916 state->selection_start = -1; | |
| 917 state->selection_end = -1; | |
| 918 } | |
| 919 state->cursor_pos = line_start; | |
| 920 } | |
| 921 | |
| 922 if (IsKeyPressed(KEY_END)) { | |
| 923 int line_end = GetLineEnd(text, state->cursor_pos); | |
| 924 if (shift_pressed) { | |
| 925 if (state->selection_start < 0) { | |
| 926 state->selection_start = state->cursor_pos; | |
| 927 state->selection_end = state->cursor_pos; | |
| 928 } | |
| 929 state->selection_end = line_end; | |
| 930 } else { | |
| 931 state->selection_start = -1; | |
| 932 state->selection_end = -1; | |
| 933 } | |
| 934 state->cursor_pos = line_end; | |
| 935 } | |
| 936 | |
| 937 // Text Input (not allowed in readonly mode) | |
| 938 if (!readonly && !ctrl_pressed) { | |
| 939 int key = GetCharPressed(); | |
| 940 while (key > 0) { | |
| 941 if (key >= 32 && key <= 126) { | |
| 942 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 943 state->selection_start != state->selection_end) { | |
| 944 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 945 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 946 PushUndoState(state, text, arena); | |
| 947 DeleteTextRange(text, sel_min, sel_max); | |
| 948 state->cursor_pos = sel_min; | |
| 949 state->selection_start = -1; | |
| 950 state->selection_end = -1; | |
| 951 text_changed = TRUE; | |
| 952 } else if (!text_changed) { | |
| 953 PushUndoState(state, text, arena); | |
| 954 } | |
| 955 | |
| 956 char insert_str[2] = {(char)key, '\0'}; | |
| 957 int new_cursor; | |
| 958 InsertTextAtCursor(text, text_size, state->cursor_pos, insert_str, &new_cursor); | |
| 959 state->cursor_pos = new_cursor; | |
| 960 text_changed = TRUE; | |
| 961 } | |
| 962 key = GetCharPressed(); | |
| 963 } | |
| 964 } else if (readonly) { | |
| 965 // Consume key presses to prevent them from being handled elsewhere | |
| 966 while (GetCharPressed() > 0) {} | |
| 967 } | |
| 968 | |
| 969 // Enter key (not allowed in readonly mode) | |
| 970 if (!readonly && (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER))) { | |
| 971 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 972 state->selection_start != state->selection_end) { | |
| 973 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 974 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 975 PushUndoState(state, text, arena); | |
| 976 DeleteTextRange(text, sel_min, sel_max); | |
| 977 state->cursor_pos = sel_min; | |
| 978 state->selection_start = -1; | |
| 979 state->selection_end = -1; | |
| 980 } else { | |
| 981 PushUndoState(state, text, arena); | |
| 982 } | |
| 983 | |
| 984 int new_cursor; | |
| 985 InsertTextAtCursor(text, text_size, state->cursor_pos, "\n", &new_cursor); | |
| 986 state->cursor_pos = new_cursor; | |
| 987 text_changed = TRUE; | |
| 988 } | |
| 989 | |
| 990 // Backspace (not allowed in readonly mode) | |
| 991 if (!readonly && (IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE))) { | |
| 992 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 993 state->selection_start != state->selection_end) { | |
| 994 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 995 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 996 PushUndoState(state, text, arena); | |
| 997 DeleteTextRange(text, sel_min, sel_max); | |
| 998 state->cursor_pos = sel_min; | |
| 999 state->selection_start = -1; | |
| 1000 state->selection_end = -1; | |
| 1001 text_changed = TRUE; | |
| 1002 } else if (state->cursor_pos > 0) { | |
| 1003 PushUndoState(state, text, arena); | |
| 1004 DeleteTextRange(text, state->cursor_pos - 1, state->cursor_pos); | |
| 1005 state->cursor_pos--; | |
| 1006 text_changed = TRUE; | |
| 1007 } | |
| 1008 } | |
| 1009 | |
| 1010 // Delete key (not allowed in readonly mode) | |
| 1011 if (!readonly && (IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE))) { | |
| 1012 text_len = strlen(text); | |
| 1013 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 1014 state->selection_start != state->selection_end) { | |
| 1015 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 1016 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 1017 PushUndoState(state, text, arena); | |
| 1018 DeleteTextRange(text, sel_min, sel_max); | |
| 1019 state->cursor_pos = sel_min; | |
| 1020 state->selection_start = -1; | |
| 1021 state->selection_end = -1; | |
| 1022 text_changed = TRUE; | |
| 1023 } else if (state->cursor_pos < text_len) { | |
| 1024 PushUndoState(state, text, arena); | |
| 1025 DeleteTextRange(text, state->cursor_pos, state->cursor_pos + 1); | |
| 1026 text_changed = TRUE; | |
| 1027 } | |
| 1028 } | |
| 1029 | |
| 1030 // Tab key (not allowed in readonly mode) | |
| 1031 if (!readonly && IsKeyPressed(KEY_TAB)) { | |
| 1032 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 1033 state->selection_start != state->selection_end) { | |
| 1034 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 1035 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 1036 PushUndoState(state, text, arena); | |
| 1037 DeleteTextRange(text, sel_min, sel_max); | |
| 1038 state->cursor_pos = sel_min; | |
| 1039 state->selection_start = -1; | |
| 1040 state->selection_end = -1; | |
| 1041 } else { | |
| 1042 PushUndoState(state, text, arena); | |
| 1043 } | |
| 1044 | |
| 1045 int new_cursor; | |
| 1046 InsertTextAtCursor(text, text_size, state->cursor_pos, " ", &new_cursor); | |
| 1047 state->cursor_pos = new_cursor; | |
| 1048 text_changed = TRUE; | |
| 1049 } | |
| 1050 | |
| 1051 // Auto-scroll to keep cursor visible | |
| 1052 // Rebuild cache if text changed during editing | |
| 1053 if (text_changed) { | |
| 1054 state->cache_valid = FALSE; | |
| 1055 BuildLineCache(state, text, content_width, GuiGetStyle(DEFAULT, TEXT_SIZE), should_text_wrap, arena); | |
| 1056 } | |
| 1057 Vector2 cursor_screen = GetCursorScreenPosFromCache(text, state->cursor_pos, bounds, | |
| 1058 state->scroll_offset_y, | |
| 1059 GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_AREA_LINE_HEIGHT, | |
| 1060 state->line_cache); | |
| 1061 | |
| 1062 float visible_top = bounds.y + TEXT_AREA_PADDING; | |
| 1063 float visible_bottom = bounds.y + bounds.height - TEXT_AREA_PADDING - TEXT_AREA_LINE_HEIGHT; | |
| 1064 | |
| 1065 if (cursor_screen.y < visible_top) { | |
| 1066 state->scroll_offset_y -= visible_top - cursor_screen.y; | |
| 1067 } else if (cursor_screen.y > visible_bottom) { | |
| 1068 state->scroll_offset_y += cursor_screen.y - visible_bottom; | |
| 1069 } | |
| 1070 | |
| 1071 state->scroll_offset_y = TA_Max_Float(0, TA_Min_Float(state->scroll_offset_y, max_scroll)); | |
| 1072 } | |
| 1073 | |
| 1074 // Drawing | |
| 1075 DrawRectangleSelectiveRounded(bounds, 0.2, 1, g_colors.secondary, FALSE, FALSE, TRUE, TRUE); | |
| 1076 DrawRectangleRec(AddPadding(bounds, 10), WHITE); | |
| 1077 // DrawRectangleRec(bounds, is_edit_mode ? DARKGRAY : (Color){40, 40, 40, 255}); | |
| 1078 // DrawRectangleLinesEx(bounds, 1, is_edit_mode ? WHITE : GRAY); | |
| 1079 // DrawRectangleRoundedLines(bounds, 0.2, 1, is_edit_mode ? BLACK : GRAY); | |
| 1080 | |
| 1081 BeginScissorMode((int)bounds.x, (int)bounds.y, (int)bounds.width, (int)bounds.height); | |
| 1082 | |
| 1083 float content_x = bounds.x + TEXT_AREA_PADDING; | |
| 1084 float content_y = bounds.y + TEXT_AREA_PADDING - state->scroll_offset_y; | |
| 1085 | |
| 1086 text_len = strlen(text); | |
| 1087 | |
| 1088 // Calculate visible line range (only render what's visible) | |
| 1089 int first_visible_line = (int)(state->scroll_offset_y / TEXT_AREA_LINE_HEIGHT); | |
| 1090 int last_visible_line = first_visible_line + (int)(visible_height / TEXT_AREA_LINE_HEIGHT) + 2; | |
| 1091 int line_count = Dowa_Array_Length(state->line_cache); | |
| 1092 if (first_visible_line < 0) first_visible_line = 0; | |
| 1093 if (last_visible_line > line_count) last_visible_line = line_count; | |
| 1094 | |
| 1095 // Draw selection highlight (only for visible lines) | |
| 1096 if (state->selection_start >= 0 && state->selection_end >= 0 && | |
| 1097 state->selection_start != state->selection_end) { | |
| 1098 int sel_min = TA_Min_Int(state->selection_start, state->selection_end); | |
| 1099 int sel_max = TA_Max_Int(state->selection_start, state->selection_end); | |
| 1100 | |
| 1101 for (int line_idx = first_visible_line; line_idx < last_visible_line; line_idx++) { | |
| 1102 LineInfo *line = &state->line_cache[line_idx]; | |
| 1103 | |
| 1104 // Check if selection intersects this line | |
| 1105 if (sel_min < line->end_pos && sel_max > line->start_pos) { | |
| 1106 int highlight_start = TA_Max_Int(sel_min, line->start_pos); | |
| 1107 int highlight_end = TA_Min_Int(sel_max, line->end_pos); | |
| 1108 | |
| 1109 float x1 = content_x + MeasureTextRange(text, line->start_pos, highlight_start, GuiGetStyle(DEFAULT, TEXT_SIZE)); | |
| 1110 float x2 = content_x + MeasureTextRange(text, line->start_pos, highlight_end, GuiGetStyle(DEFAULT, TEXT_SIZE)); | |
| 1111 float y = content_y + line_idx * TEXT_AREA_LINE_HEIGHT; | |
| 1112 | |
| 1113 DrawRectangle((int)x1, (int)y, (int)(x2 - x1), TEXT_AREA_LINE_HEIGHT, | |
| 1114 Fade(SKYBLUE, 0.5f)); | |
| 1115 } | |
| 1116 } | |
| 1117 } | |
| 1118 | |
| 1119 // Draw text using line cache (only visible lines) | |
| 1120 Color text_color = should_text_wrap ? BLACK : WHITE; | |
| 1121 for (int line_idx = first_visible_line; line_idx < last_visible_line; line_idx++) { | |
| 1122 LineInfo *line = &state->line_cache[line_idx]; | |
| 1123 | |
| 1124 if (line->char_count > 0) { | |
| 1125 char line_buffer[1024]; | |
| 1126 int line_len = TA_Min_Int(line->end_pos - line->start_pos, 1023); | |
| 1127 strncpy(line_buffer, text + line->start_pos, line_len); | |
| 1128 line_buffer[line_len] = '\0'; | |
| 1129 | |
| 1130 Vector2 draw_text_vector = { | |
| 1131 .x = content_x, | |
| 1132 .y = content_y + line_idx * TEXT_AREA_LINE_HEIGHT | |
| 1133 }; | |
| 1134 DrawTextEx(GuiGetFont(), line_buffer, draw_text_vector, | |
| 1135 GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), text_color); | |
| 1136 } | |
| 1137 } | |
| 1138 | |
| 1139 // Draw cursor | |
| 1140 if (is_edit_mode && state->cursor_visible) { | |
| 1141 Vector2 cursor_pos = GetCursorScreenPosFromCache(text, state->cursor_pos, bounds, | |
| 1142 state->scroll_offset_y, | |
| 1143 GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_AREA_LINE_HEIGHT, | |
| 1144 state->line_cache); | |
| 1145 | |
| 1146 DrawRectangle((int)cursor_pos.x, (int)cursor_pos.y, | |
| 1147 TEXT_AREA_CURSOR_WIDTH, TEXT_AREA_LINE_HEIGHT, BLACK); | |
| 1148 } | |
| 1149 | |
| 1150 EndScissorMode(); | |
| 1151 | |
| 1152 // Draw scrollbar if needed | |
| 1153 if (max_scroll > 0) { | |
| 1154 float scrollbar_height = (visible_height / content_height) * visible_height; | |
| 1155 float scrollbar_y = bounds.y + TEXT_AREA_PADDING + | |
| 1156 (state->scroll_offset_y / max_scroll) * (visible_height - scrollbar_height); | |
| 1157 | |
| 1158 DrawRectangle((int)(bounds.x + bounds.width - 8), (int)scrollbar_y, | |
| 1159 6, (int)scrollbar_height, Fade(WHITE, 0.3f)); | |
| 1160 } | |
| 1161 | |
| 1162 return should_toggle_edit_mode; | |
| 1163 } | |
| 1164 | |
| 1165 void GuiTextAreaResetState(int id) { | |
| 1166 TextAreaState *state = GetTextAreaState(id); | |
| 1167 if (state) { | |
| 1168 if (state->undo_stack) { | |
| 1169 Dowa_Array_Free(state->undo_stack); | |
| 1170 state->undo_stack = NULL; | |
| 1171 } | |
| 1172 if (state->line_cache) { | |
| 1173 Dowa_Array_Free(state->line_cache); | |
| 1174 state->line_cache = NULL; | |
| 1175 } | |
| 1176 state->cache_valid = FALSE; | |
| 1177 state->is_initialized = FALSE; | |
| 1178 } | |
| 1179 } | |
| 1180 | |
| 1181 void GuiTextAreaResetAllStates(void) { | |
| 1182 for (int i = 0; i < g_text_area_state_count; i++) { | |
| 1183 if (g_text_area_states[i].undo_stack) { | |
| 1184 Dowa_Array_Free(g_text_area_states[i].undo_stack); | |
| 1185 g_text_area_states[i].undo_stack = NULL; | |
| 1186 } | |
| 1187 if (g_text_area_states[i].line_cache) { | |
| 1188 Dowa_Array_Free(g_text_area_states[i].line_cache); | |
| 1189 g_text_area_states[i].line_cache = NULL; | |
| 1190 } | |
| 1191 g_text_area_states[i].cache_valid = FALSE; | |
| 1192 g_text_area_states[i].is_initialized = FALSE; | |
| 1193 } | |
| 1194 g_text_area_state_count = 0; | |
| 1195 } | |
| 1196 | |
| 1197 // ============================================================================ | |
| 1198 // End TextArea Component | |
| 1199 // ============================================================================ | |
| 39 | 1200 |
| 40 typedef Dowa_KV(char*, char*) INPUT_HASHMAP; | 1201 typedef Dowa_KV(char*, char*) INPUT_HASHMAP; |
| 41 | 1202 |
| 42 typedef struct { | 1203 typedef struct { |
| 43 char *data; | 1204 char *data; |
| 44 size_t size; | 1205 size_t size; |
| 45 } ResponseBuffer; | 1206 } ResponseBuffer; |
| 46 | 1207 |
| 47 typedef struct { | 1208 typedef struct { |
| 48 char *filename; | 1209 char *filename; |
| 49 char *title; | 1210 char *title; |
| 50 Rectangle rect; | 1211 Rectangle rect; |
| 51 long time_modified; | 1212 long time_modified; |
| 52 boolean deleted; | 1213 boolean deleted; |
| 53 } HistoryItem; | 1214 } HistoryItem; |
| 54 | 1215 |
| 60 | 1221 |
| 61 typedef enum { | 1222 typedef enum { |
| 62 TAB_HEADER = 0, | 1223 TAB_HEADER = 0, |
| 63 TAB_BODY, | 1224 TAB_BODY, |
| 64 TAB_GET_PARAMS, | 1225 TAB_GET_PARAMS, |
| 65 TAB_BAR, | 1226 TAB_WEBSOCKET, |
| 66 TAB_LENGTH | 1227 TAB_LENGTH |
| 67 } PostDog_Tab_Enum; | 1228 } PostDog_Tab_Enum; |
| 68 | 1229 |
| 1230 typedef enum { | |
| 1231 RESULT_TAB_BODY = 0, | |
| 1232 RESULT_TAB_HEADERS, | |
| 1233 RESULT_TAB_LENGTH | |
| 1234 } PostDog_ResultTab_Enum; | |
| 1235 | |
| 1236 // Text area IDs | |
| 1237 #define TEXT_AREA_ID_INPUT_HEADER 1 | |
| 1238 #define TEXT_AREA_ID_INPUT_BODY 2 | |
| 1239 #define TEXT_AREA_ID_INPUT_PARAMS 3 | |
| 1240 #define TEXT_AREA_ID_INPUT_WS 4 | |
| 1241 #define TEXT_AREA_ID_RESULT_BODY 5 | |
| 1242 #define TEXT_AREA_ID_RESULT_HEADERS 6 | |
| 1243 | |
| 69 static uint32 counter = 0; | 1244 static uint32 counter = 0; |
| 1245 static uv_mutex_t history_mutex; | |
| 1246 static uv_loop_t *main_loop = NULL; | |
| 70 HistoryItem *history_items = NULL; | 1247 HistoryItem *history_items = NULL; |
| 71 HistoryItem *new_history_items = NULL; | 1248 HistoryItem *new_history_items = NULL; |
| 72 | 1249 |
| 73 // Global UI state | 1250 // Global UI state |
| 74 char *url_input_text = NULL; | 1251 char *url_input_text = NULL; |
| 75 char *url_result_text = NULL; | 1252 char **result_body_array = NULL; // [RESULT_TAB_BODY, RESULT_TAB_HEADERS] |
| 76 char **url_body_map = NULL; | 1253 char **input_body_array = NULL; |
| 77 int active_method_dropdown = 0; | 1254 int active_method_dropdown = 0; |
| 1255 int active_input_tab = 0; | |
| 1256 int active_result_tab = 0; | |
| 1257 Seobeo_WebSocket *ws = NULL; | |
| 1258 boolean WS_BREAK = FALSE; | |
| 1259 uv_thread_t websocket_thread_id; | |
| 1260 Color TEXT_COLOR = BLACK; | |
| 1261 boolean LOADING = FALSE; | |
| 78 | 1262 |
| 79 int CompareHistoryItemsByDate(const void *a, const void *b) { | 1263 int CompareHistoryItemsByDate(const void *a, const void *b) { |
| 80 HistoryItem *itemA = (HistoryItem *)a; | 1264 HistoryItem *itemA = (HistoryItem *)a; |
| 81 HistoryItem *itemB = (HistoryItem *)b; | 1265 HistoryItem *itemB = (HistoryItem *)b; |
| 82 return (itemB->time_modified - itemA->time_modified); | 1266 return (itemB->time_modified - itemA->time_modified); |
| 86 { | 1270 { |
| 87 char full_file_path[512] = {0}; | 1271 char full_file_path[512] = {0}; |
| 88 snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); | 1272 snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); |
| 89 FILE *file = fopen(full_file_path, "r"); | 1273 FILE *file = fopen(full_file_path, "r"); |
| 90 if (!file) | 1274 if (!file) |
| 91 return strdup(filename); | 1275 return strdup(filename); |
| 92 | 1276 |
| 93 char *title = malloc(sizeof(char) * 512); | 1277 char *title = malloc(sizeof(char) * 512); |
| 94 if (!fgets(title, 512, file)) { | 1278 if (!fgets(title, 512, file)) { |
| 95 fclose(file); | 1279 fclose(file); |
| 96 return strdup(filename); | 1280 free(title); |
| 97 } | 1281 return strdup(filename); |
| 1282 } | |
| 1283 fclose(file); | |
| 1284 | |
| 1285 // Strip trailing newline | |
| 1286 title[strcspn(title, "\n")] = '\0'; | |
| 98 | 1287 |
| 99 return title; | 1288 return title; |
| 100 } | 1289 } |
| 101 | 1290 |
| 102 // TODO: Make this into generic fucntion so I can use it across different thing. | 1291 // TODO: Make this into generic fucntion so I can use it across different thing. |
| 129 if (dp == NULL) return; | 1318 if (dp == NULL) return; |
| 130 | 1319 |
| 131 char full_path[256]; | 1320 char full_path[256]; |
| 132 while ((entry = readdir(dp))) | 1321 while ((entry = readdir(dp))) |
| 133 { | 1322 { |
| 134 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) | 1323 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) |
| 135 continue; | 1324 continue; |
| 136 snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name); | 1325 snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name); |
| 137 if (stat(full_path, &file_stat) == 0) | 1326 if (stat(full_path, &file_stat) == 0) |
| 138 { | 1327 { |
| 139 HistoryItem item = {0}; | 1328 HistoryItem item = {0}; |
| 140 item.filename = strdup(entry->d_name); | 1329 item.filename = strdup(entry->d_name); |
| 141 item.title = PostDog_Extract_Title(entry->d_name); | 1330 item.title = PostDog_Extract_Title(entry->d_name); |
| 142 item.time_modified = file_stat.st_mtime; | 1331 item.time_modified = file_stat.st_mtime; |
| 143 item.deleted = FALSE; | 1332 item.deleted = FALSE; |
| 144 Dowa_Array_Push(file_arr, item); | 1333 Dowa_Array_Push(file_arr, item); |
| 145 } | 1334 } |
| 146 } | 1335 } |
| 147 closedir(dp); | 1336 closedir(dp); |
| 148 #endif | 1337 #endif |
| 1338 | |
| 1339 // Update the caller's pointer in case array was reallocated | |
| 1340 *p_file_arr = file_arr; | |
| 1341 | |
| 149 int count = Dowa_Array_Length(file_arr); | 1342 int count = Dowa_Array_Length(file_arr); |
| 150 if (count > 1) { | 1343 if (count > 1) { |
| 151 qsort(file_arr, count, sizeof(HistoryItem), CompareHistoryItemsByDate); | 1344 qsort(file_arr, count, sizeof(HistoryItem), CompareHistoryItemsByDate); |
| 152 } | 1345 } |
| 153 } | 1346 } |
| 154 | 1347 |
| 155 int PostDog_History_Load(HistoryItem **p_history_files) | 1348 int PostDog_History_Load(HistoryItem **p_history_files) |
| 156 { | 1349 { |
| 159 printf("Directory '%s' not found. Creating it...\n", POSTDOG_PATHS); | 1352 printf("Directory '%s' not found. Creating it...\n", POSTDOG_PATHS); |
| 160 if (mkdir(POSTDOG_PATHS, 0777) != 0) | 1353 if (mkdir(POSTDOG_PATHS, 0777) != 0) |
| 161 return -1; | 1354 return -1; |
| 162 return 0; | 1355 return 0; |
| 163 } | 1356 } |
| 1357 | |
| 164 printf("Directory '%s' already exists.\n", POSTDOG_PATHS); | 1358 printf("Directory '%s' already exists.\n", POSTDOG_PATHS); |
| 165 PostDog_List_Directory(POSTDOG_PATHS, p_history_files); | 1359 PostDog_List_Directory(POSTDOG_PATHS, p_history_files); |
| 166 return 0; | 1360 return 0; |
| 167 } | 1361 } |
| 168 | 1362 |
| 169 bool InArea(Vector2 mouse_position, Rectangle area) | 1363 bool InArea(Vector2 mouse_position, Rectangle area) |
| 170 { | 1364 { |
| 171 return ( | 1365 return ( |
| 172 mouse_position.x >= area.x && | 1366 mouse_position.x >= area.x && |
| 173 mouse_position.x < area.x + area.width && | 1367 mouse_position.x < area.x + area.width && |
| 174 mouse_position.y >= area.y && | 1368 mouse_position.y >= area.y && |
| 175 mouse_position.y < area.y + area.height | 1369 mouse_position.y < area.y + area.height |
| 176 ); | 1370 ); |
| 177 } | 1371 } |
| 178 | 1372 |
| 179 bool Clicked(Vector2 mouse_position, Rectangle area) | 1373 bool Clicked(Vector2 mouse_position, Rectangle area) |
| 180 { | 1374 { |
| 181 return (InArea(mouse_position, area) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)); | 1375 return (InArea(mouse_position, area) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)); |
| 182 } | 1376 } |
| 1377 | |
| 1378 // -------- END of UI ---- // | |
| 183 | 1379 |
| 184 char *PostDog_Enum_To_String(int active_enum) | 1380 char *PostDog_Enum_To_String(int active_enum) |
| 185 { | 1381 { |
| 186 switch(active_enum) | 1382 switch(active_enum) |
| 187 { | 1383 { |
| 188 case 0: return "GET"; | 1384 case 0: return "GET"; |
| 189 case 1: return "POST"; | 1385 case 1: return "POST"; |
| 190 case 2: return "PUT"; | 1386 case 2: return "PUT"; |
| 191 case 3: return "DELETE"; | 1387 case 3: return "DELETE"; |
| 192 } | 1388 } |
| 193 return 0; | 1389 return 0; |
| 194 } | 1390 } |
| 195 | 1391 |
| 196 char *PostDog_Construct_URL(char *filename) | 1392 char *PostDog_Construct_URL(char *filename, char *out_buffer, size_t buffer_size) |
| 197 { | 1393 { |
| 198 char full_file_path[512] = {0}; | 1394 snprintf(out_buffer, buffer_size, "%s/%s", POSTDOG_PATHS, filename); |
| 199 snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); | 1395 return out_buffer; |
| 200 return &full_file_path; | 1396 } |
| 201 } | 1397 |
| 202 | 1398 boolean PostDog_History_CreateFile(char *filename, char* values) |
| 203 void PostDog_History_CreateFile(char *filename, char* values) | |
| 204 { | 1399 { |
| 205 char full_file_path[512] = {0}; | 1400 char full_file_path[512] = {0}; |
| 206 snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); | 1401 snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); |
| 207 FILE *file = fopen(full_file_path, "w"); | 1402 FILE *file = fopen(full_file_path, "w"); |
| 208 if (!file) | 1403 if (!file) |
| 209 { | 1404 { |
| 210 printf("Failed to create a file: %s\n", full_file_path); | 1405 fprintf(stderr, "Failed to create a file: %s\n", full_file_path); |
| 211 return; | 1406 return FALSE; |
| 212 } | 1407 } |
| 213 fwrite(values, 1, strlen(values), file); | 1408 fwrite(values, 1, strlen(values), file); |
| 214 fclose(file); | 1409 fclose(file); |
| 1410 return TRUE; | |
| 215 } | 1411 } |
| 216 | 1412 |
| 217 void PostDog_Request_SaveFile(void) | 1413 void PostDog_Request_SaveFile(void) |
| 218 { | 1414 { |
| 219 const char *method = PostDog_Enum_To_String(active_method_dropdown); | 1415 const char *method = PostDog_Enum_To_String(active_method_dropdown); |
| 220 size_t new_file_size = 1024 * 1024; | 1416 size_t new_file_size = 1024 * 1024; |
| 221 Dowa_Arena *arena = Dowa_Arena_Create(1024 * 1024 * 2); | 1417 Dowa_Arena *arena = Dowa_Arena_Create(1024 * 1024 * 2); |
| 222 char *new_file = Dowa_Arena_Allocate(arena, 1024 * 1024); | 1418 char *new_file = Dowa_Arena_Allocate(arena, 1024 * 1024); |
| 223 char *title = malloc(strlen(method) + strlen(url_input_text) + 2); | 1419 char *title = Dowa_Arena_Allocate(arena, strlen(method) + strlen(url_input_text) + 2); |
| 224 sprintf(title, "%s %s", method, url_input_text); | 1420 sprintf(title, "%s %s", method, url_input_text); |
| 225 snprintf( | 1421 snprintf( |
| 226 new_file, | 1422 new_file, |
| 227 new_file_size, | 1423 new_file_size, |
| 228 "%s\n" | 1424 "%s\n" |
| 229 "---\n" | 1425 "---\n" |
| 230 "%s\n" | 1426 "%s\n" |
| 231 "---\n" | 1427 "---\n" |
| 232 "%s\n" | 1428 "%s\n" |
| 233 "---\n" | 1429 "---\n" |
| 234 "%s\n" | 1430 "%s\n" |
| 235 "---\n" | 1431 "---\n" |
| 236 "%s\n" | 1432 "%s\n" |
| 237 "---\n" | 1433 "---\n" |
| 238 "%s\n" | 1434 "%s\n" |
| 239 "---\n" | 1435 "---\n" |
| 240 "%s\n" | 1436 "%s\n" |
| 241 "---\n" | 1437 "---\n" |
| 242 "%s\n", | 1438 "%s\n", |
| 243 title, | 1439 title, |
| 244 url_input_text, | 1440 url_input_text, |
| 245 method, | 1441 method, |
| 246 url_body_map[TAB_HEADER], | 1442 input_body_array[TAB_HEADER], |
| 247 url_body_map[TAB_BODY], | 1443 input_body_array[TAB_BODY], |
| 248 url_body_map[TAB_GET_PARAMS], | 1444 input_body_array[TAB_GET_PARAMS], |
| 249 url_body_map[TAB_BAR], | 1445 input_body_array[TAB_WEBSOCKET], |
| 250 url_result_text | 1446 result_body_array[RESULT_TAB_BODY] |
| 251 ); | 1447 ); |
| 252 char *filename = Dowa_Arena_Allocate(arena, 1024); | 1448 char *filename = Dowa_Arena_Allocate(arena, 1024); |
| 253 if (!filename) | 1449 if (!filename) |
| 254 { | 1450 { |
| 255 perror("Error opening file"); | 1451 perror("Error opening file"); |
| 256 exit(EXIT_FAILURE); | 1452 exit(EXIT_FAILURE); |
| 257 } | 1453 } |
| 258 char *uuid4 = (char *)Dowa_Arena_Allocate(arena, 37); | 1454 char *uuid4 = (char *)Dowa_Arena_Allocate(arena, 37); |
| 259 if (!uuid4) | 1455 if (!uuid4) |
| 260 { | 1456 { |
| 261 perror("Error uuid"); | 1457 perror("Error uuid"); |
| 262 exit(EXIT_FAILURE); | 1458 exit(EXIT_FAILURE); |
| 263 } | 1459 } |
| 264 | 1460 |
| 1461 uv_mutex_lock(&history_mutex); | |
| 1462 | |
| 265 int32 seed = (uint32)time(NULL) ^ counter++; | 1463 int32 seed = (uint32)time(NULL) ^ counter++; |
| 266 Dowa_String_UUID(seed, uuid4); | 1464 Dowa_String_UUID(seed, uuid4); |
| 267 snprintf(filename, 1024, "%s.txt", uuid4); | 1465 snprintf(filename, 1024, "%s.txt", uuid4); |
| 268 PostDog_History_CreateFile(filename, new_file); | 1466 |
| 269 | 1467 if (PostDog_History_CreateFile(filename, new_file)) |
| 1468 { | |
| 270 HistoryItem item = {0}; | 1469 HistoryItem item = {0}; |
| 271 item.filename = strdup(filename); | 1470 item.filename = strdup(filename); |
| 272 item.title = title; | 1471 item.title = strdup(title); |
| 273 item.deleted = FALSE; | 1472 item.deleted = FALSE; |
| 1473 | |
| 274 Dowa_Array_Push(new_history_items, item); | 1474 Dowa_Array_Push(new_history_items, item); |
| 275 | 1475 } |
| 1476 | |
| 1477 uv_mutex_unlock(&history_mutex); | |
| 276 Dowa_Arena_Free(arena); | 1478 Dowa_Arena_Free(arena); |
| 277 } | 1479 } |
| 278 | 1480 |
| 279 static size_t Postdog_Curl_Callback(void *contents, size_t size, size_t nmemb, void *userp) | 1481 void PostDog_Websocket_Listen(void) |
| 280 { | 1482 { |
| 281 size_t real_size = size * nmemb; | 1483 while (TRUE) |
| 282 ResponseBuffer *buf = (ResponseBuffer *)userp; | |
| 283 | |
| 284 char *ptr = realloc(buf->data, buf->size + real_size + 1); | |
| 285 if (ptr == NULL) | |
| 286 { | 1484 { |
| 287 printf("Not enough memory for response\n"); | 1485 if (WS_BREAK) break; |
| 288 return 0; | 1486 |
| 289 } | 1487 Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Receive(ws); |
| 290 | 1488 if (p_msg) |
| 291 buf->data = ptr; | 1489 { |
| 292 memcpy(&(buf->data[buf->size]), contents, real_size); | 1490 if (p_msg->opcode == SEOBEO_WS_OPCODE_TEXT) |
| 293 buf->size += real_size; | 1491 { |
| 294 buf->data[buf->size] = 0; | 1492 printf("Response: %.*s\n", (int)p_msg->length, (char*)p_msg->data); |
| 295 | 1493 char *body = result_body_array[RESULT_TAB_BODY]; |
| 296 return real_size; | 1494 size_t current_len = strlen(body); |
| 1495 snprintf(body + current_len, RESULT_BUFFER_LENGTH - current_len, | |
| 1496 "\n%s", (char*)p_msg->data); | |
| 1497 } | |
| 1498 Seobeo_WebSocket_Message_Destroy(p_msg); | |
| 1499 } | |
| 1500 usleep(10000); | |
| 1501 } | |
| 1502 return; | |
| 1503 } | |
| 1504 | |
| 1505 void PostDog_Websocket_Connect(void) | |
| 1506 { | |
| 1507 // Pass headers from the Headers tab when connecting | |
| 1508 const char *headers = input_body_array[TAB_HEADER]; | |
| 1509 if (headers && strlen(headers) > 0) | |
| 1510 ws = Seobeo_WebSocket_Connect_With_Headers(url_input_text, headers); | |
| 1511 else | |
| 1512 ws = Seobeo_WebSocket_Connect(url_input_text); | |
| 1513 | |
| 1514 result_body_array[RESULT_TAB_BODY][0] = '\0'; | |
| 1515 result_body_array[RESULT_TAB_HEADERS][0] = '\0'; | |
| 1516 | |
| 1517 // Reset result text area states | |
| 1518 GuiTextAreaResetState(TEXT_AREA_ID_RESULT_BODY); | |
| 1519 GuiTextAreaResetState(TEXT_AREA_ID_RESULT_HEADERS); | |
| 1520 } | |
| 1521 | |
| 1522 int PostDog_Websocket_Send(void) | |
| 1523 { | |
| 1524 char *body = result_body_array[RESULT_TAB_BODY]; | |
| 1525 if (Seobeo_WebSocket_Send_Text(ws, input_body_array[active_input_tab]) < 0) | |
| 1526 { | |
| 1527 snprintf(body + strlen(body), RESULT_BUFFER_LENGTH - strlen(body), | |
| 1528 "Failed to send message\n"); | |
| 1529 return -1; | |
| 1530 } | |
| 1531 snprintf(body + strlen(body), RESULT_BUFFER_LENGTH - strlen(body), | |
| 1532 "\n%s", input_body_array[active_input_tab]); | |
| 1533 return 0; | |
| 1534 } | |
| 1535 | |
| 1536 void PostDog_Websocket_Destroy(uv_thread_t thread_id) | |
| 1537 { | |
| 1538 Seobeo_WebSocket_Destroy(ws); | |
| 1539 uv_thread_join(&thread_id); | |
| 1540 } | |
| 1541 | |
| 1542 void PostDog_Websocket_Start(void *arg) | |
| 1543 { | |
| 1544 PostDog_Websocket_Connect(); | |
| 1545 PostDog_Websocket_Listen(); | |
| 1546 } | |
| 1547 | |
| 1548 uv_thread_t PostDog_Websocket_Start_Thread() | |
| 1549 { | |
| 1550 uv_thread_t thread_id; | |
| 1551 | |
| 1552 if (uv_thread_create(&thread_id, PostDog_Websocket_Start, NULL) != 0) | |
| 1553 { | |
| 1554 perror("Failed to create thread"); | |
| 1555 memset(&thread_id, 0, sizeof(thread_id)); | |
| 1556 return thread_id; | |
| 1557 } | |
| 1558 | |
| 1559 return thread_id; | |
| 297 } | 1560 } |
| 298 | 1561 |
| 299 int PostDog_Http_Request(void) | 1562 int PostDog_Http_Request(void) |
| 300 { | 1563 { |
| 301 const char *method = PostDog_Enum_To_String(active_method_dropdown); | 1564 Seobeo_Client_Request *req = Seobeo_Client_Request_Create(url_input_text); |
| 302 CURL *curl; | 1565 printf("URL: %s\n", url_input_text); |
| 303 CURLcode res; | 1566 Seobeo_Client_Response *res; |
| 304 ResponseBuffer buffer = { .data = malloc(1), .size = 0 }; | 1567 switch (active_method_dropdown) |
| 305 | |
| 306 url_result_text[0] = '\n'; | |
| 307 | |
| 308 if (buffer.data == NULL) | |
| 309 { | 1568 { |
| 310 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "Error: Failed to allocate memory"); | 1569 case 0: |
| 311 return -1; | 1570 { |
| 312 } | 1571 Seobeo_Client_Request_Set_Method(req, "GET"); |
| 313 buffer.data[0] = '\0'; | 1572 break; |
| 314 | 1573 } |
| 315 curl_global_init(CURL_GLOBAL_DEFAULT); | 1574 case 1: |
| 316 curl = curl_easy_init(); | 1575 { |
| 317 | 1576 Seobeo_Client_Request_Set_Method(req, "POST"); |
| 318 if (curl) | 1577 break; |
| 1578 } | |
| 1579 case 2: | |
| 1580 { | |
| 1581 Seobeo_Client_Request_Set_Method(req, "PUT"); | |
| 1582 break; | |
| 1583 } | |
| 1584 case 3: | |
| 1585 { | |
| 1586 Seobeo_Client_Request_Set_Method(req, "DELETE"); | |
| 1587 break; | |
| 1588 } | |
| 1589 } | |
| 1590 | |
| 1591 if (input_body_array[TAB_HEADER] && strlen(input_body_array[TAB_HEADER]) > 0) | |
| 319 { | 1592 { |
| 320 struct curl_slist *headerList = NULL; | 1593 char *headersCopy = strdup(input_body_array[TAB_HEADER]); |
| 321 | 1594 char *line = strtok(headersCopy, "\n"); |
| 322 // Set URL | 1595 while (line != NULL) |
| 323 curl_easy_setopt(curl, CURLOPT_URL, url_input_text); | |
| 324 | |
| 325 // Set HTTP method | |
| 326 if (strcmp(method, "POST") == 0) | |
| 327 { | 1596 { |
| 328 curl_easy_setopt(curl, CURLOPT_POST, 1L); | 1597 while (*line == ' ' || *line == '\t') line++; |
| 329 if (url_body_map[TAB_BODY] && strlen(url_body_map[TAB_BODY]) > 0) | 1598 if (strlen(line) > 0) |
| 330 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, url_body_map[TAB_BODY]); | 1599 Seobeo_Client_Request_Add_Header_Array(req, line); |
| 331 } | 1600 line = strtok(NULL, "\n"); |
| 332 else if (strcmp(method, "PUT") == 0) | 1601 } |
| 1602 | |
| 1603 } | |
| 1604 Seobeo_Client_Request_Set_Follow_Redirects(req, TRUE, 5); // TODO: remove magic number; | |
| 1605 res = Seobeo_Client_Request_Execute(req); | |
| 1606 | |
| 1607 if (res == NULL) { | |
| 1608 snprintf(result_body_array[RESULT_TAB_BODY], RESULT_BUFFER_LENGTH, "Error: Failed to send the request\n"); | |
| 1609 result_body_array[RESULT_TAB_HEADERS][0] = '\0'; | |
| 1610 } else { | |
| 1611 // Store response body | |
| 1612 snprintf(result_body_array[RESULT_TAB_BODY], RESULT_BUFFER_LENGTH, "%s", | |
| 1613 res->body ? res->body : ""); | |
| 1614 // Sanitize body for display (replace tabs, remove non-printable chars) | |
| 1615 SanitizeTextForDisplay(result_body_array[RESULT_TAB_BODY]); | |
| 1616 | |
| 1617 // Store response headers | |
| 1618 int offset = snprintf(result_body_array[RESULT_TAB_HEADERS], RESULT_BUFFER_LENGTH, | |
| 1619 "HTTP Status: %d %s\n\n", | |
| 1620 res->status_code, res->status_text ? res->status_text : ""); | |
| 1621 if (res->headers) { | |
| 1622 size_t header_count = Dowa_Array_Length(res->headers); | |
| 1623 for (size_t i = 0; i < header_count && offset < RESULT_BUFFER_LENGTH - 100; i++) { | |
| 1624 offset += snprintf(result_body_array[RESULT_TAB_HEADERS] + offset, | |
| 1625 RESULT_BUFFER_LENGTH - offset, | |
| 1626 "%s: %s\n", res->headers[i].key, res->headers[i].value); | |
| 1627 } | |
| 1628 } | |
| 1629 } | |
| 1630 printf("Body: %s\n", res ? res->body : "NULL"); | |
| 1631 Seobeo_Client_Request_Destroy(req); | |
| 1632 Seobeo_Client_Response_Destroy(res); | |
| 1633 PostDog_Request_SaveFile(); | |
| 1634 | |
| 1635 // Reset result text area states so scroll/cursor don't carry over | |
| 1636 GuiTextAreaResetState(TEXT_AREA_ID_RESULT_BODY); | |
| 1637 GuiTextAreaResetState(TEXT_AREA_ID_RESULT_HEADERS); | |
| 1638 | |
| 1639 return 0; | |
| 1640 } | |
| 1641 | |
| 1642 void PostDog_Http_Work(uv_work_t *req) | |
| 1643 { | |
| 1644 PostDog_Http_Request(); | |
| 1645 printf("HTTP request finished.\n"); | |
| 1646 } | |
| 1647 | |
| 1648 void PostDog_Http_Work_Done(uv_work_t *req, int status) | |
| 1649 { | |
| 1650 LOADING = FALSE; | |
| 1651 free(req); | |
| 1652 } | |
| 1653 | |
| 1654 void PostDog_Http_Thread_Request() | |
| 1655 { | |
| 1656 uv_work_t *work_req = malloc(sizeof(uv_work_t)); | |
| 1657 if (!work_req) | |
| 1658 { | |
| 1659 perror("Failed to allocate work request"); | |
| 1660 return; | |
| 1661 } | |
| 1662 LOADING = TRUE; | |
| 1663 if (uv_queue_work(main_loop, work_req, PostDog_Http_Work, PostDog_Http_Work_Done) != 0) | |
| 1664 { | |
| 1665 perror("Failed to queue work"); | |
| 1666 free(work_req); | |
| 1667 LOADING = FALSE; | |
| 1668 } | |
| 1669 } | |
| 1670 | |
| 1671 void PostDog_Update_URL(boolean is_url_updated) | |
| 1672 { | |
| 1673 char *params_start = strchr(url_input_text, '?'); | |
| 1674 if (is_url_updated) | |
| 1675 { | |
| 1676 if (!params_start) | |
| 1677 return; | |
| 1678 params_start++; | |
| 1679 Dowa_Arena *arena = Dowa_Arena_Create(1024*1024); | |
| 1680 | |
| 1681 char buffer[4096] = ""; | |
| 1682 char **lines = Dowa_String_Split(params_start, "&", URL_TEXT_BUFFER_LENGTH, 1, arena); | |
| 1683 for (int i = 0; i < Dowa_Array_Length(lines); i++) | |
| 333 { | 1684 { |
| 334 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); | 1685 char *line = lines[i]; |
| 335 if (url_body_map[TAB_BODY] && strlen(url_body_map[TAB_BODY]) > 0) | 1686 char **key_value = Dowa_String_Split(line, "=", (int)strlen(line), 1, arena); |
| 336 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, url_body_map[TAB_BODY]); | 1687 |
| 337 } | 1688 if (Dowa_Array_Length(key_value) < 2) |
| 338 else if (strcmp(method, "DELETE") == 0) | 1689 break; |
| 1690 snprintf(buffer + strlen(buffer), 4096 - strlen(buffer), "%s %s\n", key_value[0], key_value[1]); | |
| 1691 } | |
| 1692 snprintf(input_body_array[TAB_GET_PARAMS], URL_TEXT_BUFFER_LENGTH, "%s", buffer); | |
| 1693 int length = strlen(input_body_array[TAB_GET_PARAMS]); | |
| 1694 input_body_array[TAB_GET_PARAMS][length--] = '\0'; | |
| 1695 Dowa_Arena_Free(arena); | |
| 1696 } | |
| 1697 else | |
| 1698 { | |
| 1699 if (params_start) | |
| 339 { | 1700 { |
| 340 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); | 1701 size_t length = params_start - url_input_text; |
| 341 } | 1702 url_input_text[length] = '\0'; |
| 342 // Default is GET | 1703 } |
| 343 | 1704 int get_params_length = (int)strlen(input_body_array[TAB_GET_PARAMS]); |
| 344 // Parse and add headers | 1705 if (get_params_length == 0) |
| 345 if (url_body_map[TAB_HEADER] && strlen(url_body_map[TAB_HEADER]) > 0) | 1706 return; |
| 1707 | |
| 1708 char *separator = "?"; | |
| 1709 Dowa_Arena *arena = Dowa_Arena_Create(1024*1024); | |
| 1710 char **lines = Dowa_String_Split(input_body_array[TAB_GET_PARAMS], "\n", get_params_length, 1, arena); | |
| 1711 for (int i = 0; i < Dowa_Array_Length(lines); i++) | |
| 346 { | 1712 { |
| 347 char *headersCopy = strdup(url_body_map[TAB_HEADER]); | 1713 char *line = lines[i]; |
| 348 char *line = strtok(headersCopy, "\n"); | 1714 char **key_value = Dowa_String_Split(line, " ", (int)strlen(line), 1, arena); |
| 349 while (line != NULL) { | 1715 |
| 350 while (*line == ' ' || *line == '\t') line++; | 1716 if (Dowa_Array_Length(key_value) < 2) |
| 351 if (strlen(line) > 0) | 1717 break; |
| 352 headerList = curl_slist_append(headerList, line); | 1718 |
| 353 line = strtok(NULL, "\n"); | 1719 strcat(url_input_text, separator); |
| 354 } | 1720 strcat(url_input_text, key_value[0]); |
| 355 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); | 1721 strcat(url_input_text, "="); |
| 356 free(headersCopy); | 1722 for (int i = 1; i < Dowa_Array_Length(key_value); i++) |
| 357 } | 1723 { |
| 358 | 1724 if (!key_value[i] || key_value[i][0] == '\0') |
| 359 // Set write callback | 1725 break; |
| 360 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Postdog_Curl_Callback); | 1726 if (i > 1) strcat(url_input_text, "%20"); |
| 361 curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&buffer); | 1727 strcat(url_input_text, key_value[i]); |
| 362 | 1728 } |
| 363 // Follow redirects | 1729 separator = "&"; |
| 364 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); | 1730 } |
| 365 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); | 1731 |
| 366 res = curl_easy_perform(curl); | 1732 Dowa_Arena_Free(arena); |
| 367 | 1733 } |
| 368 if (res != CURLE_OK) | 1734 } |
| 369 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "Error: %s\n", curl_easy_strerror(res)); | 1735 |
| 370 else | 1736 int PostDog_String_To_MethodEnum(char *value) |
| 371 { | 1737 { |
| 372 long response_code; | 1738 if (strstr(value, "GET")) |
| 373 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); | |
| 374 | |
| 375 if (buffer.size > RESULT_BUFFER_LENGTH) | |
| 376 printf("TODO: Realloc\n"); | |
| 377 | |
| 378 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "HTTP Status: %ld\n\n%s", | |
| 379 response_code, buffer.data ? buffer.data : ""); | |
| 380 } | |
| 381 | |
| 382 if (headerList) curl_slist_free_all(headerList); | |
| 383 curl_easy_cleanup(curl); | |
| 384 } | |
| 385 else | |
| 386 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "Error: Failed to initialize curl"); | |
| 387 | |
| 388 free(buffer.data); | |
| 389 curl_global_cleanup(); | |
| 390 | |
| 391 PostDog_Request_SaveFile(); | |
| 392 return 0; | 1739 return 0; |
| 393 } | |
| 394 | |
| 395 void PostDog_Update_URL(void) | |
| 396 { | |
| 397 // Save existing query string if present | |
| 398 char *question_mark = strchr(url_input_text, '?'); | |
| 399 if (question_mark) | |
| 400 *question_mark = '\0'; | |
| 401 | |
| 402 int get_params_length = (int)strlen(url_body_map[TAB_GET_PARAMS]); | |
| 403 if (get_params_length == 0) | |
| 404 return; | |
| 405 | |
| 406 char *separator = "?"; | |
| 407 | |
| 408 Dowa_Arena *arena = Dowa_Arena_Create(1024*1024); | |
| 409 char **lines = Dowa_String_Split(url_body_map[TAB_GET_PARAMS], "\n", get_params_length, 1, arena); | |
| 410 for (int i = 0; i < Dowa_Array_Length(lines); i++) | |
| 411 { | |
| 412 char *line = lines[i]; | |
| 413 char **key_value = Dowa_String_Split(line, " ", (int)strlen(line), 1, arena); | |
| 414 | |
| 415 if (Dowa_Array_Length(key_value) < 2) | |
| 416 break; | |
| 417 | |
| 418 strcat(url_input_text, separator); | |
| 419 strcat(url_input_text, key_value[0]); | |
| 420 strcat(url_input_text, "="); | |
| 421 for (int i = 1; i < Dowa_Array_Length(key_value); i++) | |
| 422 { | |
| 423 if (!key_value[i] || key_value[i][0] == '\0') | |
| 424 break; | |
| 425 if (i > 1) strcat(url_input_text, "%20"); | |
| 426 strcat(url_input_text, key_value[i]); | |
| 427 } | |
| 428 separator = "&"; | |
| 429 } | |
| 430 | |
| 431 Dowa_Arena_Free(arena); | |
| 432 } | |
| 433 | |
| 434 int PostDog_String_To_MethodEnum(char *value) | |
| 435 { | |
| 436 if (strstr(value, "GET")) | |
| 437 return 0; | |
| 438 if (strstr(value, "POST")) | 1740 if (strstr(value, "POST")) |
| 439 return 1; | 1741 return 1; |
| 440 if (strstr(value, "PUT")) | 1742 if (strstr(value, "PUT")) |
| 441 return 2; | 1743 return 2; |
| 442 if (strstr(value, "DELETE")) | 1744 if (strstr(value, "DELETE")) |
| 443 return 3; | 1745 return 3; |
| 444 return 0; | 1746 return 0; |
| 445 } | 1747 } |
| 446 | 1748 |
| 447 void PostDog_Params_Reset(void) | 1749 void PostDog_Params_Reset(void) |
| 448 { | 1750 { |
| 449 url_input_text[0] = '\0'; | 1751 url_input_text[0] = '\0'; |
| 450 url_result_text[0] = '\0'; | |
| 451 active_method_dropdown = 0; | 1752 active_method_dropdown = 0; |
| 452 for (int i = 0; i < Dowa_Array_Length(url_body_map); i++) | 1753 active_result_tab = 0; |
| 453 url_body_map[i][0] = '\0'; | 1754 |
| 1755 for (int i = 0; i < Dowa_Array_Length(input_body_array); i++) | |
| 1756 input_body_array[i][0] = '\0'; | |
| 1757 | |
| 1758 for (int i = 0; i < Dowa_Array_Length(result_body_array); i++) | |
| 1759 result_body_array[i][0] = '\0'; | |
| 1760 | |
| 1761 // Reset text area states when clearing | |
| 1762 GuiTextAreaResetAllStates(); | |
| 454 } | 1763 } |
| 455 | 1764 |
| 456 void PostDog_Load_File(const char *filename) | 1765 void PostDog_Load_File(const char *filename) |
| 457 { | 1766 { |
| 458 char full_file_path[512] = {0}; | 1767 char full_file_path[512] = {0}; |
| 459 snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); | 1768 snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); |
| 460 FILE *file = fopen(full_file_path, "r"); | 1769 FILE *file = fopen(full_file_path, "r"); |
| 461 if (!file) | 1770 if (!file) |
| 462 return; | 1771 return; |
| 463 | 1772 |
| 464 fseek(file, 0, SEEK_END); | 1773 fseek(file, 0, SEEK_END); |
| 465 size_t file_size = ftell(file); | 1774 size_t file_size = ftell(file); |
| 466 fseek(file, 0, SEEK_SET); | 1775 fseek(file, 0, SEEK_SET); |
| 467 | 1776 |
| 476 for (int i = 0; i < Dowa_Array_Length(values); i++) | 1785 for (int i = 0; i < Dowa_Array_Length(values); i++) |
| 477 { | 1786 { |
| 478 switch (i) | 1787 switch (i) |
| 479 { | 1788 { |
| 480 case 0: // Title - skip | 1789 case 0: // Title - skip |
| 481 break; | 1790 break; |
| 482 | 1791 |
| 483 case 1: // URL | 1792 case 1: // URL |
| 484 snprintf(url_input_text, strlen(values[i]) + 1, "%s", values[i]); | 1793 snprintf(url_input_text, strlen(values[i]) + 1, "%s", values[i]); |
| 485 url_input_text[strcspn(url_input_text, "\n")] = '\0'; | 1794 url_input_text[strcspn(url_input_text, "\n")] = '\0'; |
| 486 break; | 1795 break; |
| 487 | 1796 |
| 488 case 2: // Method | 1797 case 2: // Method |
| 489 active_method_dropdown = PostDog_String_To_MethodEnum(values[i]); | 1798 active_method_dropdown = PostDog_String_To_MethodEnum(values[i]); |
| 490 break; | 1799 break; |
| 491 | 1800 |
| 492 case 3: // Headers (TAB_HEADER) | 1801 case 3: // Headers (TAB_HEADER) |
| 493 case 4: // Body (TAB_BODY) | 1802 case 4: // Body (TAB_BODY) |
| 494 case 5: // Get Params (TAB_GET_PARAMS) | 1803 case 5: // Get Params (TAB_GET_PARAMS) |
| 495 case 6: // Bar (TAB_BAR) | 1804 case 6: // Websocket (TAB_WEBSOCKET) |
| 496 { | 1805 { |
| 497 int map_index = i - 3; // 3->0, 4->1, 5->2, 6->3 | 1806 int map_index = i - 3; // 3->0, 4->1, 5->2, 6->3 |
| 498 snprintf(url_body_map[map_index], strlen(values[i]) + 1, "%s", values[i]); | 1807 snprintf(input_body_array[map_index], strlen(values[i]) + 1, "%s", values[i]); |
| 499 // Trim trailing newlines | 1808 // Trim trailing newlines |
| 500 for (int j = strlen(values[i]); j > 0; j--) | 1809 for (int j = strlen(values[i]); j > 0; j--) |
| 1810 { | |
| 1811 if (input_body_array[map_index][j] == '\n') | |
| 501 { | 1812 { |
| 502 if (url_body_map[map_index][j] == '\n') | 1813 input_body_array[map_index][j] = '\0'; |
| 503 { | 1814 break; |
| 504 url_body_map[map_index][j] = '\0'; | |
| 505 break; | |
| 506 } | |
| 507 } | 1815 } |
| 508 break; | 1816 } |
| 509 } | 1817 break; |
| 510 | 1818 } |
| 511 default: // Response (index 7+) | 1819 |
| 512 snprintf(url_result_text, strlen(values[i]) + 1, "%s", values[i]); | 1820 default: // Response (index 7+) - load into body tab for backward compatibility |
| 513 break; | 1821 snprintf(result_body_array[RESULT_TAB_BODY], strlen(values[i]) + 1, "%s", values[i]); |
| 514 } | 1822 break; |
| 515 } | 1823 } |
| 1824 } | |
| 1825 | |
| 1826 // Reset result tab to body | |
| 1827 active_result_tab = RESULT_TAB_BODY; | |
| 1828 | |
| 1829 // Reset text area states when loading new file | |
| 1830 GuiTextAreaResetAllStates(); | |
| 516 | 1831 |
| 517 Dowa_Arena_Free(init_arena); | 1832 Dowa_Arena_Free(init_arena); |
| 518 Dowa_Arena_Free(split_arena); | 1833 Dowa_Arena_Free(split_arena); |
| 519 } | 1834 } |
| 520 | 1835 |
| 521 Rectangle AddPadding(Rectangle rect, float padding) | 1836 // ============================================================================ |
| 522 { | 1837 // UI LAYOUT STRUCTURE - All rectangles for the UI |
| 523 return (Rectangle){ | 1838 // ============================================================================ |
| 524 rect.x + padding, | 1839 typedef struct { |
| 525 rect.y + padding, | 1840 // Main areas |
| 526 rect.width - (2 * padding), | 1841 Rectangle screen; |
| 527 rect.height - (2 * padding) | 1842 Rectangle sidebar; |
| 1843 Rectangle content; | |
| 1844 | |
| 1845 // Sidebar sections | |
| 1846 Rectangle logo_area; | |
| 1847 Rectangle history_list; | |
| 1848 | |
| 1849 // URL bar section | |
| 1850 Rectangle url_bar; | |
| 1851 Rectangle method_dropdown; | |
| 1852 Rectangle url_input; | |
| 1853 Rectangle send_button; | |
| 1854 | |
| 1855 // Body section (input + result split) | |
| 1856 Rectangle body_area; | |
| 1857 Rectangle input_panel; | |
| 1858 Rectangle result_panel; | |
| 1859 | |
| 1860 // Input panel internals | |
| 1861 Rectangle input_tabs; | |
| 1862 Rectangle input_body; | |
| 1863 | |
| 1864 // Result panel internals | |
| 1865 Rectangle result_tabs; | |
| 1866 Rectangle result_body; | |
| 1867 } UILayout; | |
| 1868 | |
| 1869 // ============================================================================ | |
| 1870 // UI STATE - All interactive state | |
| 1871 // ============================================================================ | |
| 1872 typedef struct { | |
| 1873 boolean url_edit_mode; | |
| 1874 boolean method_edit_mode; | |
| 1875 boolean input_body_edit_mode; | |
| 1876 boolean result_body_edit_mode; | |
| 1877 int prev_input_tab; | |
| 1878 float history_scroll_offset; | |
| 1879 } UIState; | |
| 1880 | |
| 1881 // ============================================================================ | |
| 1882 // LAYOUT CALCULATION FUNCTIONS | |
| 1883 // ============================================================================ | |
| 1884 static UILayout CalculateLayout(int screen_width, int screen_height, float padding) | |
| 1885 { | |
| 1886 UILayout layout = {0}; | |
| 1887 | |
| 1888 // Screen | |
| 1889 layout.screen = (Rectangle){0, 0, screen_width, screen_height}; | |
| 1890 | |
| 1891 // Main split: sidebar (20%) | content (80%) | |
| 1892 layout.sidebar = LeftColumn(layout.screen, 0.20f, 0); | |
| 1893 layout.content = RightColumn(layout.screen, layout.sidebar, 0); | |
| 1894 | |
| 1895 // Sidebar sections | |
| 1896 layout.logo_area = (Rectangle){ | |
| 1897 .x = layout.sidebar.x, | |
| 1898 .y = layout.sidebar.y, | |
| 1899 .width = layout.sidebar.width, | |
| 1900 .height = 120 | |
| 528 }; | 1901 }; |
| 529 } | 1902 |
| 530 | 1903 layout.history_list = (Rectangle){ |
| 531 Rectangle AddPaddingHorizontal(Rectangle rect, float padding) | 1904 .x = layout.sidebar.x + padding, |
| 532 { | 1905 .y = layout.logo_area.y + layout.logo_area.height + padding, |
| 533 return (Rectangle){ | 1906 .width = layout.sidebar.width - (2 * padding), |
| 534 rect.x + padding, | 1907 .height = layout.sidebar.height - layout.logo_area.height - (2 * padding) |
| 535 rect.y, | |
| 536 rect.width - (2 * padding), | |
| 537 rect.height | |
| 538 }; | 1908 }; |
| 539 } | 1909 |
| 540 | 1910 // URL bar section (top of content area) |
| 541 Rectangle AddPaddingVertical(Rectangle rect, float padding) | 1911 float url_bar_height = 60; |
| 542 { | 1912 layout.url_bar = (Rectangle){ |
| 543 return (Rectangle){ | 1913 .x = layout.content.x, |
| 544 rect.x, | 1914 .y = layout.content.y + padding, |
| 545 rect.y + padding, | 1915 .width = layout.content.width, |
| 546 rect.width, | 1916 .height = url_bar_height |
| 547 rect.height - (2 * padding) | |
| 548 }; | 1917 }; |
| 549 } | 1918 |
| 550 | 1919 // URL bar components (method dropdown + URL input + send button) |
| 551 // Layout helper functions | 1920 float dropdown_width = 100; |
| 552 Rectangle RightOf(Rectangle ref, float padding) | 1921 float button_width = 80; |
| 553 { | 1922 float url_bar_inner_padding = 8; |
| 554 return (Rectangle){ | 1923 float control_height = 36; |
| 555 .x = ref.x + ref.width + padding, | 1924 float control_y = layout.url_bar.y + (layout.url_bar.height - control_height) / 2; |
| 556 .y = ref.y, | 1925 |
| 557 .width = 0, | 1926 layout.method_dropdown = (Rectangle){ |
| 558 .height = ref.height | 1927 .x = layout.url_bar.x + padding, |
| 1928 .y = control_y, | |
| 1929 .width = dropdown_width, | |
| 1930 .height = control_height | |
| 559 }; | 1931 }; |
| 560 } | 1932 |
| 561 | 1933 layout.url_input = (Rectangle){ |
| 562 Rectangle Below(Rectangle ref, float padding) | 1934 .x = layout.method_dropdown.x + layout.method_dropdown.width + url_bar_inner_padding, |
| 563 { | 1935 .y = control_y, |
| 564 return (Rectangle){ | 1936 .width = layout.url_bar.width - dropdown_width - button_width - (4 * padding) - (2 * url_bar_inner_padding), |
| 565 .x = ref.x, | 1937 .height = control_height |
| 566 .y = ref.y + ref.height + padding, | |
| 567 .width = ref.width, | |
| 568 .height = 0 | |
| 569 }; | 1938 }; |
| 570 } | 1939 |
| 571 | 1940 layout.send_button = (Rectangle){ |
| 572 Rectangle LeftColumn(Rectangle container, float ratio, float padding) | 1941 .x = layout.url_input.x + layout.url_input.width + url_bar_inner_padding, |
| 573 { | 1942 .y = control_y, |
| 574 return (Rectangle){ | 1943 .width = button_width, |
| 575 .x = container.x + padding, | 1944 .height = control_height |
| 576 .y = container.y + padding, | |
| 577 .width = (container.width * ratio) - padding, | |
| 578 .height = container.height - (2 * padding) | |
| 579 }; | 1945 }; |
| 580 } | 1946 |
| 581 | 1947 // Body area (below URL bar) |
| 582 Rectangle RightColumn(Rectangle container, Rectangle leftCol, float padding) | 1948 layout.body_area = (Rectangle){ |
| 583 { | 1949 .x = layout.content.x, |
| 584 return (Rectangle){ | 1950 .y = layout.url_bar.y + layout.url_bar.height + padding, |
| 585 .x = leftCol.x + leftCol.width + padding, | 1951 .width = layout.content.width, |
| 586 .y = container.y + padding, | 1952 .height = layout.content.height - layout.url_bar.height - (3 * padding) |
| 587 .width = container.width - leftCol.width - (3 * padding), | |
| 588 .height = container.height - (2 * padding) | |
| 589 }; | 1953 }; |
| 590 } | 1954 |
| 591 | 1955 // Split body into input (left) and result (right) panels |
| 592 Rectangle HorizontalSplit(Rectangle container, float ratio) | 1956 float split_ratio = 0.5f; |
| 593 { | 1957 layout.input_panel = (Rectangle){ |
| 594 return (Rectangle){ | 1958 .x = layout.body_area.x, |
| 595 .x = container.x, | 1959 .y = layout.body_area.y, |
| 596 .y = container.y, | 1960 .width = layout.body_area.width * split_ratio, |
| 597 .width = container.width * ratio, | 1961 .height = layout.body_area.height |
| 598 .height = container.height | |
| 599 }; | 1962 }; |
| 600 } | 1963 |
| 601 | 1964 layout.result_panel = (Rectangle){ |
| 1965 .x = layout.body_area.x + layout.input_panel.width, | |
| 1966 .y = layout.body_area.y, | |
| 1967 .width = layout.body_area.width - layout.input_panel.width, | |
| 1968 .height = layout.body_area.height | |
| 1969 }; | |
| 1970 | |
| 1971 // Input panel internals | |
| 1972 float tab_height = 40; | |
| 1973 layout.input_tabs = (Rectangle){ | |
| 1974 .x = layout.input_panel.x + padding, | |
| 1975 .y = layout.input_panel.y + padding, | |
| 1976 .width = layout.input_panel.width - (2 * padding), | |
| 1977 .height = tab_height | |
| 1978 }; | |
| 1979 | |
| 1980 layout.input_body = (Rectangle){ | |
| 1981 .x = layout.input_panel.x + padding, | |
| 1982 .y = layout.input_tabs.y + layout.input_tabs.height, | |
| 1983 .width = layout.input_panel.width - (2 * padding), | |
| 1984 .height = layout.input_panel.height - tab_height - (2 * padding) | |
| 1985 }; | |
| 1986 | |
| 1987 // Result panel internals | |
| 1988 layout.result_tabs = (Rectangle){ | |
| 1989 .x = layout.result_panel.x + padding, | |
| 1990 .y = layout.result_panel.y + padding, | |
| 1991 .width = layout.result_panel.width - (2 * padding), | |
| 1992 .height = tab_height | |
| 1993 }; | |
| 1994 | |
| 1995 layout.result_body = (Rectangle){ | |
| 1996 .x = layout.result_panel.x + padding, | |
| 1997 .y = layout.result_tabs.y + layout.result_tabs.height, | |
| 1998 .width = layout.result_panel.width - (2 * padding), | |
| 1999 .height = layout.result_panel.height - tab_height - (2 * padding) | |
| 2000 }; | |
| 2001 | |
| 2002 return layout; | |
| 2003 } | |
| 2004 | |
| 2005 // ============================================================================ | |
| 2006 // DRAWING FUNCTIONS - Separated for clarity | |
| 2007 // ============================================================================ | |
| 2008 | |
| 2009 static void DrawSidebar(UILayout *layout, Texture2D logo_texture, float padding) | |
| 2010 { | |
| 2011 // Sidebar background with rounded left corners | |
| 2012 DrawRectangleRec(layout->sidebar, g_colors.primary); | |
| 2013 | |
| 2014 // Logo | |
| 2015 Rectangle logo_inner = AddPadding(layout->logo_area, padding); | |
| 2016 float logo_size = logo_inner.height < logo_inner.width ? logo_inner.height : logo_inner.width; | |
| 2017 Rectangle dest_rect = { | |
| 2018 .x = logo_inner.x + (logo_inner.width - logo_size) / 2, | |
| 2019 .y = logo_inner.y, | |
| 2020 .width = logo_size, | |
| 2021 .height = logo_size | |
| 2022 }; | |
| 2023 Rectangle source_rect = {0, 0, logo_texture.width, logo_texture.height}; | |
| 2024 DrawTexturePro(logo_texture, source_rect, dest_rect, (Vector2){0, 0}, 0.0f, WHITE); | |
| 2025 } | |
| 2026 | |
| 2027 static void DrawHistoryList(UILayout *layout, Vector2 mouse_pos, float padding, float scroll_offset) | |
| 2028 { | |
| 2029 int32 new_len = Dowa_Array_Length(new_history_items); | |
| 2030 int32 history_len = Dowa_Array_Length(history_items); | |
| 2031 int32 total = new_len + history_len; | |
| 2032 float item_height = 70; | |
| 2033 | |
| 2034 BeginScissorMode(layout->history_list.x, layout->history_list.y, | |
| 2035 layout->history_list.width, layout->history_list.height); | |
| 2036 | |
| 2037 int32 visible_index = 0; | |
| 2038 for (int i = 0; i < total; i++) | |
| 2039 { | |
| 2040 HistoryItem *item = i < new_len ? | |
| 2041 &new_history_items[new_len - i - 1] : &history_items[i - new_len]; | |
| 2042 | |
| 2043 if (item->deleted) continue; | |
| 2044 | |
| 2045 // Calculate item rectangle | |
| 2046 Rectangle item_rect = { | |
| 2047 .x = layout->history_list.x, | |
| 2048 .y = layout->history_list.y + (padding + item_height) * visible_index + scroll_offset, | |
| 2049 .width = layout->history_list.width, | |
| 2050 .height = item_height | |
| 2051 }; | |
| 2052 item->rect = item_rect; | |
| 2053 visible_index++; | |
| 2054 | |
| 2055 // Skip if not visible | |
| 2056 if (item_rect.y + item_rect.height < layout->history_list.y || | |
| 2057 item_rect.y > layout->history_list.y + layout->history_list.height) | |
| 2058 continue; | |
| 2059 | |
| 2060 // Draw item background | |
| 2061 DrawRectangleRounded(item_rect, 0.3f, 8, g_colors.secondary); | |
| 2062 | |
| 2063 // Title area (top 60%) | |
| 2064 Rectangle title_rect = item_rect; | |
| 2065 title_rect.height = item_rect.height * 0.55f; | |
| 2066 | |
| 2067 // Draw title text | |
| 2068 Rectangle title_text_rect = AddPadding(title_rect, 8); | |
| 2069 if (item->title) | |
| 2070 { | |
| 2071 DrawTextEx(GuiGetFont(), item->title, (Vector2){ .x=title_text_rect.x, .y=title_text_rect.y + 4 }, | |
| 2072 GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), g_colors.text); | |
| 2073 } | |
| 2074 | |
| 2075 // Button area (bottom 40%) | |
| 2076 Rectangle button_area = { | |
| 2077 .x = item_rect.x, | |
| 2078 .y = title_rect.y + title_rect.height, | |
| 2079 .width = item_rect.width, | |
| 2080 .height = item_rect.height - title_rect.height | |
| 2081 }; | |
| 2082 | |
| 2083 Rectangle btn_view = { | |
| 2084 .x = button_area.x + padding, | |
| 2085 .y = button_area.y, | |
| 2086 .width = (button_area.width - 3 * padding) / 2, | |
| 2087 .height = button_area.height - padding | |
| 2088 }; | |
| 2089 | |
| 2090 Rectangle btn_delete = { | |
| 2091 .x = btn_view.x + btn_view.width + padding, | |
| 2092 .y = btn_view.y, | |
| 2093 .width = btn_view.width, | |
| 2094 .height = btn_view.height | |
| 2095 }; | |
| 2096 | |
| 2097 // Hover cursor for buttons | |
| 2098 if (CheckCollisionPointRec(mouse_pos, btn_view) || | |
| 2099 CheckCollisionPointRec(mouse_pos, btn_delete)) | |
| 2100 SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); | |
| 2101 | |
| 2102 if (GuiButton(btn_view, "View")) | |
| 2103 PostDog_Load_File(item->filename); | |
| 2104 | |
| 2105 if (GuiButton(btn_delete, "Delete")) | |
| 2106 { | |
| 2107 char delete_path[512]; | |
| 2108 if (!remove(PostDog_Construct_URL(item->filename, delete_path, sizeof(delete_path)))) | |
| 2109 item->deleted = TRUE; | |
| 2110 else | |
| 2111 fprintf(stderr, "Couldn't delete file: %s\n", item->filename); | |
| 2112 } | |
| 2113 } | |
| 2114 | |
| 2115 EndScissorMode(); | |
| 2116 | |
| 2117 // Scrollbar | |
| 2118 float content_height = visible_index * (item_height + padding); | |
| 2119 if (content_height > layout->history_list.height) | |
| 2120 { | |
| 2121 float scrollbar_height = (layout->history_list.height / content_height) * layout->history_list.height; | |
| 2122 float scrollbar_y = layout->history_list.y - (scroll_offset / content_height) * layout->history_list.height; | |
| 2123 Rectangle scrollbar_rect = { | |
| 2124 layout->history_list.x + layout->history_list.width - 6, | |
| 2125 scrollbar_y, | |
| 2126 4, | |
| 2127 scrollbar_height | |
| 2128 }; | |
| 2129 DrawRectangleRounded(scrollbar_rect, 0.5f, 4, Fade(g_colors.text_light, 0.5f)); | |
| 2130 } | |
| 2131 } | |
| 2132 | |
| 2133 static void DrawURLBar(UILayout *layout, UIState *state, float padding) | |
| 2134 { | |
| 2135 // URL bar background | |
| 2136 Rectangle url_bar_bg = AddPadding(layout->url_bar, padding / 2); | |
| 2137 DrawRectangleRounded(url_bar_bg, 0.3f, 8, g_colors.secondary); | |
| 2138 | |
| 2139 // Combined method dropdown + URL input with rounded corners | |
| 2140 Rectangle combined_input = { | |
| 2141 .x = layout->method_dropdown.x, | |
| 2142 .y = layout->method_dropdown.y, | |
| 2143 .width = layout->url_input.x + layout->url_input.width - layout->method_dropdown.x, | |
| 2144 .height = layout->method_dropdown.height | |
| 2145 }; | |
| 2146 | |
| 2147 // Method colors: GET=Green, POST=Blue, PUT=Orange, DELETE=Red | |
| 2148 Color method_colors[] = { | |
| 2149 (Color){76, 175, 80, 255}, // GET - Green | |
| 2150 (Color){33, 150, 243, 255}, // POST - Blue | |
| 2151 (Color){255, 152, 0, 255}, // PUT - Orange | |
| 2152 (Color){244, 67, 54, 255} // DELETE - Red | |
| 2153 }; | |
| 2154 | |
| 2155 // Method dropdown + URL input combined component | |
| 2156 DropdownTextBoxConfig url_config = { | |
| 2157 .dropdown_items = "GET;POST;PUT;DELETE", | |
| 2158 .dropdown_active = &active_method_dropdown, | |
| 2159 .dropdown_edit_mode = &state->method_edit_mode, | |
| 2160 .dropdown_width = layout->method_dropdown.width, | |
| 2161 .text_buffer = url_input_text, | |
| 2162 .text_buffer_size = URL_TEXT_BUFFER_LENGTH, | |
| 2163 .text_edit_mode = &state->url_edit_mode, | |
| 2164 .corner_radius = 0.2f, | |
| 2165 .background_color = g_colors.background, | |
| 2166 .border_color = g_colors.border, | |
| 2167 .text_color = g_colors.text, | |
| 2168 .item_colors = method_colors, | |
| 2169 .item_count = 4 | |
| 2170 }; | |
| 2171 PostDog_DropdownTextBox(combined_input, url_config); | |
| 2172 | |
| 2173 // Handle Enter key in URL input | |
| 2174 if (state->url_edit_mode && IsKeyPressed(KEY_ENTER)) | |
| 2175 { | |
| 2176 PostDog_Http_Thread_Request(); | |
| 2177 state->url_edit_mode = FALSE; | |
| 2178 } | |
| 2179 | |
| 2180 // Send button | |
| 2181 DrawRectangleRounded(layout->send_button, 0.3f, 8, g_colors.primary); | |
| 2182 Rectangle btn_text_rect = layout->send_button; | |
| 2183 char *btn_text = "Send"; | |
| 2184 int text_width = MeasureText(btn_text, GuiGetStyle(DEFAULT, TEXT_SIZE)); | |
| 2185 DrawTextEx(GuiGetFont(), | |
| 2186 btn_text, | |
| 2187 (Vector2){ .x=btn_text_rect.x + (btn_text_rect.width - text_width) / 2, .y=btn_text_rect.y + (btn_text_rect.height - GuiGetStyle(DEFAULT, TEXT_SIZE)) / 2 }, | |
| 2188 GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), g_colors.text_light); | |
| 2189 | |
| 2190 if (CheckCollisionPointRec(GetMousePosition(), layout->send_button) && | |
| 2191 IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) | |
| 2192 PostDog_Http_Thread_Request(); | |
| 2193 } | |
| 2194 | |
| 2195 static void DrawBodyPanels(UILayout *layout, UIState *state, float padding) | |
| 2196 { | |
| 2197 // Draw split panel background (input left, result right) | |
| 2198 DrawRectangleSelectiveRounded(layout->input_panel, 12, 8, g_colors.background, | |
| 2199 TRUE, FALSE, FALSE, TRUE); | |
| 2200 DrawRectangleSelectiveRounded(layout->result_panel, 12, 8, | |
| 2201 LOADING ? Fade(g_colors.error, 0.3f) : g_colors.secondary, | |
| 2202 FALSE, TRUE, TRUE, FALSE); | |
| 2203 | |
| 2204 // Tab labels | |
| 2205 const char *tab_labels[] = {"Headers", "Body", "Params", "WebSocket"}; | |
| 2206 int tab_count = 4; | |
| 2207 | |
| 2208 // Draw custom tabs | |
| 2209 PostDog_TabBarSimple(layout->input_tabs, tab_labels, tab_count, &active_input_tab); | |
| 2210 | |
| 2211 // Input body text area | |
| 2212 int text_area_id = TEXT_AREA_ID_INPUT_HEADER + active_input_tab; | |
| 2213 if (GuiTextArea(text_area_id, layout->input_body, input_body_array[active_input_tab], | |
| 2214 BODY_BUFFER_LENGTH, state->input_body_edit_mode, TRUE, FALSE, g_text_area_arena)) | |
| 2215 state->input_body_edit_mode = !state->input_body_edit_mode; | |
| 2216 | |
| 2217 // WebSocket send button (only on WebSocket tab) | |
| 2218 if (active_input_tab == TAB_WEBSOCKET) | |
| 2219 { | |
| 2220 WS_BREAK = FALSE; | |
| 2221 Rectangle ws_send_btn = { | |
| 2222 .x = layout->input_body.x + layout->input_body.width - 90 - padding, | |
| 2223 .y = layout->input_body.y + layout->input_body.height - 35 - padding, | |
| 2224 .width = 90, | |
| 2225 .height = 35 | |
| 2226 }; | |
| 2227 | |
| 2228 DrawRectangleRounded(ws_send_btn, 0.3f, 8, g_colors.highlight); | |
| 2229 char *ws_text = !ws ? "Connect" : "Send"; | |
| 2230 int ws_text_width = MeasureText(ws_text, GuiGetStyle(DEFAULT, TEXT_SIZE)); | |
| 2231 DrawTextEx(GuiGetFont(), | |
| 2232 ws_text, | |
| 2233 (Vector2) { .x=ws_send_btn.x + (ws_send_btn.width - ws_text_width) / 2, .y=ws_send_btn.y + (ws_send_btn.height - GuiGetStyle(DEFAULT, TEXT_SIZE)) / 2 }, | |
| 2234 GuiGetStyle(DEFAULT, TEXT_SIZE), | |
| 2235 TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), | |
| 2236 g_colors.text); | |
| 2237 | |
| 2238 if ((CheckCollisionPointRec(GetMousePosition(), ws_send_btn) && | |
| 2239 IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) || | |
| 2240 (state->input_body_edit_mode && IsKeyDown(KEY_LEFT_SHIFT) && IsKeyPressed(KEY_ENTER))) | |
| 2241 { | |
| 2242 if (!ws) | |
| 2243 websocket_thread_id = PostDog_Websocket_Start_Thread(); | |
| 2244 usleep(10000); | |
| 2245 PostDog_Websocket_Send(); | |
| 2246 } | |
| 2247 } | |
| 2248 else | |
| 2249 { | |
| 2250 WS_BREAK = TRUE; | |
| 2251 } | |
| 2252 | |
| 2253 // Result tabs | |
| 2254 const char *result_tab_labels[] = {"Body", "Headers"}; | |
| 2255 int result_tab_count = 2; | |
| 2256 PostDog_TabBarSimple(layout->result_tabs, result_tab_labels, result_tab_count, &active_result_tab); | |
| 2257 | |
| 2258 // Result body text area | |
| 2259 int result_text_area_id = TEXT_AREA_ID_RESULT_BODY + active_result_tab; | |
| 2260 // Result text area is readonly (selectable/copyable but not editable) | |
| 2261 if (GuiTextArea(result_text_area_id, layout->result_body, result_body_array[active_result_tab], | |
| 2262 RESULT_BUFFER_LENGTH, state->result_body_edit_mode, TRUE, TRUE, g_text_area_arena)) | |
| 2263 state->result_body_edit_mode = !state->result_body_edit_mode; | |
| 2264 | |
| 2265 PostDog_Update_URL(state->url_edit_mode); | |
| 2266 } | |
| 2267 | |
| 2268 // ============================================================================ | |
| 2269 // MAIN FUNCTION | |
| 2270 // ============================================================================ | |
| 602 int main() | 2271 int main() |
| 603 { | 2272 { |
| 604 // -- initizlied --// | 2273 // ======================================================================== |
| 2274 // INITIALIZATION | |
| 2275 // ======================================================================== | |
| 2276 SetConfigFlags(FLAG_WINDOW_UNDECORATED); | |
| 2277 | |
| 2278 // Initialize libuv | |
| 2279 main_loop = uv_default_loop(); | |
| 2280 uv_mutex_init(&history_mutex); | |
| 2281 | |
| 2282 // Initialize color scheme | |
| 2283 PostDog_InitColorScheme(); | |
| 2284 | |
| 2285 // Initialize window | |
| 605 InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "PostDog"); | 2286 InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "PostDog"); |
| 606 SetWindowState(FLAG_WINDOW_RESIZABLE); | 2287 SetWindowState(FLAG_WINDOW_RESIZABLE); |
| 607 SetTargetFPS(60); | 2288 SetTargetFPS(60); |
| 608 | 2289 |
| 2290 // Load resources | |
| 609 Font customFont = LoadFontEx("postdog/Roboto-Regular.ttf", 20, 0, 0); | 2291 Font customFont = LoadFontEx("postdog/Roboto-Regular.ttf", 20, 0, 0); |
| 610 GuiSetFont(customFont); | 2292 GuiSetFont(customFont); |
| 611 GuiSetStyle(DEFAULT, TEXT_SIZE, 10); | 2293 GuiSetStyle(DEFAULT, TEXT_SIZE, TEXT_SIZE_DEFAULT); |
| 612 Image logo_original = LoadImage("postdog/epi_all_colors.png"); | 2294 |
| 613 ImageResize(&logo_original, 60, 60); | 2295 Image logo_original = LoadImage("postdog/logo_bigger.png"); |
| 2296 ImageResize(&logo_original, 200, 200); | |
| 614 SetWindowIcon(logo_original); | 2297 SetWindowIcon(logo_original); |
| 615 Texture2D logo_texture = LoadTextureFromImage(logo_original); | 2298 Texture2D logo_texture = LoadTextureFromImage(logo_original); |
| 2299 // ToggleBorderlessWindowed(); | |
| 616 UnloadImage(logo_original); | 2300 UnloadImage(logo_original); |
| 617 | 2301 |
| 618 // -- Starting pos ---// | 2302 // Initialize text area arena |
| 619 Rectangle history_sidebar = { 0 }; | 2303 g_text_area_arena = Dowa_Arena_Create(1024 * 1024 * 4); |
| 2304 | |
| 2305 // Initialize history | |
| 620 Dowa_Array_Reserve(history_items, 10); | 2306 Dowa_Array_Reserve(history_items, 10); |
| 621 Dowa_Array_Reserve(new_history_items, 10); | 2307 Dowa_Array_Reserve(new_history_items, 10); |
| 622 PostDog_History_Load(&history_items); | 2308 PostDog_History_Load(&history_items); |
| 623 int32 *history_deleted_items = NULL; | 2309 |
| 624 | 2310 // Initialize text buffers |
| 625 Rectangle url_area = { 0 }; | |
| 626 Rectangle textBounds = { 0 }; | |
| 627 Rectangle url_input_bounds = { 0 }; | |
| 628 bool url_input_edit = false; | |
| 629 Rectangle url_text_bounds = { 0 }; | |
| 630 Rectangle url_enter_button = { 0 }; | |
| 631 Rectangle method_dropdown = { 0 }; | |
| 632 | |
| 633 | |
| 634 // Initialize global UI state | |
| 635 url_input_text = (char *)malloc(sizeof(char) * URL_TEXT_BUFFER_LENGTH); | 2311 url_input_text = (char *)malloc(sizeof(char) * URL_TEXT_BUFFER_LENGTH); |
| 636 snprintf(url_input_text, URL_TEXT_BUFFER_LENGTH, "https://httpbin.org/get"); | 2312 snprintf(url_input_text, URL_TEXT_BUFFER_LENGTH, URL_TEXT_DEFAULT); |
| 637 | 2313 |
| 638 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * HEADER_BUFFER_LENGTH)); | 2314 Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * HEADER_BUFFER_LENGTH)); |
| 639 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * BODY_BUFFER_LENGTH)); | 2315 Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * BODY_BUFFER_LENGTH)); |
| 640 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); | 2316 Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); |
| 641 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); | 2317 Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); |
| 642 | 2318 |
| 643 snprintf(url_body_map[TAB_HEADER], HEADER_BUFFER_LENGTH, "Content-Type: application/json"); | 2319 snprintf(input_body_array[TAB_HEADER], HEADER_BUFFER_LENGTH, HEADER_TEXT_DEFAULT); |
| 644 snprintf(url_body_map[TAB_BODY], HEADER_BUFFER_LENGTH, ""); | 2320 snprintf(input_body_array[TAB_BODY], BODY_BUFFER_LENGTH, BODY_TEXT_DEFAULT); |
| 645 | 2321 snprintf(input_body_array[TAB_GET_PARAMS], DEFAULT_TEXT_BUFFER_LENGTH, GET_PARAM_TEXT_DEFAULT); |
| 646 url_result_text = (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH); | 2322 snprintf(input_body_array[TAB_WEBSOCKET], DEFAULT_TEXT_BUFFER_LENGTH, GET_PARAM_TEXT_DEFAULT); |
| 647 | 2323 |
| 648 bool method_edit = false; | 2324 // Initialize result buffers (body and headers tabs) |
| 649 | 2325 Dowa_Array_Push(result_body_array, (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH)); |
| 650 int sendRequest; | 2326 Dowa_Array_Push(result_body_array, (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH)); |
| 651 | 2327 result_body_array[RESULT_TAB_BODY][0] = '\0'; |
| 652 // -- input --// | 2328 result_body_array[RESULT_TAB_HEADERS][0] = '\0'; |
| 653 Rectangle input_area = { 0 }; | 2329 |
| 654 Rectangle input_tab = { 0 }; | 2330 // Initialize UI state |
| 655 Rectangle input_tab_item = { 0 }; | 2331 UIState ui_state = {0}; |
| 656 Rectangle input_body = { 0 }; | 2332 float padding = 10.0f; |
| 657 bool input_body_bool = false; | 2333 |
| 658 | 2334 // ======================================================================== |
| 659 // -- result --// | 2335 // MAIN LOOP |
| 660 Rectangle result_area = { 0 }; | 2336 // ======================================================================== |
| 661 Rectangle result_body = { 0 }; | |
| 662 | |
| 663 // General styling. | |
| 664 float padding = 10; // TODO make it % based? | |
| 665 int active_input_tab = 0; | |
| 666 int prev_input_tab = 0; | |
| 667 | |
| 668 // Scroll offsets | |
| 669 float history_scroll_offset = 0; | |
| 670 float input_body_scroll_offset = 0; | |
| 671 float result_body_scroll_offset = 0; | |
| 672 | |
| 673 while (!WindowShouldClose()) | 2337 while (!WindowShouldClose()) |
| 674 { | 2338 { |
| 2339 // Process libuv events (non-blocking) | |
| 2340 uv_run(main_loop, UV_RUN_NOWAIT); | |
| 2341 | |
| 2342 // Handle keyboard shortcuts | |
| 2343 DefaultBehaviours(); | |
| 2344 | |
| 2345 // ==================================================================== | |
| 2346 // LAYOUT CALCULATION | |
| 2347 // ==================================================================== | |
| 675 int screen_width = GetScreenWidth(); | 2348 int screen_width = GetScreenWidth(); |
| 676 int screen_height = GetScreenHeight(); | 2349 int screen_height = GetScreenHeight(); |
| 677 | 2350 UILayout layout = CalculateLayout(screen_width, screen_height, padding); |
| 678 if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyDown(KEY_EQUAL)) | 2351 |
| 679 GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetStyle(DEFAULT, TEXT_SIZE) + 1); | 2352 // ==================================================================== |
| 680 | 2353 // INPUT HANDLING |
| 681 if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyDown(KEY_MINUS)) | 2354 // ==================================================================== |
| 682 GuiSetStyle(DEFAULT, TEXT_SIZE, GuiGetStyle(DEFAULT, TEXT_SIZE) - 1); | 2355 Vector2 mouse_pos = GetMousePosition(); |
| 683 | 2356 float mouse_wheel = GetMouseWheelMove(); |
| 684 Rectangle screen = { 0, 0, screen_width, screen_height }; | 2357 |
| 685 | 2358 // Reset cursor |
| 686 // -- Side bar --// | 2359 SetMouseCursor(MOUSE_CURSOR_DEFAULT); |
| 687 history_sidebar = LeftColumn(screen, 0.15, padding); | 2360 |
| 688 Rectangle content_area = RightColumn(screen, history_sidebar, padding); | 2361 // History scroll |
| 689 Rectangle logo_area = (Rectangle){ | 2362 if (CheckCollisionPointRec(mouse_pos, layout.history_list) && mouse_wheel != 0) |
| 690 .x = history_sidebar.x, | |
| 691 .y = history_sidebar.y, | |
| 692 .width = history_sidebar.width, | |
| 693 .height = 80 | |
| 694 }; | |
| 695 | |
| 696 Rectangle history_list_area = Below(logo_area, padding); | |
| 697 history_list_area.width = history_sidebar.width; | |
| 698 history_list_area.height = history_sidebar.height - logo_area.height - padding; | |
| 699 | |
| 700 int32 new_history_items_length = Dowa_Array_Length(new_history_items); | |
| 701 int32 history_item_length = Dowa_Array_Length(history_items); | |
| 702 int32 total = new_history_items_length + history_item_length; | |
| 703 float item_height = history_list_area.height * 0.10; | |
| 704 | |
| 705 int32 number_of_skipped_items = 0; | |
| 706 for (int i = 0; i < total; i++) | |
| 707 { | 2363 { |
| 708 HistoryItem *curr_history_items = i < new_history_items_length ? | 2364 ui_state.history_scroll_offset += mouse_wheel * 30; |
| 709 &new_history_items[i - new_history_items_length - 1] : &history_items[i - new_history_items_length]; | 2365 int32 total = Dowa_Array_Length(new_history_items) + Dowa_Array_Length(history_items); |
| 710 | 2366 float content_height = total * 80; |
| 711 if (curr_history_items->deleted) | 2367 float max_scroll = content_height - layout.history_list.height; |
| 712 { | 2368 if (ui_state.history_scroll_offset > 0) ui_state.history_scroll_offset = 0; |
| 713 number_of_skipped_items++; | 2369 if (max_scroll > 0 && ui_state.history_scroll_offset < -max_scroll) |
| 714 continue; | 2370 ui_state.history_scroll_offset = -max_scroll; |
| 715 } | 2371 } |
| 716 | 2372 |
| 717 curr_history_items->rect = (Rectangle){ | 2373 // Cursor changes for interactive areas |
| 718 .x = history_list_area.x, | 2374 if (CheckCollisionPointRec(mouse_pos, layout.send_button) || |
| 719 .y = history_list_area.y + (padding + item_height) * (i - number_of_skipped_items) + history_scroll_offset, | 2375 CheckCollisionPointRec(mouse_pos, layout.input_tabs) || |
| 720 .width = history_list_area.width, | 2376 CheckCollisionPointRec(mouse_pos, layout.logo_area)) |
| 721 .height = item_height | 2377 SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); |
| 722 }; | 2378 |
| 723 } | 2379 // Logo click resets |
| 724 | 2380 if (CheckCollisionPointRec(mouse_pos, layout.logo_area) && |
| 725 // --- URL --- // | 2381 IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) |
| 726 url_area = (Rectangle){ | 2382 PostDog_Params_Reset(); |
| 727 .x = content_area.x, | 2383 |
| 728 .y = content_area.y, | 2384 // ==================================================================== |
| 729 .width = content_area.width, | 2385 // DRAWING |
| 730 .height = content_area.height * 0.1 | 2386 // ==================================================================== |
| 731 }; | |
| 732 | |
| 733 float url_control_y = url_area.y + (url_area.height - TEXT_SIZE * 2) / 2; | |
| 734 | |
| 735 url_text_bounds = (Rectangle){ | |
| 736 .x = url_area.x + padding, | |
| 737 .y = url_control_y, | |
| 738 .width = 7 * (TEXT_SIZE / 2), | |
| 739 .height = TEXT_SIZE * 2 | |
| 740 }; | |
| 741 | |
| 742 url_input_bounds = RightOf(url_text_bounds, padding); | |
| 743 url_input_bounds.width = url_area.width * 0.7; | |
| 744 url_input_bounds.height = TEXT_SIZE * 2; | |
| 745 | |
| 746 url_enter_button = RightOf(url_input_bounds, padding); | |
| 747 url_enter_button.width = url_area.width * 0.1; | |
| 748 url_enter_button.height = TEXT_SIZE * 2; | |
| 749 | |
| 750 method_dropdown = RightOf(url_enter_button, padding); | |
| 751 method_dropdown.width = url_area.width * 0.1; | |
| 752 method_dropdown.height = TEXT_SIZE * 2; | |
| 753 | |
| 754 // -- Body -- // | |
| 755 Rectangle body_area = Below(url_area, 0); | |
| 756 body_area.height = content_area.height - url_area.height; | |
| 757 | |
| 758 input_area = HorizontalSplit(body_area, 0.5); | |
| 759 result_area = RightOf(input_area, 0); | |
| 760 result_area.width = body_area.width - input_area.width; | |
| 761 | |
| 762 input_tab = (Rectangle){ | |
| 763 .x = input_area.x + padding, | |
| 764 .y = input_area.y + padding, | |
| 765 .width = input_area.width - (2 * padding), | |
| 766 .height = input_area.height * 0.1 | |
| 767 }; | |
| 768 | |
| 769 input_tab_item = input_tab; | |
| 770 input_tab_item.width = input_tab.width / 4; | |
| 771 | |
| 772 input_body = Below(input_tab, 0); | |
| 773 input_body.width = input_tab.width; | |
| 774 input_body.height = input_area.height - input_tab.height - (2 * padding); | |
| 775 | |
| 776 // -- Result -- / | |
| 777 result_body = (Rectangle){ | |
| 778 .x = result_area.x + padding, | |
| 779 .y = input_body.y, | |
| 780 .width = result_area.width - (2 * padding), | |
| 781 .height = input_body.height | |
| 782 }; | |
| 783 | |
| 784 Vector2 mouse_position = GetMousePosition(); | |
| 785 float mouse_wheel = GetMouseWheelMove(); | |
| 786 | |
| 787 // Reset input body scroll when tab changes | |
| 788 if (prev_input_tab != active_input_tab) { | |
| 789 input_body_scroll_offset = 0; | |
| 790 prev_input_tab = active_input_tab; | |
| 791 } | |
| 792 | |
| 793 // Handle scroll wheel for history | |
| 794 if (InArea(mouse_position, history_list_area) && mouse_wheel != 0) { | |
| 795 history_scroll_offset += mouse_wheel * 30; // 30 pixels per wheel tick | |
| 796 // Clamp scroll offset | |
| 797 float max_scroll = (total * (item_height + padding)) - history_list_area.height; | |
| 798 if (history_scroll_offset > 0) history_scroll_offset = 0; | |
| 799 if (history_scroll_offset < -max_scroll && max_scroll > 0) history_scroll_offset = -max_scroll; | |
| 800 } | |
| 801 | |
| 802 // Handle scroll wheel for input body | |
| 803 if (InArea(mouse_position, input_body) && mouse_wheel != 0) { | |
| 804 input_body_scroll_offset += mouse_wheel * 30; | |
| 805 if (input_body_scroll_offset > 0) input_body_scroll_offset = 0; | |
| 806 } | |
| 807 | |
| 808 // Handle scroll wheel for result body | |
| 809 if (InArea(mouse_position, result_body) && mouse_wheel != 0) { | |
| 810 result_body_scroll_offset += mouse_wheel * 30; | |
| 811 if (result_body_scroll_offset > 0) result_body_scroll_offset = 0; | |
| 812 } | |
| 813 | |
| 814 BeginDrawing(); | 2387 BeginDrawing(); |
| 815 ClearBackground(GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR))); | 2388 ClearBackground(g_colors.background); |
| 816 | 2389 DrawRectangleSelectiveRounded(layout.content, 12, 8, g_colors.secondary, |
| 817 DrawRectangleRec(history_sidebar, Fade(GRAY, 0.1f)); | 2390 FALSE, TRUE, TRUE, FALSE); |
| 818 | 2391 DrawSidebar(&layout, logo_texture, padding); |
| 819 // DrawRectangleRec(logo_area, Fade(BLUE, 0.2f)); | 2392 DrawHistoryList(&layout, mouse_pos, padding, ui_state.history_scroll_offset); |
| 820 Rectangle logo_image_rect = AddPadding(logo_area, padding); | 2393 DrawBodyPanels(&layout, &ui_state, padding); |
| 821 // Fit logo to area while maintaining aspect ratio | 2394 DrawURLBar(&layout, &ui_state, padding); |
| 822 float logo_size = logo_image_rect.height < logo_image_rect.width ? logo_image_rect.height : logo_image_rect.width; | |
| 823 Rectangle dest = { | |
| 824 .x = logo_image_rect.x + (logo_image_rect.width - logo_size) / 2, // Center horizontally | |
| 825 .y = logo_image_rect.y, | |
| 826 .width = logo_size, | |
| 827 .height = logo_size | |
| 828 }; | |
| 829 | |
| 830 Rectangle source = { 0, 0, logo_texture.width, logo_texture.height }; | |
| 831 DrawTexturePro(logo_texture, source, dest, (Vector2){0, 0}, 0.0f, WHITE); | |
| 832 | |
| 833 BeginScissorMode(history_list_area.x, history_list_area.y, history_list_area.width, history_list_area.height); | |
| 834 for (int i = 0; i < total; i++) | |
| 835 { | |
| 836 HistoryItem *curr_history_items = i < new_history_items_length ? | |
| 837 &new_history_items[i - new_history_items_length - 1] : &history_items[i - new_history_items_length]; | |
| 838 | |
| 839 if (curr_history_items->deleted) | |
| 840 continue; | |
| 841 | |
| 842 float diff = curr_history_items->rect.height*0.3; | |
| 843 // DrawRectangleRec(curr_history_items->rect, Fade(RED, 0.1f)); | |
| 844 Rectangle filename_area = curr_history_items->rect; | |
| 845 | |
| 846 filename_area.height -= diff; | |
| 847 Rectangle icon_area = Below(filename_area, 0); | |
| 848 icon_area.height = diff; | |
| 849 | |
| 850 DrawRectangleRec(filename_area, Fade(BLUE, 0.1f)); | |
| 851 DrawRectangleRec(icon_area, Fade(YELLOW, 0.1f)); | |
| 852 | |
| 853 Rectangle icon_area_left_column = LeftColumn(icon_area, 0.5, 0); | |
| 854 Rectangle icon_area_right_column = RightColumn(icon_area, icon_area_left_column, 0); | |
| 855 | |
| 856 GuiDrawText(curr_history_items->title, AddPadding(filename_area, padding), TEXT_ALIGN_CENTER, RED); | |
| 857 if (GuiButton(AddPaddingHorizontal(icon_area_left_column, padding), "view")) | |
| 858 PostDog_Load_File(curr_history_items->filename); | |
| 859 if (GuiButton(AddPaddingHorizontal(icon_area_right_column,padding), "delete")) | |
| 860 { | |
| 861 if (!remove(PostDog_Construct_URL(curr_history_items->filename))) | |
| 862 curr_history_items->deleted = TRUE; | |
| 863 else | |
| 864 fprintf(stderr, "Wasn't able to delete file: %s \n", curr_history_items->filename); | |
| 865 } | |
| 866 } | |
| 867 EndScissorMode(); | |
| 868 | |
| 869 if (total > 0) | |
| 870 { | |
| 871 float content_height = total * (item_height + padding); | |
| 872 if (content_height > history_list_area.height) | |
| 873 { | |
| 874 float scrollbar_height = (history_list_area.height / content_height) * history_list_area.height; | |
| 875 float scrollbar_y = history_list_area.y - (history_scroll_offset / content_height) * history_list_area.height; | |
| 876 Rectangle scrollbar = { | |
| 877 history_list_area.x + history_list_area.width - 5, | |
| 878 scrollbar_y, | |
| 879 5, | |
| 880 scrollbar_height | |
| 881 }; | |
| 882 DrawRectangleRec(scrollbar, Fade(WHITE, 0.5f)); | |
| 883 } | |
| 884 } | |
| 885 | |
| 886 // URL area Rect | |
| 887 GuiDrawText("URL: ", url_text_bounds, TEXT_ALIGN_CENTER, RED); | |
| 888 DrawRectangleRec(url_area, Fade(RED, 0.1f)); | |
| 889 if (GuiTextBox(url_input_bounds, url_input_text, DEFAULT_TEXT_BUFFER_LENGTH, url_input_edit)) | |
| 890 url_input_edit = !url_input_edit; | |
| 891 | |
| 892 sendRequest = GuiButton(url_enter_button, "ENTER"); | |
| 893 if (sendRequest) | |
| 894 PostDog_Http_Request(); | |
| 895 if (GuiDropdownBox(method_dropdown, "GET;POST;PUT;DELETE", &active_method_dropdown, method_edit)) | |
| 896 method_edit = !method_edit; | |
| 897 | |
| 898 // Input Tabs Rect | |
| 899 DrawRectangleRec(input_area, Fade(BLUE, 0.1f)); | |
| 900 DrawRectangleRec(input_tab, Fade(DARKBLUE, 0.1f)); | |
| 901 GuiSetStyle(TOGGLE, GROUP_PADDING, 0); | |
| 902 GuiDrawRectangle(input_body, 1, GetColor(GuiGetStyle(TEXTBOX, BORDER)), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED))); | |
| 903 | |
| 904 BeginScissorMode(input_body.x, input_body.y, input_body.width, input_body.height); | |
| 905 Rectangle scrolled_input = AddPadding(input_body, padding * 2); | |
| 906 scrolled_input.y += input_body_scroll_offset; | |
| 907 scrolled_input.height = MAX_SCROLL_HEIGHT; | |
| 908 if (JUNE_GuiTextBox(scrolled_input, url_body_map[active_input_tab], DEFAULT_TEXT_BUFFER_LENGTH, input_body_bool)) | |
| 909 input_body_bool = !input_body_bool; | |
| 910 EndScissorMode(); | |
| 911 | |
| 912 if (input_body_scroll_offset < 0) { | |
| 913 float scrollbar_height = 10; | |
| 914 float scrollbar_y = input_body.y - (input_body_scroll_offset / MAX_SCROLL_HEIGHT) * (input_body.height - scrollbar_height); | |
| 915 Rectangle scrollbar = { | |
| 916 input_body.x + input_body.width - 5, | |
| 917 scrollbar_y, | |
| 918 5, | |
| 919 scrollbar_height | |
| 920 }; | |
| 921 DrawRectangleRec(scrollbar, Fade(BLUE, 0.5f)); | |
| 922 } | |
| 923 | |
| 924 GuiToggleGroup(input_tab_item, "Header;Body;Get Param;Bar", &active_input_tab); | |
| 925 | |
| 926 PostDog_Update_URL(); | |
| 927 | |
| 928 // Result Rect | |
| 929 DrawRectangleRec(result_area, Fade(GREEN, 0.1f)); | |
| 930 DrawRectangleRec(result_body, Fade(DARKGREEN, 0.1f)); | |
| 931 | |
| 932 // Create scrollable result body with offset | |
| 933 BeginScissorMode(result_body.x, result_body.y, result_body.width, result_body.height); | |
| 934 Rectangle scrolled_result = result_body; | |
| 935 scrolled_result.y += result_body_scroll_offset; | |
| 936 scrolled_result.height = MAX_SCROLL_HEIGHT; | |
| 937 GuiTextBoxMulti(scrolled_result, url_result_text, RESULT_BUFFER_LENGTH, false); | |
| 938 EndScissorMode(); | |
| 939 | |
| 940 if (result_body_scroll_offset < 0) { | |
| 941 float scrollbar_height = 10; | |
| 942 float scrollbar_y = result_body.y - (result_body_scroll_offset / MAX_SCROLL_HEIGHT) * (result_body.height - scrollbar_height); | |
| 943 Rectangle scrollbar = { | |
| 944 result_body.x + result_body.width - 5, | |
| 945 scrollbar_y, | |
| 946 5, | |
| 947 scrollbar_height | |
| 948 }; | |
| 949 DrawRectangleRec(scrollbar, Fade(GREEN, 0.5f)); | |
| 950 } | |
| 951 | |
| 952 if (url_input_edit && (IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C)) | |
| 953 { | |
| 954 SetClipboardText(url_input_text); | |
| 955 } | |
| 956 else if (input_body_bool && (IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C)) | |
| 957 { | |
| 958 SetClipboardText(url_body_map[active_input_tab]); | |
| 959 } | |
| 960 else if (InArea(mouse_position, result_body)) | |
| 961 { | |
| 962 DrawRectangleRec(result_body, Fade(GREEN, 0.3f)); | |
| 963 if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C)) | |
| 964 SetClipboardText(url_result_text); | |
| 965 } | |
| 966 EndDrawing(); | 2395 EndDrawing(); |
| 967 } | 2396 } |
| 2397 | |
| 2398 // ======================================================================== | |
| 2399 // CLEANUP | |
| 2400 // ======================================================================== | |
| 2401 GuiTextAreaResetAllStates(); | |
| 2402 Dowa_Arena_Free(g_text_area_arena); | |
| 2403 | |
| 2404 uv_mutex_destroy(&history_mutex); | |
| 2405 uv_loop_close(main_loop); | |
| 2406 | |
| 2407 UnloadTexture(logo_texture); | |
| 968 CloseWindow(); | 2408 CloseWindow(); |
| 2409 | |
| 969 return 0; | 2410 return 0; |
| 970 } | 2411 } |