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 }