comparison postdog/main.c @ 163:058de208e640

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