comparison infinite_canvas/canvas.c @ 278:8d560f50ed4c

Improve infinite canvas interactions and browser chrome Render Lucide icons directly with Raylib, add searchable icon browsing, robust text editing, entity lifecycle animations, z-order-safe input, semantic themes, and animated editable browser controls. Document rendering, pinning, context, and component extension for future agents. Co-authored-by: Copilot <[email protected]> Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:16:14 -0700
parents b55c22cff335
children 49e9e591c9bb
comparison
equal deleted inserted replaced
277:1d99147f520c 278:8d560f50ed4c
1 #include "infinite_canvas/canvas.h" 1 #include "infinite_canvas/canvas.h"
2 #include "infinite_canvas/generated/lucide_data.h"
2 3
3 #include <math.h> 4 #include <math.h>
4 #include <stdarg.h> 5 #include <stdarg.h>
5 #include <stdio.h> 6 #include <stdio.h>
6 #include <string.h> 7 #include <string.h>
22 "Ready", 23 "Ready",
23 "Live", 24 "Live",
24 "Draft", 25 "Draft",
25 "Synced", 26 "Synced",
26 }; 27 };
28
29 #define CANVAS_LUCIDE_COLUMNS 10
30 #define CANVAS_LUCIDE_CARD_WIDTH 128.0f
31 #define CANVAS_LUCIDE_CARD_HEIGHT 96.0f
32 #define CANVAS_LUCIDE_GAP 12.0f
33 #define CANVAS_LUCIDE_HEADER_HEIGHT 88.0f
34 #define CANVAS_LUCIDE_PADDING 24.0f
35 #define CANVAS_LUCIDE_SEARCH_WIDTH 360.0f
36 #define CANVAS_LUCIDE_SEARCH_HEIGHT 42.0f
37 #define CANVAS_TEXT_AREA_PADDING 16.0f
38 #define CANVAS_TEXT_AREA_FONT_SIZE 16.0f
39 #define CANVAS_TEXT_AREA_SPACING 0.1f
40 #define CANVAS_TEXT_AREA_LINE_HEIGHT 22.0f
41
42 typedef struct {
43 int32 start;
44 int32 end;
45 int32 next;
46 float width;
47 } Canvas_Text_Line;
27 48
28 static float Canvas_Clamp(float value, float min_value, float max_value) 49 static float Canvas_Clamp(float value, float min_value, float max_value)
29 { 50 {
30 if (value < min_value) return min_value; 51 if (value < min_value) return min_value;
31 if (value > max_value) return max_value; 52 if (value > max_value) return max_value;
51 (uint8)((float)from.r + ((float)to.r - (float)from.r) * amount), 72 (uint8)((float)from.r + ((float)to.r - (float)from.r) * amount),
52 (uint8)((float)from.g + ((float)to.g - (float)from.g) * amount), 73 (uint8)((float)from.g + ((float)to.g - (float)from.g) * amount),
53 (uint8)((float)from.b + ((float)to.b - (float)from.b) * amount), 74 (uint8)((float)from.b + ((float)to.b - (float)from.b) * amount),
54 (uint8)((float)from.a + ((float)to.a - (float)from.a) * amount), 75 (uint8)((float)from.a + ((float)to.a - (float)from.a) * amount),
55 }; 76 };
77 }
78
79 static Color Canvas_Color_With_Opacity(Color color, float opacity)
80 {
81 color.a = (uint8)((float)color.a * Canvas_Clamp(opacity, 0.0f, 1.0f));
82 return color;
83 }
84
85 static Color Canvas_Color_Fade(Color color, float amount)
86 {
87 return Canvas_Color_With_Opacity(color, amount);
88 }
89
90 static float Canvas_Entity_Visibility(const Canvas_Entity *p_entity)
91 {
92 float amount = Canvas_Clamp(p_entity->visibility_amount, 0.0f, 1.0f);
93 return amount * amount * (3.0f - 2.0f * amount);
94 }
95
96 static Canvas_Theme Canvas_Theme_With_Opacity(
97 const Canvas_Theme *p_theme,
98 float opacity)
99 {
100 Canvas_Theme faded = *p_theme;
101 faded.background = Canvas_Color_With_Opacity(faded.background, opacity);
102 faded.surface = Canvas_Color_With_Opacity(faded.surface, opacity);
103 faded.surface_muted = Canvas_Color_With_Opacity(faded.surface_muted, opacity);
104 faded.border = Canvas_Color_With_Opacity(faded.border, opacity);
105 faded.text = Canvas_Color_With_Opacity(faded.text, opacity);
106 faded.text_muted = Canvas_Color_With_Opacity(faded.text_muted, opacity);
107 faded.accent = Canvas_Color_With_Opacity(faded.accent, opacity);
108 faded.accent_soft = Canvas_Color_With_Opacity(faded.accent_soft, opacity);
109 faded.on_accent = Canvas_Color_With_Opacity(faded.on_accent, opacity);
110 faded.selection = Canvas_Color_With_Opacity(faded.selection, opacity);
111 faded.entity_outline = Canvas_Color_With_Opacity(faded.entity_outline, opacity);
112 faded.button = Canvas_Color_With_Opacity(faded.button, opacity);
113 faded.button_hover = Canvas_Color_With_Opacity(faded.button_hover, opacity);
114 faded.button_active = Canvas_Color_With_Opacity(faded.button_active, opacity);
115 faded.button_active_hover = Canvas_Color_With_Opacity(
116 faded.button_active_hover,
117 opacity);
118 faded.switch_track_off = Canvas_Color_With_Opacity(
119 faded.switch_track_off,
120 opacity);
121 faded.switch_track_off_hover = Canvas_Color_With_Opacity(
122 faded.switch_track_off_hover,
123 opacity);
124 faded.switch_track_on = Canvas_Color_With_Opacity(
125 faded.switch_track_on,
126 opacity);
127 faded.switch_track_on_hover = Canvas_Color_With_Opacity(
128 faded.switch_track_on_hover,
129 opacity);
130 faded.switch_thumb = Canvas_Color_With_Opacity(faded.switch_thumb, opacity);
131 faded.pin = Canvas_Color_With_Opacity(faded.pin, opacity);
132 faded.danger = Canvas_Color_With_Opacity(faded.danger, opacity);
133 faded.shadow = Canvas_Color_With_Opacity(faded.shadow, opacity);
134 faded.toast_surface = Canvas_Color_With_Opacity(faded.toast_surface, opacity);
135 faded.toast_border = Canvas_Color_With_Opacity(faded.toast_border, opacity);
136 return faded;
137 }
138
139 static boolean Canvas_Control_Is_Down(void)
140 {
141 return IsKeyDown(KEY_LEFT_CONTROL) ||
142 IsKeyDown(KEY_RIGHT_CONTROL) ||
143 IsKeyDown(KEY_LEFT_SUPER) ||
144 IsKeyDown(KEY_RIGHT_SUPER);
145 }
146
147 static boolean Canvas_Shift_Is_Down(void);
148 static boolean Canvas_Key_Activated(int32 key);
149
150 static char Canvas_Ascii_Lower(char value)
151 {
152 if (value >= 'A' && value <= 'Z') return (char)(value + ('a' - 'A'));
153 return value;
154 }
155
156 static boolean Canvas_Lucide_Name_Matches(
157 const char *p_name,
158 const char *p_query)
159 {
160 if (!p_query || !p_query[0]) return TRUE;
161 for (const char *p_start = p_name; *p_start; p_start++) {
162 const char *p_name_cursor = p_start;
163 const char *p_query_cursor = p_query;
164 while (*p_name_cursor &&
165 *p_query_cursor &&
166 Canvas_Ascii_Lower(*p_name_cursor) ==
167 Canvas_Ascii_Lower(*p_query_cursor)) {
168 p_name_cursor++;
169 p_query_cursor++;
170 }
171 if (!*p_query_cursor) return TRUE;
172 }
173 return FALSE;
174 }
175
176 int32 Canvas_Lucide_Count_Matches(const char *p_query)
177 {
178 int32 count = 0;
179 for (int32 index = 0; index < CANVAS_LUCIDE_ICON_COUNT; index++) {
180 if (Canvas_Lucide_Name_Matches(
181 CANVAS_LUCIDE_ICONS[index].p_name,
182 p_query)) {
183 count++;
184 }
185 }
186 return count;
187 }
188
189 static Rectangle Canvas_Lucide_Search_Bounds(const Canvas_Entity *p_entity)
190 {
191 return (Rectangle){
192 p_entity->position.x + p_entity->size.x -
193 CANVAS_LUCIDE_PADDING - CANVAS_LUCIDE_SEARCH_WIDTH,
194 p_entity->position.y +
195 (CANVAS_LUCIDE_HEADER_HEIGHT - CANVAS_LUCIDE_SEARCH_HEIGHT) * 0.5f,
196 CANVAS_LUCIDE_SEARCH_WIDTH,
197 CANVAS_LUCIDE_SEARCH_HEIGHT,
198 };
199 }
200
201 static float Canvas_Lucide_Content_Height(const char *p_query)
202 {
203 int32 icon_count = Canvas_Lucide_Count_Matches(p_query);
204 int32 rows =
205 (icon_count + CANVAS_LUCIDE_COLUMNS - 1) /
206 CANVAS_LUCIDE_COLUMNS;
207 if (rows == 0) {
208 return CANVAS_LUCIDE_HEADER_HEIGHT + CANVAS_LUCIDE_PADDING * 2.0f;
209 }
210 return CANVAS_LUCIDE_HEADER_HEIGHT +
211 CANVAS_LUCIDE_PADDING +
212 (float)rows * CANVAS_LUCIDE_CARD_HEIGHT +
213 (float)(rows - 1) * CANVAS_LUCIDE_GAP +
214 CANVAS_LUCIDE_PADDING;
215 }
216
217 static float Canvas_Lucide_Max_Scroll(const Canvas_Entity *p_entity)
218 {
219 return fmaxf(
220 0.0f,
221 Canvas_Lucide_Content_Height(p_entity->text) - p_entity->size.y);
222 }
223
224 static const Canvas_Lucide_Icon *Canvas_Lucide_Find_Icon(
225 const char *p_name)
226 {
227 for (int32 index = 0; index < CANVAS_LUCIDE_ICON_COUNT; index++) {
228 if (strcmp(CANVAS_LUCIDE_ICONS[index].p_name, p_name) == 0) {
229 return &CANVAS_LUCIDE_ICONS[index];
230 }
231 }
232 return NULL;
233 }
234
235 static void Canvas_Lucide_Draw_Icon(
236 const Canvas_Lucide_Icon *p_icon,
237 Vector2 origin,
238 float scale,
239 float line_thickness,
240 Color color)
241 {
242 if (!p_icon) return;
243 for (uint32 segment_index = p_icon->first_segment;
244 segment_index < p_icon->first_segment + p_icon->segment_count;
245 segment_index++) {
246 const Canvas_Lucide_Segment *p_segment =
247 &CANVAS_LUCIDE_SEGMENTS[segment_index];
248 DrawLineEx(
249 (Vector2){
250 origin.x + p_segment->x1 * scale,
251 origin.y + p_segment->y1 * scale,
252 },
253 (Vector2){
254 origin.x + p_segment->x2 * scale,
255 origin.y + p_segment->y2 * scale,
256 },
257 line_thickness,
258 color);
259 }
260 }
261
262 static Rectangle Canvas_Web_Toolbar_Bounds(
263 const Canvas_Camera *p_camera,
264 const Canvas_Entity *p_entity)
265 {
266 float zoom = p_camera->zoom;
267 return (Rectangle){
268 p_entity->position.x + CANVAS_WEB_FRAME_INSET / zoom,
269 p_entity->position.y + CANVAS_WEB_FRAME_INSET / zoom,
270 p_entity->size.x -
271 (CANVAS_WEB_FRAME_INSET + CANVAS_WEB_RESIZE_HANDLE) / zoom,
272 CANVAS_WEB_TOOLBAR_HEIGHT *
273 (p_entity->web_chrome_amount *
274 p_entity->web_chrome_amount *
275 (3.0f - 2.0f * p_entity->web_chrome_amount)) /
276 zoom,
277 };
278 }
279
280 static Rectangle Canvas_Web_Toolbar_Button_Bounds(
281 Rectangle toolbar,
282 float zoom,
283 int32 button_index)
284 {
285 return (Rectangle){
286 toolbar.x + (6.0f + (float)button_index * 34.0f) / zoom,
287 toolbar.y + 7.0f / zoom,
288 28.0f / zoom,
289 28.0f / zoom,
290 };
291 }
292
293 static Rectangle Canvas_Web_Address_Bounds(Rectangle toolbar, float zoom)
294 {
295 return (Rectangle){
296 toolbar.x + 76.0f / zoom,
297 toolbar.y + 7.0f / zoom,
298 toolbar.width - 82.0f / zoom,
299 28.0f / zoom,
300 };
301 }
302
303 static void Canvas_Web_URL_Selection_Bounds(
304 const Canvas_Scene *p_scene,
305 int32 *p_start,
306 int32 *p_end)
307 {
308 *p_start = p_scene->web_url_cursor;
309 *p_end = p_scene->web_url_selection_anchor;
310 if (*p_start > *p_end) {
311 int32 swap = *p_start;
312 *p_start = *p_end;
313 *p_end = swap;
314 }
315 }
316
317 static void Canvas_Web_URL_Delete_Selection(Canvas_Scene *p_scene)
318 {
319 int32 start = 0;
320 int32 end = 0;
321 Canvas_Web_URL_Selection_Bounds(p_scene, &start, &end);
322 if (start == end) return;
323 size_t length = strlen(p_scene->web_url_draft);
324 memmove(
325 p_scene->web_url_draft + start,
326 p_scene->web_url_draft + end,
327 length - (size_t)end + 1);
328 p_scene->web_url_cursor = start;
329 p_scene->web_url_selection_anchor = start;
330 }
331
332 static void Canvas_Web_URL_Insert(Canvas_Scene *p_scene, const char *p_text)
333 {
334 Canvas_Web_URL_Delete_Selection(p_scene);
335 size_t length = strlen(p_scene->web_url_draft);
336 size_t available = sizeof(p_scene->web_url_draft) - length - 1;
337 if (available == 0) return;
338 char filtered[CANVAS_ENTITY_TEXT_CAPACITY];
339 size_t filtered_length = 0;
340 for (const char *p_cursor = p_text;
341 *p_cursor && filtered_length < available;
342 p_cursor++) {
343 if (*p_cursor >= 32 && *p_cursor <= 126) {
344 filtered[filtered_length++] = *p_cursor;
345 }
346 }
347 if (filtered_length == 0) return;
348 int32 cursor = p_scene->web_url_cursor;
349 memmove(
350 p_scene->web_url_draft + cursor + filtered_length,
351 p_scene->web_url_draft + cursor,
352 length - (size_t)cursor + 1);
353 memcpy(p_scene->web_url_draft + cursor, filtered, filtered_length);
354 p_scene->web_url_cursor += (int32)filtered_length;
355 p_scene->web_url_selection_anchor = p_scene->web_url_cursor;
356 }
357
358 static int32 Canvas_Web_URL_Cursor_At_Point(
359 Font font,
360 const Canvas_Scene *p_scene,
361 Rectangle address,
362 float zoom,
363 Vector2 point)
364 {
365 float font_size = 13.0f / zoom;
366 float local_x =
367 point.x - address.x - 9.0f / zoom + p_scene->web_url_scroll_x;
368 int32 length = (int32)strlen(p_scene->web_url_draft);
369 for (int32 cursor = 0; cursor < length; cursor++) {
370 char prefix[CANVAS_ENTITY_TEXT_CAPACITY];
371 memcpy(prefix, p_scene->web_url_draft, (size_t)cursor + 1);
372 prefix[cursor + 1] = '\0';
373 float right = MeasureTextEx(font, prefix, font_size, 0.0f).x;
374 prefix[cursor] = '\0';
375 float left = MeasureTextEx(font, prefix, font_size, 0.0f).x;
376 if (local_x < (left + right) * 0.5f) return cursor;
377 }
378 return length;
379 }
380
381 static void Canvas_Web_URL_Ensure_Cursor_Visible(
382 Canvas_Scene *p_scene,
383 Font font,
384 Rectangle address,
385 float zoom)
386 {
387 char prefix[CANVAS_ENTITY_TEXT_CAPACITY];
388 memcpy(
389 prefix,
390 p_scene->web_url_draft,
391 (size_t)p_scene->web_url_cursor);
392 prefix[p_scene->web_url_cursor] = '\0';
393 float cursor_x = MeasureTextEx(font, prefix, 13.0f / zoom, 0.0f).x;
394 float visible_width = address.width - 18.0f / zoom;
395 if (cursor_x < p_scene->web_url_scroll_x) {
396 p_scene->web_url_scroll_x = cursor_x;
397 } else if (cursor_x > p_scene->web_url_scroll_x + visible_width) {
398 p_scene->web_url_scroll_x = cursor_x - visible_width;
399 }
400 if (p_scene->web_url_scroll_x < 0.0f) p_scene->web_url_scroll_x = 0.0f;
401 }
402
403 static void Canvas_Web_URL_Copy_Selection(Canvas_Scene *p_scene)
404 {
405 int32 start = 0;
406 int32 end = 0;
407 Canvas_Web_URL_Selection_Bounds(p_scene, &start, &end);
408 if (start == end) return;
409 char selected[CANVAS_ENTITY_TEXT_CAPACITY];
410 memcpy(
411 selected,
412 p_scene->web_url_draft + start,
413 (size_t)(end - start));
414 selected[end - start] = '\0';
415 SetClipboardText(selected);
416 }
417
418 static void Canvas_Web_URL_Normalize(char *p_url, size_t capacity)
419 {
420 if (!p_url[0] ||
421 strstr(p_url, "://") ||
422 strncmp(p_url, "about:", 6) == 0 ||
423 strncmp(p_url, "data:", 5) == 0 ||
424 strncmp(p_url, "file:", 5) == 0) {
425 return;
426 }
427 char normalized[CANVAS_ENTITY_TEXT_CAPACITY];
428 snprintf(normalized, sizeof(normalized), "https://%s", p_url);
429 snprintf(p_url, capacity, "%s", normalized);
430 }
431
432 Canvas_Web_Chrome_Action Canvas_Web_Chrome_Update_Input(
433 const Canvas_Camera *p_camera,
434 Canvas_Scene *p_scene,
435 Font font,
436 const char *p_current_url)
437 {
438 if (p_scene->selected_index < 0 ||
439 p_scene->selected_index >=
440 (int32)Dowa_Array_Length(p_scene->p_entities)) {
441 p_scene->editing_web_url = FALSE;
442 return CANVAS_WEB_CHROME_NONE;
443 }
444 Canvas_Entity *p_entity =
445 &p_scene->p_entities[p_scene->selected_index];
446 if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT ||
447 p_entity->removing) {
448 p_scene->editing_web_url = FALSE;
449 return CANVAS_WEB_CHROME_NONE;
450 }
451
452 Rectangle toolbar = Canvas_Web_Toolbar_Bounds(p_camera, p_entity);
453 Rectangle address = Canvas_Web_Address_Bounds(toolbar, p_camera->zoom);
454 Vector2 pointer = Canvas_Camera_Screen_To_World(
455 p_camera,
456 GetMousePosition());
457 if (p_scene->editing_web_url &&
458 p_scene->web_url_entity_id == p_entity->id) {
459 boolean shortcut = Canvas_Control_Is_Down();
460 boolean extend = Canvas_Shift_Is_Down();
461 int32 length = (int32)strlen(p_scene->web_url_draft);
462 if (shortcut && IsKeyPressed(KEY_A)) {
463 p_scene->web_url_selection_anchor = 0;
464 p_scene->web_url_cursor = length;
465 }
466 if (shortcut && IsKeyPressed(KEY_C)) {
467 Canvas_Web_URL_Copy_Selection(p_scene);
468 }
469 if (shortcut && IsKeyPressed(KEY_X)) {
470 Canvas_Web_URL_Copy_Selection(p_scene);
471 Canvas_Web_URL_Delete_Selection(p_scene);
472 }
473 if (shortcut && IsKeyPressed(KEY_V)) {
474 const char *p_clipboard = GetClipboardText();
475 if (p_clipboard) Canvas_Web_URL_Insert(p_scene, p_clipboard);
476 }
477 if (Canvas_Key_Activated(KEY_LEFT)) {
478 int32 cursor = p_scene->web_url_cursor;
479 if (!extend &&
480 cursor != p_scene->web_url_selection_anchor) {
481 int32 end = 0;
482 Canvas_Web_URL_Selection_Bounds(p_scene, &cursor, &end);
483 } else {
484 cursor--;
485 }
486 if (cursor < 0) cursor = 0;
487 p_scene->web_url_cursor = cursor;
488 if (!extend) p_scene->web_url_selection_anchor = cursor;
489 }
490 if (Canvas_Key_Activated(KEY_RIGHT)) {
491 int32 cursor = p_scene->web_url_cursor;
492 if (!extend &&
493 cursor != p_scene->web_url_selection_anchor) {
494 int32 start = 0;
495 Canvas_Web_URL_Selection_Bounds(p_scene, &start, &cursor);
496 } else {
497 cursor++;
498 }
499 if (cursor > length) cursor = length;
500 p_scene->web_url_cursor = cursor;
501 if (!extend) p_scene->web_url_selection_anchor = cursor;
502 }
503 if (Canvas_Key_Activated(KEY_HOME)) {
504 p_scene->web_url_cursor = 0;
505 if (!extend) p_scene->web_url_selection_anchor = 0;
506 }
507 if (Canvas_Key_Activated(KEY_END)) {
508 p_scene->web_url_cursor = length;
509 if (!extend) p_scene->web_url_selection_anchor = length;
510 }
511 if (Canvas_Key_Activated(KEY_BACKSPACE)) {
512 if (p_scene->web_url_cursor !=
513 p_scene->web_url_selection_anchor) {
514 Canvas_Web_URL_Delete_Selection(p_scene);
515 } else if (p_scene->web_url_cursor > 0) {
516 p_scene->web_url_selection_anchor =
517 p_scene->web_url_cursor - 1;
518 Canvas_Web_URL_Delete_Selection(p_scene);
519 }
520 }
521 if (Canvas_Key_Activated(KEY_DELETE)) {
522 if (p_scene->web_url_cursor !=
523 p_scene->web_url_selection_anchor) {
524 Canvas_Web_URL_Delete_Selection(p_scene);
525 } else if (p_scene->web_url_cursor < length) {
526 p_scene->web_url_selection_anchor =
527 p_scene->web_url_cursor + 1;
528 Canvas_Web_URL_Delete_Selection(p_scene);
529 }
530 }
531 int32 codepoint = GetCharPressed();
532 while (codepoint > 0) {
533 if (!shortcut && codepoint >= 32 && codepoint <= 126) {
534 char inserted[2] = {(char)codepoint, '\0'};
535 Canvas_Web_URL_Insert(p_scene, inserted);
536 }
537 codepoint = GetCharPressed();
538 }
539 Canvas_Web_URL_Ensure_Cursor_Visible(
540 p_scene,
541 font,
542 address,
543 p_camera->zoom);
544 if (IsKeyPressed(KEY_ESCAPE)) {
545 p_scene->editing_web_url = FALSE;
546 }
547 if (IsKeyPressed(KEY_ENTER)) {
548 Canvas_Web_URL_Normalize(
549 p_scene->web_url_draft,
550 sizeof(p_scene->web_url_draft));
551 snprintf(
552 p_entity->text,
553 sizeof(p_entity->text),
554 "%s",
555 p_scene->web_url_draft);
556 p_scene->editing_web_url = FALSE;
557 return CANVAS_WEB_CHROME_NAVIGATE;
558 }
559 }
560
561 if (!CheckCollisionPointRec(pointer, toolbar)) {
562 if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
563 p_scene->editing_web_url = FALSE;
564 return CANVAS_WEB_CHROME_NONE;
565 }
566 return p_scene->editing_web_url ?
567 CANVAS_WEB_CHROME_CAPTURE :
568 CANVAS_WEB_CHROME_NONE;
569 }
570 if (!IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
571 return CANVAS_WEB_CHROME_CAPTURE;
572 }
573 if (CheckCollisionPointRec(
574 pointer,
575 Canvas_Web_Toolbar_Button_Bounds(toolbar, p_camera->zoom, 0))) {
576 return CANVAS_WEB_CHROME_BACK;
577 }
578 if (CheckCollisionPointRec(
579 pointer,
580 Canvas_Web_Toolbar_Button_Bounds(toolbar, p_camera->zoom, 1))) {
581 return CANVAS_WEB_CHROME_FORWARD;
582 }
583 if (CheckCollisionPointRec(pointer, address)) {
584 if (!p_scene->editing_web_url ||
585 p_scene->web_url_entity_id != p_entity->id) {
586 snprintf(
587 p_scene->web_url_draft,
588 sizeof(p_scene->web_url_draft),
589 "%s",
590 p_current_url && p_current_url[0] ?
591 p_current_url :
592 p_entity->text);
593 p_scene->web_url_entity_id = p_entity->id;
594 p_scene->web_url_cursor =
595 (int32)strlen(p_scene->web_url_draft);
596 p_scene->web_url_selection_anchor = 0;
597 p_scene->web_url_scroll_x = 0.0f;
598 p_scene->editing_web_url = TRUE;
599 } else {
600 int32 cursor = Canvas_Web_URL_Cursor_At_Point(
601 font,
602 p_scene,
603 address,
604 p_camera->zoom,
605 pointer);
606 p_scene->web_url_cursor = cursor;
607 if (!Canvas_Shift_Is_Down()) {
608 p_scene->web_url_selection_anchor = cursor;
609 }
610 }
611 Canvas_Web_URL_Ensure_Cursor_Visible(
612 p_scene,
613 font,
614 address,
615 p_camera->zoom);
616 return CANVAS_WEB_CHROME_CAPTURE;
617 }
618 p_scene->editing_web_url = FALSE;
619 return CANVAS_WEB_CHROME_CAPTURE;
620 }
621
622 boolean Canvas_Web_Chrome_Is_Editing(const Canvas_Scene *p_scene)
623 {
624 return p_scene->editing_web_url;
56 } 625 }
57 626
58 void Canvas_Camera_Init(Canvas_Camera *p_camera, int32 width, int32 height) 627 void Canvas_Camera_Init(Canvas_Camera *p_camera, int32 width, int32 height)
59 { 628 {
60 *p_camera = (Canvas_Camera){ 629 *p_camera = (Canvas_Camera){
127 return (Rectangle){ 696 return (Rectangle){
128 .x = min.x, 697 .x = min.x,
129 .y = min.y, 698 .y = min.y,
130 .width = max.x - min.x, 699 .width = max.x - min.x,
131 .height = max.y - min.y, 700 .height = max.y - min.y,
701 };
702 }
703
704 void Canvas_Camera_Fit_Bounds(
705 Canvas_Camera *p_camera,
706 Rectangle bounds,
707 float padding_left,
708 float padding_top,
709 float padding_right,
710 float padding_bottom)
711 {
712 float available_width = fmaxf(
713 1.0f,
714 (float)p_camera->viewport_width - padding_left - padding_right);
715 float available_height = fmaxf(
716 1.0f,
717 (float)p_camera->viewport_height - padding_top - padding_bottom);
718 float zoom = fminf(
719 available_width / fmaxf(bounds.width, 1.0f),
720 available_height / fmaxf(bounds.height, 1.0f));
721 p_camera->zoom = Canvas_Clamp(
722 zoom,
723 p_camera->min_zoom,
724 p_camera->max_zoom);
725
726 Vector2 world_center = {
727 bounds.x + bounds.width * 0.5f,
728 bounds.y + bounds.height * 0.5f,
729 };
730 Vector2 desired_screen_center = {
731 padding_left + available_width * 0.5f,
732 padding_top + available_height * 0.5f,
733 };
734 Vector2 viewport_center = {
735 (float)p_camera->viewport_width * 0.5f,
736 (float)p_camera->viewport_height * 0.5f,
737 };
738 p_camera->target = (Vector2){
739 world_center.x -
740 (desired_screen_center.x - viewport_center.x) / p_camera->zoom,
741 world_center.y -
742 (desired_screen_center.y - viewport_center.y) / p_camera->zoom,
132 }; 743 };
133 } 744 }
134 745
135 static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity) 746 static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity)
136 { 747 {
254 case CANVAS_ENTITY_TEXT_AREA: 865 case CANVAS_ENTITY_TEXT_AREA:
255 Canvas_Context_Append(p_writer, "content="); 866 Canvas_Context_Append(p_writer, "content=");
256 Canvas_Context_Append_Quoted(p_writer, p_entity->text); 867 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
257 Canvas_Context_Append( 868 Canvas_Context_Append(
258 p_writer, 869 p_writer,
259 " editing=%s", 870 " editing=%s cursor=%d selection_anchor=%d",
260 p_entity->active ? "true" : "false"); 871 p_entity->active ? "true" : "false",
872 p_entity->text_cursor,
873 p_entity->text_selection_anchor);
261 break; 874 break;
262 case CANVAS_ENTITY_DROPDOWN: 875 case CANVAS_ENTITY_DROPDOWN:
263 Canvas_Context_Append(p_writer, "selected="); 876 Canvas_Context_Append(p_writer, "selected=");
264 Canvas_Context_Append_Quoted( 877 Canvas_Context_Append_Quoted(
265 p_writer, 878 p_writer,
334 break; 947 break;
335 case CANVAS_ENTITY_WEB_CONTENT: 948 case CANVAS_ENTITY_WEB_CONTENT:
336 Canvas_Context_Append(p_writer, "url="); 949 Canvas_Context_Append(p_writer, "url=");
337 Canvas_Context_Append_Quoted(p_writer, p_entity->text); 950 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
338 break; 951 break;
952 case CANVAS_ENTITY_LUCIDE_GALLERY:
953 Canvas_Context_Append(
954 p_writer,
955 "icons=%d query=",
956 Canvas_Lucide_Count_Matches(p_entity->text));
957 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
958 Canvas_Context_Append(
959 p_writer,
960 " search_focused=%s scroll_offset=%d renderer=\"raylib\"",
961 p_entity->active ? "true" : "false",
962 p_entity->value);
963 break;
339 default: 964 default:
340 Canvas_Context_Append(p_writer, "value=%d", p_entity->value); 965 Canvas_Context_Append(p_writer, "value=%d", p_entity->value);
341 break; 966 break;
342 } 967 }
343 } 968 }
375 if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue; 1000 if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue;
376 1001
377 snapshot.visible_count++; 1002 snapshot.visible_count++;
378 Canvas_Context_Append( 1003 Canvas_Context_Append(
379 &writer, 1004 &writer,
380 "- id=%u type=\"%s\" position=[%.1f, %.1f] size=[%.1f, %.1f] pinned=%s\n" 1005 "- id=%u type=\"%s\" position=[%.1f, %.1f] size=[%.1f, %.1f] pinned=%s",
381 " state: ",
382 p_entity->id, 1006 p_entity->id,
383 Canvas_Entity_Type_Name(p_entity->type), 1007 Canvas_Entity_Type_Name(p_entity->type),
384 p_entity->position.x, 1008 p_entity->position.x,
385 p_entity->position.y, 1009 p_entity->position.y,
386 p_entity->size.x, 1010 p_entity->size.x,
387 p_entity->size.y, 1011 p_entity->size.y,
388 p_entity->pinned ? "true" : "false"); 1012 p_entity->pinned ? "true" : "false");
1013 Canvas_Context_Append(
1014 &writer,
1015 " visibility=%.2f removing=%s\n"
1016 " state: ",
1017 p_entity->visibility_amount,
1018 p_entity->removing ? "true" : "false");
389 Canvas_Context_Append_Entity_State(&writer, p_entity); 1019 Canvas_Context_Append_Entity_State(&writer, p_entity);
390 Canvas_Context_Append(&writer, "\n"); 1020 Canvas_Context_Append(&writer, "\n");
391 } 1021 }
392 if (snapshot.visible_count == 0) { 1022 if (snapshot.visible_count == 0) {
393 Canvas_Context_Append(&writer, " (none)\n"); 1023 Canvas_Context_Append(&writer, " (none)\n");
428 .position = position, 1058 .position = position,
429 .size = {160.0f, 100.0f}, 1059 .size = {160.0f, 100.0f},
430 .color = Canvas_Entity_Color(index), 1060 .color = Canvas_Entity_Color(index),
431 .text = {0}, 1061 .text = {0},
432 .value = 0, 1062 .value = 0,
1063 .visibility_amount = 0.0f,
433 .active = FALSE, 1064 .active = FALSE,
434 .pinned = FALSE, 1065 .pinned = FALSE,
1066 .removing = FALSE,
435 .hover_amount = 0.0f, 1067 .hover_amount = 0.0f,
436 .animation_amount = 0.0f, 1068 .animation_amount = 0.0f,
437 }; 1069 };
438 1070
439 if (type == CANVAS_ENTITY_CIRCLE) entity.size = (Vector2){56.0f, 56.0f}; 1071 if (type == CANVAS_ENTITY_CIRCLE) entity.size = (Vector2){56.0f, 56.0f};
489 } 1121 }
490 if (type == CANVAS_ENTITY_WEB_CONTENT) { 1122 if (type == CANVAS_ENTITY_WEB_CONTENT) {
491 entity.size = (Vector2){720.0f, 460.0f}; 1123 entity.size = (Vector2){720.0f, 460.0f};
492 entity.active = TRUE; 1124 entity.active = TRUE;
493 snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com"); 1125 snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com");
1126 }
1127 if (type == CANVAS_ENTITY_LUCIDE_GALLERY) {
1128 entity.size = (Vector2){1440.0f, 1040.0f};
494 } 1129 }
495 1130
496 Dowa_Array_Push_Arena(p_scene->p_entities, entity, p_scene->p_arena); 1131 Dowa_Array_Push_Arena(p_scene->p_entities, entity, p_scene->p_arena);
497 return TRUE; 1132 return TRUE;
498 } 1133 }
504 p_scene->selected_index = -1; 1139 p_scene->selected_index = -1;
505 p_scene->pressed_index = -1; 1140 p_scene->pressed_index = -1;
506 p_scene->hover_index = -1; 1141 p_scene->hover_index = -1;
507 p_scene->dragging = FALSE; 1142 p_scene->dragging = FALSE;
508 p_scene->resizing = FALSE; 1143 p_scene->resizing = FALSE;
1144 p_scene->selecting_text = FALSE;
1145 p_scene->editing_web_url = FALSE;
1146 p_scene->web_url_entity_id = 0;
1147 p_scene->web_url_draft[0] = '\0';
509 p_scene->notification_stack_amount = 0.0f; 1148 p_scene->notification_stack_amount = 0.0f;
1149 }
1150
1151 void Canvas_Scene_Fade_Out_All(Canvas_Scene *p_scene)
1152 {
1153 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
1154 p_scene->p_entities[index].removing = TRUE;
1155 p_scene->p_entities[index].active = FALSE;
1156 }
1157 p_scene->selected_index = -1;
1158 p_scene->pressed_index = -1;
1159 p_scene->hover_index = -1;
1160 p_scene->dragging = FALSE;
1161 p_scene->resizing = FALSE;
1162 p_scene->selecting_text = FALSE;
1163 p_scene->editing_web_url = FALSE;
1164 }
1165
1166 void Canvas_Scene_Fade_Out_Selected(Canvas_Scene *p_scene)
1167 {
1168 if (p_scene->selected_index < 0 ||
1169 p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) {
1170 return;
1171 }
1172 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
1173 p_entity->removing = TRUE;
1174 p_entity->active = FALSE;
1175 p_scene->selected_index = -1;
1176 p_scene->pressed_index = -1;
1177 p_scene->hover_index = -1;
1178 p_scene->dragging = FALSE;
1179 p_scene->resizing = FALSE;
1180 p_scene->selecting_text = FALSE;
1181 p_scene->editing_web_url = FALSE;
510 } 1182 }
511 1183
512 void Canvas_Scene_Add_Demo(Canvas_Scene *p_scene) 1184 void Canvas_Scene_Add_Demo(Canvas_Scene *p_scene)
513 { 1185 {
514 Canvas_Scene_Clear(p_scene); 1186 Canvas_Scene_Clear(p_scene);
548 Canvas_Entity *p_entity = 1220 Canvas_Entity *p_entity =
549 &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1]; 1221 &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1];
550 p_entity->size = (Vector2){236.0f, 176.0f}; 1222 p_entity->size = (Vector2){236.0f, 176.0f};
551 } 1223 }
552 } 1224 }
1225 }
1226
1227 boolean Canvas_Scene_Show_Lucide_Gallery(
1228 Canvas_Scene *p_scene,
1229 Canvas_Camera *p_camera)
1230 {
1231 int32 gallery_index = -1;
1232 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
1233 const Canvas_Entity *p_entity = &p_scene->p_entities[index];
1234 if (p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) {
1235 gallery_index = (int32)index;
1236 break;
1237 }
1238 }
1239
1240 if (gallery_index < 0) {
1241 if (!Canvas_Scene_Add(
1242 p_scene,
1243 CANVAS_ENTITY_LUCIDE_GALLERY,
1244 (Vector2){1200.0f, -520.0f})) {
1245 return FALSE;
1246 }
1247 gallery_index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1;
1248 }
1249
1250 Canvas_Entity *p_gallery = &p_scene->p_entities[gallery_index];
1251 p_gallery->removing = FALSE;
1252 p_scene->selected_index = gallery_index;
1253 Canvas_Camera_Fit_Bounds(
1254 p_camera,
1255 (Rectangle){
1256 p_gallery->position.x,
1257 p_gallery->position.y,
1258 p_gallery->size.x,
1259 p_gallery->size.y,
1260 },
1261 320.0f,
1262 56.0f,
1263 56.0f,
1264 56.0f);
1265 return TRUE;
553 } 1266 }
554 1267
555 static float Canvas_Distance_To_Segment(Vector2 point, Vector2 start, Vector2 end) 1268 static float Canvas_Distance_To_Segment(Vector2 point, Vector2 start, Vector2 end)
556 { 1269 {
557 Vector2 segment = {end.x - start.x, end.y - start.y}; 1270 Vector2 segment = {end.x - start.x, end.y - start.y};
621 p_entity->position.y, 1334 p_entity->position.y,
622 p_entity->size.x, 1335 p_entity->size.x,
623 height, 1336 height,
624 }) ? TRUE : FALSE; 1337 }) ? TRUE : FALSE;
625 } 1338 }
1339 case CANVAS_ENTITY_LUCIDE_GALLERY:
1340 return CheckCollisionPointRec(
1341 point,
1342 (Rectangle){
1343 p_entity->position.x,
1344 p_entity->position.y,
1345 p_entity->size.x,
1346 p_entity->size.y,
1347 }) ? TRUE : FALSE;
626 case CANVAS_ENTITY_NOTIFICATION: 1348 case CANVAS_ENTITY_NOTIFICATION:
627 if (p_entity->animation_amount <= 0.01f) return FALSE; 1349 if (p_entity->animation_amount <= 0.01f) return FALSE;
628 return CheckCollisionPointRec( 1350 return CheckCollisionPointRec(
629 point, 1351 point,
630 (Rectangle){ 1352 (Rectangle){
681 Vector2 world_pointer, 1403 Vector2 world_pointer,
682 float zoom) 1404 float zoom)
683 { 1405 {
684 for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) { 1406 for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) {
685 const Canvas_Entity *p_entity = &p_scene->p_entities[index]; 1407 const Canvas_Entity *p_entity = &p_scene->p_entities[index];
1408 if (p_entity->removing) continue;
686 if (p_entity->type == CANVAS_ENTITY_DROPDOWN && 1409 if (p_entity->type == CANVAS_ENTITY_DROPDOWN &&
687 p_entity->active && 1410 p_entity->active &&
688 Canvas_Entity_Contains(p_entity, world_pointer, zoom)) { 1411 Canvas_Entity_Contains(p_entity, world_pointer, zoom)) {
689 return index; 1412 return index;
690 } 1413 }
691 } 1414 }
692 1415
693 for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) { 1416 for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) {
1417 if (p_scene->p_entities[index].removing) continue;
694 if (Canvas_Entity_Contains(&p_scene->p_entities[index], world_pointer, zoom)) return index; 1418 if (Canvas_Entity_Contains(&p_scene->p_entities[index], world_pointer, zoom)) return index;
1419 }
1420 return -1;
1421 }
1422
1423 int32 Canvas_Scene_Topmost_Visual(
1424 const Canvas_Scene *p_scene,
1425 Vector2 world_pointer,
1426 float zoom)
1427 {
1428 for (int32 index =
1429 (int32)Dowa_Array_Length(p_scene->p_entities) - 1;
1430 index >= 0;
1431 index--) {
1432 const Canvas_Entity *p_entity = &p_scene->p_entities[index];
1433 if (p_entity->removing) continue;
1434 if (p_entity->type == CANVAS_ENTITY_WEB_CONTENT) {
1435 if (CheckCollisionPointRec(
1436 world_pointer,
1437 (Rectangle){
1438 p_entity->position.x,
1439 p_entity->position.y,
1440 p_entity->size.x,
1441 p_entity->size.y,
1442 })) {
1443 return index;
1444 }
1445 continue;
1446 }
1447 if (Canvas_Entity_Contains(p_entity, world_pointer, zoom)) {
1448 return index;
1449 }
695 } 1450 }
696 return -1; 1451 return -1;
697 } 1452 }
698 1453
699 static void Canvas_Scene_Activate(Canvas_Scene *p_scene, int32 index, Vector2 world_pointer) 1454 static void Canvas_Scene_Activate(Canvas_Scene *p_scene, int32 index, Vector2 world_pointer)
761 default: 1516 default:
762 break; 1517 break;
763 } 1518 }
764 } 1519 }
765 1520
766 static void Canvas_Scene_Update_Text_Input(Canvas_Scene *p_scene) 1521 static boolean Canvas_Shift_Is_Down(void)
1522 {
1523 return IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
1524 }
1525
1526 static boolean Canvas_Alt_Is_Down(void)
1527 {
1528 return IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT);
1529 }
1530
1531 static boolean Canvas_Super_Is_Down(void)
1532 {
1533 return IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER);
1534 }
1535
1536 static boolean Canvas_Key_Activated(int32 key)
1537 {
1538 return IsKeyPressed(key) || IsKeyPressedRepeat(key);
1539 }
1540
1541 static float Canvas_Text_Measure_Range(
1542 Font font,
1543 const char *p_text,
1544 int32 start,
1545 int32 end)
1546 {
1547 if (end <= start) return 0.0f;
1548 char buffer[CANVAS_ENTITY_TEXT_CAPACITY];
1549 int32 length = end - start;
1550 if (length >= (int32)sizeof(buffer)) length = (int32)sizeof(buffer) - 1;
1551 memcpy(buffer, p_text + start, (size_t)length);
1552 buffer[length] = '\0';
1553 return MeasureTextEx(
1554 font,
1555 buffer,
1556 CANVAS_TEXT_AREA_FONT_SIZE,
1557 CANVAS_TEXT_AREA_SPACING).x;
1558 }
1559
1560 static int32 Canvas_Text_Build_Lines(
1561 Font font,
1562 const char *p_text,
1563 float max_width,
1564 Canvas_Text_Line *p_lines,
1565 int32 line_capacity)
1566 {
1567 int32 length = (int32)strlen(p_text);
1568 int32 line_count = 0;
1569 int32 start = 0;
1570
1571 while (start <= length && line_count < line_capacity) {
1572 int32 index = start;
1573 int32 last_space = -1;
1574 float width = 0.0f;
1575 boolean ended_with_newline = FALSE;
1576
1577 while (index < length) {
1578 if (p_text[index] == '\n') {
1579 ended_with_newline = TRUE;
1580 break;
1581 }
1582 float character_width = Canvas_Text_Measure_Range(
1583 font,
1584 p_text,
1585 index,
1586 index + 1);
1587 if (width + character_width > max_width && index > start) {
1588 if (last_space >= start) index = last_space + 1;
1589 break;
1590 }
1591 width += character_width;
1592 if (p_text[index] == ' ' || p_text[index] == '\t') {
1593 last_space = index;
1594 }
1595 index++;
1596 }
1597
1598 int32 end = index;
1599 int32 next = ended_with_newline ? index + 1 : index;
1600 p_lines[line_count++] = (Canvas_Text_Line){
1601 .start = start,
1602 .end = end,
1603 .next = next,
1604 .width = Canvas_Text_Measure_Range(font, p_text, start, end),
1605 };
1606 if (index >= length) break;
1607 start = next;
1608 }
1609
1610 return line_count;
1611 }
1612
1613 static Rectangle Canvas_Text_Area_Content_Bounds(
1614 const Canvas_Entity *p_entity)
1615 {
1616 return (Rectangle){
1617 p_entity->position.x + CANVAS_TEXT_AREA_PADDING,
1618 p_entity->position.y + CANVAS_TEXT_AREA_PADDING,
1619 p_entity->size.x - CANVAS_TEXT_AREA_PADDING * 2.0f,
1620 p_entity->size.y - CANVAS_TEXT_AREA_PADDING * 2.0f,
1621 };
1622 }
1623
1624 static int32 Canvas_Text_Line_For_Cursor(
1625 const Canvas_Text_Line *p_lines,
1626 int32 line_count,
1627 int32 cursor)
1628 {
1629 for (int32 line = 0; line < line_count; line++) {
1630 if (cursor <= p_lines[line].end) return line;
1631 if (line + 1 < line_count && cursor < p_lines[line + 1].start) {
1632 return line;
1633 }
1634 }
1635 return line_count > 0 ? line_count - 1 : 0;
1636 }
1637
1638 static int32 Canvas_Text_Cursor_At_Point(
1639 Font font,
1640 const Canvas_Entity *p_entity,
1641 Vector2 point)
1642 {
1643 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
1644 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
1645 int32 line_count = Canvas_Text_Build_Lines(
1646 font,
1647 p_entity->text,
1648 content.width,
1649 lines,
1650 CANVAS_ENTITY_TEXT_CAPACITY);
1651 int32 line = (int32)floorf(
1652 (point.y - content.y + p_entity->text_scroll_y) /
1653 CANVAS_TEXT_AREA_LINE_HEIGHT);
1654 if (line < 0) line = 0;
1655 if (line >= line_count) line = line_count - 1;
1656 float local_x = point.x - content.x;
1657 if (local_x <= 0.0f) return lines[line].start;
1658
1659 for (int32 cursor = lines[line].start; cursor < lines[line].end; cursor++) {
1660 float left = Canvas_Text_Measure_Range(
1661 font,
1662 p_entity->text,
1663 lines[line].start,
1664 cursor);
1665 float right = Canvas_Text_Measure_Range(
1666 font,
1667 p_entity->text,
1668 lines[line].start,
1669 cursor + 1);
1670 if (local_x < (left + right) * 0.5f) return cursor;
1671 }
1672 return lines[line].end;
1673 }
1674
1675 static void Canvas_Text_Selection_Bounds(
1676 const Canvas_Entity *p_entity,
1677 int32 *p_start,
1678 int32 *p_end)
1679 {
1680 *p_start = p_entity->text_cursor;
1681 *p_end = p_entity->text_selection_anchor;
1682 if (*p_start > *p_end) {
1683 int32 swap = *p_start;
1684 *p_start = *p_end;
1685 *p_end = swap;
1686 }
1687 }
1688
1689 static boolean Canvas_Text_Has_Selection(const Canvas_Entity *p_entity)
1690 {
1691 return p_entity->text_cursor != p_entity->text_selection_anchor;
1692 }
1693
1694 static void Canvas_Text_Delete_Selection(Canvas_Entity *p_entity)
1695 {
1696 int32 start = 0;
1697 int32 end = 0;
1698 Canvas_Text_Selection_Bounds(p_entity, &start, &end);
1699 if (start == end) return;
1700 size_t length = strlen(p_entity->text);
1701 memmove(
1702 p_entity->text + start,
1703 p_entity->text + end,
1704 length - (size_t)end + 1);
1705 p_entity->text_cursor = start;
1706 p_entity->text_selection_anchor = start;
1707 }
1708
1709 static void Canvas_Text_Insert(
1710 Canvas_Entity *p_entity,
1711 const char *p_text)
1712 {
1713 Canvas_Text_Delete_Selection(p_entity);
1714 size_t length = strlen(p_entity->text);
1715 size_t available =
1716 sizeof(p_entity->text) - length - 1;
1717 if (available == 0) return;
1718
1719 char filtered[CANVAS_ENTITY_TEXT_CAPACITY];
1720 size_t filtered_length = 0;
1721 for (const char *p_cursor = p_text;
1722 *p_cursor && filtered_length < available;
1723 p_cursor++) {
1724 if (*p_cursor == '\r') continue;
1725 if (*p_cursor == '\n' || *p_cursor == '\t' ||
1726 (*p_cursor >= 32 && *p_cursor <= 126)) {
1727 filtered[filtered_length++] = *p_cursor == '\t' ? ' ' : *p_cursor;
1728 }
1729 }
1730 if (filtered_length == 0) return;
1731
1732 int32 cursor = p_entity->text_cursor;
1733 memmove(
1734 p_entity->text + cursor + filtered_length,
1735 p_entity->text + cursor,
1736 length - (size_t)cursor + 1);
1737 memcpy(p_entity->text + cursor, filtered, filtered_length);
1738 p_entity->text_cursor += (int32)filtered_length;
1739 p_entity->text_selection_anchor = p_entity->text_cursor;
1740 }
1741
1742 static int32 Canvas_Text_Previous_Word(const char *p_text, int32 cursor)
1743 {
1744 while (cursor > 0 && p_text[cursor - 1] == ' ') cursor--;
1745 while (cursor > 0 &&
1746 p_text[cursor - 1] != ' ' &&
1747 p_text[cursor - 1] != '\n') {
1748 cursor--;
1749 }
1750 return cursor;
1751 }
1752
1753 static int32 Canvas_Text_Next_Word(const char *p_text, int32 cursor)
1754 {
1755 int32 length = (int32)strlen(p_text);
1756 while (cursor < length &&
1757 p_text[cursor] != ' ' &&
1758 p_text[cursor] != '\n') {
1759 cursor++;
1760 }
1761 while (cursor < length && p_text[cursor] == ' ') cursor++;
1762 return cursor;
1763 }
1764
1765 static void Canvas_Text_Move_Cursor(
1766 Canvas_Entity *p_entity,
1767 int32 cursor,
1768 boolean extend_selection)
1769 {
1770 int32 length = (int32)strlen(p_entity->text);
1771 if (cursor < 0) cursor = 0;
1772 if (cursor > length) cursor = length;
1773 p_entity->text_cursor = cursor;
1774 if (!extend_selection) p_entity->text_selection_anchor = cursor;
1775 }
1776
1777 static void Canvas_Text_Ensure_Cursor_Visible(
1778 Canvas_Entity *p_entity,
1779 Font font)
1780 {
1781 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
1782 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
1783 int32 line_count = Canvas_Text_Build_Lines(
1784 font,
1785 p_entity->text,
1786 content.width,
1787 lines,
1788 CANVAS_ENTITY_TEXT_CAPACITY);
1789 int32 line = Canvas_Text_Line_For_Cursor(
1790 lines,
1791 line_count,
1792 p_entity->text_cursor);
1793 float cursor_top = (float)line * CANVAS_TEXT_AREA_LINE_HEIGHT;
1794 float cursor_bottom = cursor_top + CANVAS_TEXT_AREA_LINE_HEIGHT;
1795 if (cursor_top < p_entity->text_scroll_y) {
1796 p_entity->text_scroll_y = cursor_top;
1797 } else if (cursor_bottom >
1798 p_entity->text_scroll_y + content.height) {
1799 p_entity->text_scroll_y = cursor_bottom - content.height;
1800 }
1801 float max_scroll = fmaxf(
1802 0.0f,
1803 (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height);
1804 p_entity->text_scroll_y = Canvas_Clamp(
1805 p_entity->text_scroll_y,
1806 0.0f,
1807 max_scroll);
1808 }
1809
1810 static void Canvas_Text_Update_Keyboard(Canvas_Entity *p_entity, Font font)
1811 {
1812 boolean shortcut = Canvas_Control_Is_Down();
1813 boolean control = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
1814 boolean super = Canvas_Super_Is_Down();
1815 boolean word_modifier = control || Canvas_Alt_Is_Down();
1816 boolean extend = Canvas_Shift_Is_Down();
1817 int32 length = (int32)strlen(p_entity->text);
1818 int32 original_cursor = p_entity->text_cursor;
1819 int32 original_length = length;
1820
1821 if (shortcut && IsKeyPressed(KEY_A)) {
1822 p_entity->text_selection_anchor = 0;
1823 p_entity->text_cursor = length;
1824 }
1825 if (shortcut && IsKeyPressed(KEY_C) && Canvas_Text_Has_Selection(p_entity)) {
1826 int32 start = 0;
1827 int32 end = 0;
1828 char selected[CANVAS_ENTITY_TEXT_CAPACITY];
1829 Canvas_Text_Selection_Bounds(p_entity, &start, &end);
1830 memcpy(selected, p_entity->text + start, (size_t)(end - start));
1831 selected[end - start] = '\0';
1832 SetClipboardText(selected);
1833 }
1834 if (shortcut && IsKeyPressed(KEY_X) && Canvas_Text_Has_Selection(p_entity)) {
1835 int32 start = 0;
1836 int32 end = 0;
1837 char selected[CANVAS_ENTITY_TEXT_CAPACITY];
1838 Canvas_Text_Selection_Bounds(p_entity, &start, &end);
1839 memcpy(selected, p_entity->text + start, (size_t)(end - start));
1840 selected[end - start] = '\0';
1841 SetClipboardText(selected);
1842 Canvas_Text_Delete_Selection(p_entity);
1843 }
1844 if (shortcut && IsKeyPressed(KEY_V)) {
1845 const char *p_clipboard = GetClipboardText();
1846 if (p_clipboard) Canvas_Text_Insert(p_entity, p_clipboard);
1847 }
1848
1849 if (Canvas_Key_Activated(KEY_LEFT)) {
1850 int32 cursor = p_entity->text_cursor;
1851 if (!extend && Canvas_Text_Has_Selection(p_entity)) {
1852 int32 end = 0;
1853 Canvas_Text_Selection_Bounds(p_entity, &cursor, &end);
1854 } else if (super) {
1855 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
1856 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
1857 int32 count = Canvas_Text_Build_Lines(
1858 font,
1859 p_entity->text,
1860 content.width,
1861 lines,
1862 CANVAS_ENTITY_TEXT_CAPACITY);
1863 cursor = lines[Canvas_Text_Line_For_Cursor(
1864 lines,
1865 count,
1866 cursor)].start;
1867 } else {
1868 cursor = word_modifier ?
1869 Canvas_Text_Previous_Word(p_entity->text, cursor) :
1870 cursor - 1;
1871 }
1872 Canvas_Text_Move_Cursor(p_entity, cursor, extend);
1873 }
1874 if (Canvas_Key_Activated(KEY_RIGHT)) {
1875 int32 cursor = p_entity->text_cursor;
1876 if (!extend && Canvas_Text_Has_Selection(p_entity)) {
1877 int32 start = 0;
1878 Canvas_Text_Selection_Bounds(p_entity, &start, &cursor);
1879 } else if (super) {
1880 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
1881 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
1882 int32 count = Canvas_Text_Build_Lines(
1883 font,
1884 p_entity->text,
1885 content.width,
1886 lines,
1887 CANVAS_ENTITY_TEXT_CAPACITY);
1888 cursor = lines[Canvas_Text_Line_For_Cursor(
1889 lines,
1890 count,
1891 cursor)].end;
1892 } else {
1893 cursor = word_modifier ?
1894 Canvas_Text_Next_Word(p_entity->text, cursor) :
1895 cursor + 1;
1896 }
1897 Canvas_Text_Move_Cursor(p_entity, cursor, extend);
1898 }
1899
1900 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
1901 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
1902 int32 line_count = Canvas_Text_Build_Lines(
1903 font,
1904 p_entity->text,
1905 content.width,
1906 lines,
1907 CANVAS_ENTITY_TEXT_CAPACITY);
1908 int32 current_line = Canvas_Text_Line_For_Cursor(
1909 lines,
1910 line_count,
1911 p_entity->text_cursor);
1912 float cursor_x = Canvas_Text_Measure_Range(
1913 font,
1914 p_entity->text,
1915 lines[current_line].start,
1916 p_entity->text_cursor);
1917 if (Canvas_Key_Activated(KEY_UP) || Canvas_Key_Activated(KEY_DOWN)) {
1918 int32 target_line = current_line +
1919 (IsKeyDown(KEY_UP) ? -1 : 1);
1920 if (target_line < 0) target_line = 0;
1921 if (target_line >= line_count) target_line = line_count - 1;
1922 Vector2 target = {
1923 content.x + cursor_x,
1924 content.y +
1925 (float)target_line * CANVAS_TEXT_AREA_LINE_HEIGHT -
1926 p_entity->text_scroll_y +
1927 CANVAS_TEXT_AREA_LINE_HEIGHT * 0.5f,
1928 };
1929 Canvas_Text_Move_Cursor(
1930 p_entity,
1931 Canvas_Text_Cursor_At_Point(font, p_entity, target),
1932 extend);
1933 }
1934 if (Canvas_Key_Activated(KEY_HOME)) {
1935 Canvas_Text_Move_Cursor(
1936 p_entity,
1937 lines[current_line].start,
1938 extend);
1939 }
1940 if (Canvas_Key_Activated(KEY_END)) {
1941 Canvas_Text_Move_Cursor(
1942 p_entity,
1943 lines[current_line].end,
1944 extend);
1945 }
1946
1947 if (Canvas_Key_Activated(KEY_BACKSPACE)) {
1948 if (Canvas_Text_Has_Selection(p_entity)) {
1949 Canvas_Text_Delete_Selection(p_entity);
1950 } else if (p_entity->text_cursor > 0) {
1951 int32 start = p_entity->text_cursor - 1;
1952 if (super) {
1953 start = lines[Canvas_Text_Line_For_Cursor(
1954 lines,
1955 line_count,
1956 p_entity->text_cursor)].start;
1957 } else if (word_modifier) {
1958 start = Canvas_Text_Previous_Word(
1959 p_entity->text,
1960 p_entity->text_cursor);
1961 }
1962 p_entity->text_selection_anchor = start;
1963 Canvas_Text_Delete_Selection(p_entity);
1964 }
1965 }
1966 if (Canvas_Key_Activated(KEY_DELETE)) {
1967 if (Canvas_Text_Has_Selection(p_entity)) {
1968 Canvas_Text_Delete_Selection(p_entity);
1969 } else if (p_entity->text_cursor < length) {
1970 int32 end = p_entity->text_cursor + 1;
1971 if (super) {
1972 end = lines[Canvas_Text_Line_For_Cursor(
1973 lines,
1974 line_count,
1975 p_entity->text_cursor)].end;
1976 } else if (word_modifier) {
1977 end = Canvas_Text_Next_Word(
1978 p_entity->text,
1979 p_entity->text_cursor);
1980 }
1981 p_entity->text_selection_anchor = end;
1982 Canvas_Text_Delete_Selection(p_entity);
1983 }
1984 }
1985 if (IsKeyPressed(KEY_ENTER)) Canvas_Text_Insert(p_entity, "\n");
1986
1987 int32 codepoint = GetCharPressed();
1988 while (codepoint > 0) {
1989 if (!shortcut && codepoint >= 32 && codepoint <= 126) {
1990 char inserted[2] = {(char)codepoint, '\0'};
1991 Canvas_Text_Insert(p_entity, inserted);
1992 }
1993 codepoint = GetCharPressed();
1994 }
1995 if (p_entity->text_cursor != original_cursor ||
1996 (int32)strlen(p_entity->text) != original_length) {
1997 Canvas_Text_Ensure_Cursor_Visible(p_entity, font);
1998 }
1999 }
2000
2001 static void Canvas_Scene_Update_Text_Input(
2002 Canvas_Scene *p_scene,
2003 Font font)
767 { 2004 {
768 if (p_scene->selected_index < 0 || 2005 if (p_scene->selected_index < 0 ||
769 p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return; 2006 p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return;
770 2007
771 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; 2008 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
772 if (p_entity->type != CANVAS_ENTITY_TEXT_AREA || !p_entity->active) return; 2009 if ((p_entity->type != CANVAS_ENTITY_TEXT_AREA &&
2010 p_entity->type != CANVAS_ENTITY_LUCIDE_GALLERY) ||
2011 !p_entity->active) {
2012 return;
2013 }
2014
2015 if (p_entity->type == CANVAS_ENTITY_TEXT_AREA) {
2016 Canvas_Text_Update_Keyboard(p_entity, font);
2017 return;
2018 }
773 2019
774 size_t length = strlen(p_entity->text); 2020 size_t length = strlen(p_entity->text);
2021 boolean changed = FALSE;
775 int32 codepoint = GetCharPressed(); 2022 int32 codepoint = GetCharPressed();
776 while (codepoint > 0) { 2023 while (codepoint > 0) {
777 if (codepoint >= 32 && codepoint <= 126 && length + 1 < sizeof(p_entity->text)) { 2024 size_t input_capacity = p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY ?
778 p_entity->text[length++] = (char)codepoint; 2025 64 :
2026 sizeof(p_entity->text);
2027 if (codepoint >= 32 &&
2028 codepoint <= 126 &&
2029 length + 1 < input_capacity) {
2030 p_entity->text[length++] = p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY ?
2031 Canvas_Ascii_Lower((char)codepoint) :
2032 (char)codepoint;
779 p_entity->text[length] = '\0'; 2033 p_entity->text[length] = '\0';
2034 changed = TRUE;
780 } 2035 }
781 codepoint = GetCharPressed(); 2036 codepoint = GetCharPressed();
782 } 2037 }
783 2038
784 if (IsKeyPressed(KEY_ENTER) && length + 1 < sizeof(p_entity->text)) { 2039 if (p_entity->type == CANVAS_ENTITY_TEXT_AREA &&
2040 IsKeyPressed(KEY_ENTER) &&
2041 length + 1 < sizeof(p_entity->text)) {
785 p_entity->text[length++] = '\n'; 2042 p_entity->text[length++] = '\n';
786 p_entity->text[length] = '\0'; 2043 p_entity->text[length] = '\0';
787 } 2044 }
788 if (IsKeyPressed(KEY_BACKSPACE) && length > 0) { 2045 if (IsKeyPressed(KEY_BACKSPACE) && length > 0) {
789 p_entity->text[length - 1] = '\0'; 2046 p_entity->text[length - 1] = '\0';
2047 changed = TRUE;
2048 }
2049 if (changed && p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) {
2050 p_entity->value = 0;
790 } 2051 }
791 } 2052 }
792 2053
793 static void Canvas_Entity_Capture_Pin( 2054 static void Canvas_Entity_Capture_Pin(
794 Canvas_Entity *p_entity, 2055 Canvas_Entity *p_entity,
798 Canvas_Camera_World_To_Screen(p_camera, p_entity->position); 2059 Canvas_Camera_World_To_Screen(p_camera, p_entity->position);
799 p_entity->pinned_screen_size = (Vector2){ 2060 p_entity->pinned_screen_size = (Vector2){
800 p_entity->size.x * p_camera->zoom, 2061 p_entity->size.x * p_camera->zoom,
801 p_entity->size.y * p_camera->zoom, 2062 p_entity->size.y * p_camera->zoom,
802 }; 2063 };
2064 }
2065
2066 static void Canvas_Scene_Adjust_Index_After_Removal(
2067 int32 *p_index,
2068 int32 removed_index)
2069 {
2070 if (*p_index == removed_index) {
2071 *p_index = -1;
2072 } else if (*p_index > removed_index) {
2073 (*p_index)--;
2074 }
2075 }
2076
2077 static void Canvas_Scene_Update_Lifecycle(Canvas_Scene *p_scene)
2078 {
2079 float follow = 1.0f - expf(-GetFrameTime() * 9.0f);
2080 size_t index = 0;
2081 while (index < Dowa_Array_Length(p_scene->p_entities)) {
2082 Canvas_Entity *p_entity = &p_scene->p_entities[index];
2083 float target = p_entity->removing ? 0.0f : 1.0f;
2084 p_entity->visibility_amount +=
2085 (target - p_entity->visibility_amount) * follow;
2086 p_entity->visibility_amount = Canvas_Clamp(
2087 p_entity->visibility_amount,
2088 0.0f,
2089 1.0f);
2090 float chrome_target =
2091 p_entity->type == CANVAS_ENTITY_WEB_CONTENT &&
2092 (int32)index == p_scene->selected_index &&
2093 !p_entity->removing ?
2094 1.0f :
2095 0.0f;
2096 float chrome_follow = 1.0f - expf(-GetFrameTime() * 12.0f);
2097 p_entity->web_chrome_amount +=
2098 (chrome_target - p_entity->web_chrome_amount) * chrome_follow;
2099 p_entity->web_chrome_amount = Canvas_Clamp(
2100 p_entity->web_chrome_amount,
2101 0.0f,
2102 1.0f);
2103
2104 if (p_entity->removing && p_entity->visibility_amount <= 0.01f) {
2105 int32 removed_index = (int32)index;
2106 size_t length = Dowa_Array_Length(p_scene->p_entities);
2107 for (size_t move = index + 1; move < length; move++) {
2108 p_scene->p_entities[move - 1] = p_scene->p_entities[move];
2109 }
2110 (void)Dowa_Array_Pop(p_scene->p_entities);
2111 Canvas_Scene_Adjust_Index_After_Removal(
2112 &p_scene->selected_index,
2113 removed_index);
2114 Canvas_Scene_Adjust_Index_After_Removal(
2115 &p_scene->pressed_index,
2116 removed_index);
2117 Canvas_Scene_Adjust_Index_After_Removal(
2118 &p_scene->hover_index,
2119 removed_index);
2120 continue;
2121 }
2122 index++;
2123 }
803 } 2124 }
804 2125
805 static void Canvas_Scene_Update_Animations( 2126 static void Canvas_Scene_Update_Animations(
806 Canvas_Scene *p_scene, 2127 Canvas_Scene *p_scene,
807 const Canvas_Camera *p_camera) 2128 const Canvas_Camera *p_camera)
841 } 2162 }
842 2163
843 boolean Canvas_Scene_Update_Interaction( 2164 boolean Canvas_Scene_Update_Interaction(
844 Canvas_Scene *p_scene, 2165 Canvas_Scene *p_scene,
845 const Canvas_Camera *p_camera, 2166 const Canvas_Camera *p_camera,
2167 Font font,
846 boolean block_pointer_input) 2168 boolean block_pointer_input)
847 { 2169 {
848 Canvas_Scene_Update_Text_Input(p_scene); 2170 Canvas_Scene_Update_Lifecycle(p_scene);
2171 Canvas_Scene_Update_Text_Input(p_scene, font);
849 Canvas_Scene_Update_Animations(p_scene, p_camera); 2172 Canvas_Scene_Update_Animations(p_scene, p_camera);
850 2173
851 if (block_pointer_input && p_scene->pressed_index < 0) { 2174 if (block_pointer_input && p_scene->pressed_index < 0) {
852 return FALSE; 2175 return FALSE;
853 } 2176 }
857 -1 : 2180 -1 :
858 Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom); 2181 Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom);
859 boolean consumed_scroll = FALSE; 2182 boolean consumed_scroll = FALSE;
860 if (p_scene->hover_index >= 0) { 2183 if (p_scene->hover_index >= 0) {
861 Canvas_Entity *p_hovered = &p_scene->p_entities[p_scene->hover_index]; 2184 Canvas_Entity *p_hovered = &p_scene->p_entities[p_scene->hover_index];
862 if (p_hovered->type == CANVAS_ENTITY_SCROLL_AREA) { 2185 boolean control_down = Canvas_Control_Is_Down();
2186 if (p_hovered->type == CANVAS_ENTITY_TEXT_AREA &&
2187 p_hovered->active &&
2188 !control_down) {
2189 float wheel = GetMouseWheelMove();
2190 if (wheel != 0.0f) {
2191 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
2192 Rectangle content = Canvas_Text_Area_Content_Bounds(p_hovered);
2193 int32 line_count = Canvas_Text_Build_Lines(
2194 font,
2195 p_hovered->text,
2196 content.width,
2197 lines,
2198 CANVAS_ENTITY_TEXT_CAPACITY);
2199 float max_scroll = fmaxf(
2200 0.0f,
2201 (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT -
2202 content.height);
2203 p_hovered->text_scroll_y = Canvas_Clamp(
2204 p_hovered->text_scroll_y -
2205 wheel * CANVAS_TEXT_AREA_LINE_HEIGHT * 2.0f,
2206 0.0f,
2207 max_scroll);
2208 consumed_scroll = TRUE;
2209 }
2210 } else if (p_hovered->type == CANVAS_ENTITY_SCROLL_AREA &&
2211 !control_down) {
863 float wheel = GetMouseWheelMove(); 2212 float wheel = GetMouseWheelMove();
864 if (wheel != 0.0f) { 2213 if (wheel != 0.0f) {
865 p_hovered->value = (int32)Canvas_Clamp( 2214 p_hovered->value = (int32)Canvas_Clamp(
866 (float)p_hovered->value - wheel * 28.0f, 2215 (float)p_hovered->value - wheel * 28.0f,
867 0.0f, 2216 0.0f,
868 240.0f); 2217 240.0f);
2218 consumed_scroll = TRUE;
2219 }
2220 } else if (p_hovered->type == CANVAS_ENTITY_LUCIDE_GALLERY &&
2221 !control_down) {
2222 float wheel = GetMouseWheelMove();
2223 if (wheel != 0.0f) {
2224 p_hovered->value = (int32)Canvas_Clamp(
2225 (float)p_hovered->value - wheel * 72.0f,
2226 0.0f,
2227 Canvas_Lucide_Max_Scroll(p_hovered));
869 consumed_scroll = TRUE; 2228 consumed_scroll = TRUE;
870 } 2229 }
871 } 2230 }
872 } 2231 }
873 2232
887 } 2246 }
888 if ((int32)index != p_scene->selected_index && 2247 if ((int32)index != p_scene->selected_index &&
889 p_scene->p_entities[index].type == CANVAS_ENTITY_DROPDOWN) { 2248 p_scene->p_entities[index].type == CANVAS_ENTITY_DROPDOWN) {
890 p_scene->p_entities[index].active = FALSE; 2249 p_scene->p_entities[index].active = FALSE;
891 } 2250 }
2251 if ((int32)index != p_scene->selected_index &&
2252 p_scene->p_entities[index].type == CANVAS_ENTITY_LUCIDE_GALLERY) {
2253 p_scene->p_entities[index].active = FALSE;
2254 }
892 } 2255 }
893 2256
894 if (p_scene->selected_index >= 0) { 2257 if (p_scene->selected_index >= 0) {
895 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; 2258 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
2259 if (p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) {
2260 p_entity->active = CheckCollisionPointRec(
2261 world_pointer,
2262 Canvas_Lucide_Search_Bounds(p_entity)) ? TRUE : FALSE;
2263 }
2264 p_scene->selecting_text = FALSE;
2265 if (p_entity->type == CANVAS_ENTITY_TEXT_AREA) {
2266 boolean inside_content = CheckCollisionPointRec(
2267 world_pointer,
2268 Canvas_Text_Area_Content_Bounds(p_entity)) ? TRUE : FALSE;
2269 p_entity->active = inside_content;
2270 if (inside_content) {
2271 int32 cursor = Canvas_Text_Cursor_At_Point(
2272 font,
2273 p_entity,
2274 world_pointer);
2275 p_entity->text_cursor = cursor;
2276 if (!Canvas_Shift_Is_Down()) {
2277 p_entity->text_selection_anchor = cursor;
2278 }
2279 p_scene->selecting_text = TRUE;
2280 }
2281 }
896 p_scene->pressed_index = p_scene->selected_index; 2282 p_scene->pressed_index = p_scene->selected_index;
897 p_scene->dragging = FALSE; 2283 p_scene->dragging = FALSE;
898 p_scene->resizing = Canvas_Web_Resize_Handle_Contains( 2284 p_scene->resizing = Canvas_Web_Resize_Handle_Contains(
899 p_entity, 2285 p_entity,
900 world_pointer, 2286 world_pointer,
911 world_pointer.y - p_scene->pointer_start.y, 2297 world_pointer.y - p_scene->pointer_start.y,
912 }; 2298 };
913 float screen_distance_squared = 2299 float screen_distance_squared =
914 (delta.x * p_camera->zoom) * (delta.x * p_camera->zoom) + 2300 (delta.x * p_camera->zoom) * (delta.x * p_camera->zoom) +
915 (delta.y * p_camera->zoom) * (delta.y * p_camera->zoom); 2301 (delta.y * p_camera->zoom) * (delta.y * p_camera->zoom);
916 if (screen_distance_squared >= 16.0f) p_scene->dragging = TRUE; 2302 Canvas_Entity *p_pressed =
2303 &p_scene->p_entities[p_scene->pressed_index];
2304 if (p_scene->selecting_text &&
2305 p_pressed->type == CANVAS_ENTITY_TEXT_AREA) {
2306 p_pressed->text_cursor = Canvas_Text_Cursor_At_Point(
2307 font,
2308 p_pressed,
2309 world_pointer);
2310 Canvas_Text_Ensure_Cursor_Visible(p_pressed, font);
2311 }
2312 if (screen_distance_squared >= 16.0f &&
2313 !p_scene->selecting_text &&
2314 !(p_pressed->type == CANVAS_ENTITY_LUCIDE_GALLERY &&
2315 p_pressed->active)) {
2316 p_scene->dragging = TRUE;
2317 }
917 2318
918 if (p_scene->dragging) { 2319 if (p_scene->dragging) {
919 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index]; 2320 Canvas_Entity *p_entity = p_pressed;
920 if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) { 2321 if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) {
921 p_scene->dragging = FALSE; 2322 p_scene->dragging = FALSE;
922 } else if (p_scene->resizing) { 2323 } else if (p_scene->resizing) {
923 p_entity->size = (Vector2){ 2324 p_entity->size = (Vector2){
924 fmaxf(CANVAS_WEB_MIN_WIDTH, p_scene->entity_size_start.x + delta.x), 2325 fmaxf(CANVAS_WEB_MIN_WIDTH, p_scene->entity_size_start.x + delta.x),
934 } 2335 }
935 } 2336 }
936 2337
937 if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { 2338 if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
938 if (p_scene->pressed_index >= 0 && !p_scene->dragging) { 2339 if (p_scene->pressed_index >= 0 && !p_scene->dragging) {
939 Canvas_Scene_Activate(p_scene, p_scene->pressed_index, world_pointer);
940 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index]; 2340 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index];
2341 if (p_entity->type != CANVAS_ENTITY_TEXT_AREA) {
2342 Canvas_Scene_Activate(
2343 p_scene,
2344 p_scene->pressed_index,
2345 world_pointer);
2346 }
941 if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera); 2347 if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera);
942 } 2348 }
943 p_scene->pressed_index = -1; 2349 p_scene->pressed_index = -1;
944 p_scene->dragging = FALSE; 2350 p_scene->dragging = FALSE;
945 p_scene->resizing = FALSE; 2351 p_scene->resizing = FALSE;
2352 p_scene->selecting_text = FALSE;
946 } 2353 }
947 return p_scene->pressed_index >= 0 || consumed_scroll; 2354 return p_scene->pressed_index >= 0 || consumed_scroll;
948 } 2355 }
949 2356
950 boolean Canvas_Scene_Is_Text_Editing(const Canvas_Scene *p_scene) 2357 boolean Canvas_Scene_Is_Text_Editing(const Canvas_Scene *p_scene)
953 p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) { 2360 p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) {
954 return FALSE; 2361 return FALSE;
955 } 2362 }
956 2363
957 const Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; 2364 const Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
958 return (p_entity->type == CANVAS_ENTITY_TEXT_AREA && p_entity->active) ? TRUE : FALSE; 2365 return ((p_entity->type == CANVAS_ENTITY_TEXT_AREA ||
2366 p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) &&
2367 p_entity->active) ? TRUE : FALSE;
959 } 2368 }
960 2369
961 void Canvas_Scene_Toggle_Selected_Pin( 2370 void Canvas_Scene_Toggle_Selected_Pin(
962 Canvas_Scene *p_scene, 2371 Canvas_Scene *p_scene,
963 const Canvas_Camera *p_camera) 2372 const Canvas_Camera *p_camera)
1159 } 2568 }
1160 2569
1161 if (!block_pointer_input) { 2570 if (!block_pointer_input) {
1162 float wheel = GetMouseWheelMove(); 2571 float wheel = GetMouseWheelMove();
1163 if (wheel != 0.0f) { 2572 if (wheel != 0.0f) {
1164 Canvas_Camera_Zoom_At( 2573 if (Canvas_Control_Is_Down()) {
1165 p_camera, 2574 Canvas_Camera_Zoom_At(
1166 GetMousePosition(), 2575 p_camera,
1167 powf(1.15f, wheel)); 2576 GetMousePosition(),
2577 powf(1.15f, wheel));
2578 } else {
2579 Canvas_Camera_Pan(
2580 p_camera,
2581 (Vector2){0.0f, wheel * 72.0f});
2582 }
1168 } 2583 }
1169 } 2584 }
1170 2585
1171 if (!block_keyboard_input) { 2586 if (!block_keyboard_input) {
1172 Vector2 keyboard_delta = {0.0f, 0.0f}; 2587 Vector2 keyboard_delta = {0.0f, 0.0f};
1187 float spacing = Canvas_Grid_Spacing(p_camera->zoom); 2602 float spacing = Canvas_Grid_Spacing(p_camera->zoom);
1188 int32 first_x = (int32)floorf(bounds.x / spacing); 2603 int32 first_x = (int32)floorf(bounds.x / spacing);
1189 int32 last_x = (int32)ceilf((bounds.x + bounds.width) / spacing); 2604 int32 last_x = (int32)ceilf((bounds.x + bounds.width) / spacing);
1190 int32 first_y = (int32)floorf(bounds.y / spacing); 2605 int32 first_y = (int32)floorf(bounds.y / spacing);
1191 int32 last_y = (int32)ceilf((bounds.y + bounds.height) / spacing); 2606 int32 last_y = (int32)ceilf((bounds.y + bounds.height) / spacing);
1192 Color minor = Fade(p_theme->border, 0.36f); 2607 Color minor = Canvas_Color_Fade(p_theme->border, 0.36f);
1193 Color major = Fade(p_theme->border, 0.65f); 2608 Color major = Canvas_Color_Fade(p_theme->border, 0.65f);
1194 Color axis = p_theme->text_muted; 2609 Color axis = p_theme->text_muted;
1195 2610
1196 for (int32 index = first_x; index <= last_x; index++) { 2611 for (int32 index = first_x; index <= last_x; index++) {
1197 float x = (float)index * spacing; 2612 float x = (float)index * spacing;
1198 Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor); 2613 Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor);
1212 } 2627 }
1213 } 2628 }
1214 2629
1215 static void Canvas_Draw_Selection( 2630 static void Canvas_Draw_Selection(
1216 const Canvas_Entity *p_entity, 2631 const Canvas_Entity *p_entity,
1217 float zoom) 2632 float zoom,
2633 Color selection)
1218 { 2634 {
1219 float line = 2.0f / zoom; 2635 float line = 2.0f / zoom;
1220 float padding = 7.0f / zoom; 2636 float padding = 7.0f / zoom;
1221 Color selection = {0, 122, 255, 230};
1222 2637
1223 switch (p_entity->type) { 2638 switch (p_entity->type) {
1224 case CANVAS_ENTITY_RECTANGLE: 2639 case CANVAS_ENTITY_RECTANGLE:
1225 DrawRectangleRoundedLinesEx( 2640 DrawRectangleRoundedLinesEx(
1226 (Rectangle){ 2641 (Rectangle){
1246 (Vector2){ 2661 (Vector2){
1247 p_entity->position.x + p_entity->size.x, 2662 p_entity->position.x + p_entity->size.x,
1248 p_entity->position.y + p_entity->size.y, 2663 p_entity->position.y + p_entity->size.y,
1249 }, 2664 },
1250 10.0f / zoom, 2665 10.0f / zoom,
1251 Fade(selection, 0.35f)); 2666 Canvas_Color_Fade(selection, 0.35f));
1252 break; 2667 break;
1253 case CANVAS_ENTITY_TEXT: 2668 case CANVAS_ENTITY_TEXT:
1254 case CANVAS_ENTITY_BUTTON: 2669 case CANVAS_ENTITY_BUTTON:
1255 case CANVAS_ENTITY_TEXT_AREA: 2670 case CANVAS_ENTITY_TEXT_AREA:
1256 case CANVAS_ENTITY_DROPDOWN: 2671 case CANVAS_ENTITY_DROPDOWN:
1273 10, 2688 10,
1274 line, 2689 line,
1275 selection); 2690 selection);
1276 break; 2691 break;
1277 } 2692 }
2693 case CANVAS_ENTITY_LUCIDE_GALLERY: {
2694 DrawRectangleRoundedLinesEx(
2695 (Rectangle){
2696 p_entity->position.x - padding,
2697 p_entity->position.y - padding,
2698 p_entity->size.x + padding * 2.0f,
2699 p_entity->size.y + padding * 2.0f,
2700 },
2701 0.04f,
2702 10,
2703 line,
2704 selection);
2705 break;
2706 }
1278 case CANVAS_ENTITY_WEB_CONTENT: 2707 case CANVAS_ENTITY_WEB_CONTENT:
1279 DrawRectangleRoundedLinesEx( 2708 DrawRectangleRoundedLinesEx(
1280 (Rectangle){ 2709 (Rectangle){
1281 p_entity->position.x - padding, 2710 p_entity->position.x - padding,
1282 p_entity->position.y - padding, 2711 p_entity->position.y - padding,
1294 p_entity->position.y + p_entity->size.y - 2723 p_entity->position.y + p_entity->size.y -
1295 CANVAS_WEB_RESIZE_HANDLE / zoom, 2724 CANVAS_WEB_RESIZE_HANDLE / zoom,
1296 CANVAS_WEB_RESIZE_HANDLE / zoom, 2725 CANVAS_WEB_RESIZE_HANDLE / zoom,
1297 CANVAS_WEB_RESIZE_HANDLE / zoom, 2726 CANVAS_WEB_RESIZE_HANDLE / zoom,
1298 }, 2727 },
1299 Fade(selection, 0.18f)); 2728 Canvas_Color_Fade(selection, 0.18f));
1300 DrawLineEx( 2729 DrawLineEx(
1301 (Vector2){ 2730 (Vector2){
1302 p_entity->position.x + p_entity->size.x - 11.0f / zoom, 2731 p_entity->position.x + p_entity->size.x - 11.0f / zoom,
1303 p_entity->position.y + p_entity->size.y - 4.0f / zoom, 2732 p_entity->position.y + p_entity->size.y - 4.0f / zoom,
1304 }, 2733 },
1313 break; 2742 break;
1314 } 2743 }
1315 2744
1316 } 2745 }
1317 2746
2747 static void Canvas_Draw_Text_Area_Content(
2748 const Canvas_Entity *p_entity,
2749 Font font,
2750 const Canvas_Theme *p_theme)
2751 {
2752 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
2753 if (!p_entity->text[0]) {
2754 DrawTextEx(
2755 font,
2756 "Write something...",
2757 (Vector2){content.x, content.y},
2758 CANVAS_TEXT_AREA_FONT_SIZE,
2759 CANVAS_TEXT_AREA_SPACING,
2760 p_theme->text_muted);
2761 if (p_entity->active && fmod(GetTime(), 1.0) < 0.55) {
2762 DrawLineEx(
2763 (Vector2){content.x, content.y},
2764 (Vector2){
2765 content.x,
2766 content.y + CANVAS_TEXT_AREA_FONT_SIZE + 2.0f,
2767 },
2768 1.5f,
2769 p_theme->accent);
2770 }
2771 return;
2772 }
2773
2774 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
2775 int32 line_count = Canvas_Text_Build_Lines(
2776 font,
2777 p_entity->text,
2778 content.width,
2779 lines,
2780 CANVAS_ENTITY_TEXT_CAPACITY);
2781 int32 selection_start = 0;
2782 int32 selection_end = 0;
2783 Canvas_Text_Selection_Bounds(
2784 p_entity,
2785 &selection_start,
2786 &selection_end);
2787 int32 cursor_line = Canvas_Text_Line_For_Cursor(
2788 lines,
2789 line_count,
2790 p_entity->text_cursor);
2791
2792 for (int32 line = 0; line < line_count; line++) {
2793 float y = content.y +
2794 (float)line * CANVAS_TEXT_AREA_LINE_HEIGHT -
2795 p_entity->text_scroll_y;
2796 if (y < content.y ||
2797 y + CANVAS_TEXT_AREA_FONT_SIZE > content.y + content.height) {
2798 continue;
2799 }
2800
2801 int32 line_selection_start =
2802 selection_start > lines[line].start ?
2803 selection_start :
2804 lines[line].start;
2805 int32 line_selection_end =
2806 selection_end < lines[line].end ?
2807 selection_end :
2808 lines[line].end;
2809 if (line_selection_end > line_selection_start) {
2810 float selection_x = content.x + Canvas_Text_Measure_Range(
2811 font,
2812 p_entity->text,
2813 lines[line].start,
2814 line_selection_start);
2815 float selection_width = Canvas_Text_Measure_Range(
2816 font,
2817 p_entity->text,
2818 line_selection_start,
2819 line_selection_end);
2820 DrawRectangleRec(
2821 (Rectangle){
2822 selection_x,
2823 y - 1.0f,
2824 fmaxf(selection_width, 2.0f),
2825 CANVAS_TEXT_AREA_LINE_HEIGHT,
2826 },
2827 p_theme->accent_soft);
2828 }
2829
2830 char line_text[CANVAS_ENTITY_TEXT_CAPACITY];
2831 int32 line_length = lines[line].end - lines[line].start;
2832 memcpy(
2833 line_text,
2834 p_entity->text + lines[line].start,
2835 (size_t)line_length);
2836 line_text[line_length] = '\0';
2837 DrawTextEx(
2838 font,
2839 line_text,
2840 (Vector2){content.x, y},
2841 CANVAS_TEXT_AREA_FONT_SIZE,
2842 CANVAS_TEXT_AREA_SPACING,
2843 p_theme->text);
2844
2845 if (p_entity->active &&
2846 line == cursor_line &&
2847 fmod(GetTime(), 1.0) < 0.55) {
2848 float cursor_x = content.x + Canvas_Text_Measure_Range(
2849 font,
2850 p_entity->text,
2851 lines[line].start,
2852 p_entity->text_cursor);
2853 DrawLineEx(
2854 (Vector2){cursor_x, y},
2855 (Vector2){
2856 cursor_x,
2857 y + CANVAS_TEXT_AREA_FONT_SIZE + 2.0f,
2858 },
2859 1.5f,
2860 p_theme->accent);
2861 }
2862 }
2863 }
2864
1318 static void Canvas_Draw_Entity( 2865 static void Canvas_Draw_Entity(
1319 const Canvas_Entity *p_entity, 2866 const Canvas_Entity *p_entity,
1320 Font font, 2867 Font font,
1321 float zoom, 2868 float zoom,
1322 boolean selected, 2869 boolean selected,
1323 boolean pressed, 2870 boolean pressed,
1324 const Canvas_Theme *p_theme) 2871 const Canvas_Theme *p_theme)
1325 { 2872 {
2873 float opacity = Canvas_Entity_Visibility(p_entity);
2874 if (opacity <= 0.001f) return;
2875 Canvas_Entity faded_entity = *p_entity;
2876 faded_entity.color = Canvas_Color_With_Opacity(
2877 faded_entity.color,
2878 opacity);
2879 Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity);
2880 p_entity = &faded_entity;
2881 p_theme = &faded_theme;
2882
1326 float shadow_offset = 6.0f / zoom; 2883 float shadow_offset = 6.0f / zoom;
1327 Color shadow = p_theme->shadow; 2884 Color shadow = p_theme->shadow;
1328 static const char *calendar_weekdays[] = { 2885 static const char *calendar_weekdays[] = {
1329 "S", "M", "T", "W", "T", "F", "S", 2886 "S", "M", "T", "W", "T", "F", "S",
1330 }; 2887 };
1350 p_entity->size.x, 2907 p_entity->size.x,
1351 p_entity->size.y, 2908 p_entity->size.y,
1352 }, 2909 },
1353 0.14f, 2910 0.14f,
1354 12, 2911 12,
1355 Fade(p_entity->color, 0.92f)); 2912 Canvas_Color_Fade(p_entity->color, 0.92f));
1356 DrawRectangleRoundedLinesEx( 2913 DrawRectangleRoundedLinesEx(
1357 (Rectangle){ 2914 (Rectangle){
1358 p_entity->position.x, 2915 p_entity->position.x,
1359 p_entity->position.y, 2916 p_entity->position.y,
1360 p_entity->size.x, 2917 p_entity->size.x,
1361 p_entity->size.y, 2918 p_entity->size.y,
1362 }, 2919 },
1363 0.14f, 2920 0.14f,
1364 12, 2921 12,
1365 1.0f / zoom, 2922 1.0f / zoom,
1366 Fade(BLACK, 0.08f)); 2923 p_theme->entity_outline);
1367 break; 2924 break;
1368 case CANVAS_ENTITY_CIRCLE: 2925 case CANVAS_ENTITY_CIRCLE:
1369 DrawCircleV( 2926 DrawCircleV(
1370 (Vector2){ 2927 (Vector2){
1371 p_entity->position.x + shadow_offset, 2928 p_entity->position.x + shadow_offset,
1411 p_entity->position.y - grow - lift, 2968 p_entity->position.y - grow - lift,
1412 p_entity->size.x + grow * 2.0f, 2969 p_entity->size.x + grow * 2.0f,
1413 p_entity->size.y + grow * 2.0f, 2970 p_entity->size.y + grow * 2.0f,
1414 }; 2971 };
1415 Color base_fill = p_entity->active ? 2972 Color base_fill = p_entity->active ?
1416 (Color){35, 99, 235, 255} : 2973 p_theme->button_active :
1417 (Color){17, 24, 39, 255}; 2974 p_theme->button;
1418 Color hover_fill = p_entity->active ? 2975 Color hover_fill = p_entity->active ?
1419 (Color){59, 130, 246, 255} : 2976 p_theme->button_active_hover :
1420 (Color){39, 48, 64, 255}; 2977 p_theme->button_hover;
1421 Color fill = Canvas_Color_Lerp(base_fill, hover_fill, hover); 2978 Color fill = Canvas_Color_Lerp(base_fill, hover_fill, hover);
1422 Canvas_Theme_Draw_Shadow( 2979 Canvas_Theme_Draw_Shadow(
1423 p_theme, 2980 p_theme,
1424 bounds, 2981 bounds,
1425 0.46f, 2982 0.46f,
1435 bounds.x + (bounds.width - text_size.x) * 0.5f, 2992 bounds.x + (bounds.width - text_size.x) * 0.5f,
1436 bounds.y + (bounds.height - text_size.y) * 0.5f, 2993 bounds.y + (bounds.height - text_size.y) * 0.5f,
1437 }, 2994 },
1438 16.0f, 2995 16.0f,
1439 0.1f, 2996 0.1f,
1440 WHITE); 2997 p_theme->on_accent);
1441 break; 2998 break;
1442 } 2999 }
1443 case CANVAS_ENTITY_TEXT_AREA: { 3000 case CANVAS_ENTITY_TEXT_AREA: {
1444 Rectangle bounds = { 3001 Rectangle bounds = {
1445 p_entity->position.x, 3002 p_entity->position.x,
1453 bounds, 3010 bounds,
1454 0.10f, 3011 0.10f,
1455 14, 3012 14,
1456 (p_entity->active ? 2.0f : 1.0f) / zoom, 3013 (p_entity->active ? 2.0f : 1.0f) / zoom,
1457 p_entity->active ? p_theme->accent : p_theme->border); 3014 p_entity->active ? p_theme->accent : p_theme->border);
1458 DrawTextEx( 3015 Canvas_Draw_Text_Area_Content(p_entity, font, p_theme);
1459 font,
1460 p_entity->text[0] ? p_entity->text : "Write something...",
1461 (Vector2){bounds.x + 16.0f, bounds.y + 15.0f},
1462 16.0f,
1463 0.1f,
1464 p_entity->text[0] ? p_theme->text : p_theme->text_muted);
1465 if (p_entity->active) {
1466 DrawCircleV(
1467 (Vector2){bounds.x + bounds.width - 16.0f, bounds.y + 16.0f},
1468 3.0f,
1469 p_theme->accent);
1470 }
1471 break; 3016 break;
1472 } 3017 }
1473 case CANVAS_ENTITY_DROPDOWN: { 3018 case CANVAS_ENTITY_DROPDOWN: {
1474 Rectangle bounds = { 3019 Rectangle bounds = {
1475 p_entity->position.x, 3020 p_entity->position.x,
1550 if (amount > 0.0f) { 3095 if (amount > 0.0f) {
1551 DrawLineEx( 3096 DrawLineEx(
1552 (Vector2){bounds.x, bounds.y + 56.0f}, 3097 (Vector2){bounds.x, bounds.y + 56.0f},
1553 (Vector2){bounds.x + bounds.width, bounds.y + 56.0f}, 3098 (Vector2){bounds.x + bounds.width, bounds.y + 56.0f},
1554 1.0f / zoom, 3099 1.0f / zoom,
1555 Fade(p_theme->border, amount)); 3100 Canvas_Color_Fade(p_theme->border, amount));
1556 if (amount > 0.55f) { 3101 if (amount > 0.55f) {
1557 float body_alpha = Canvas_Clamp( 3102 float body_alpha = Canvas_Clamp(
1558 (amount - 0.55f) / 0.45f, 3103 (amount - 0.55f) / 0.45f,
1559 0.0f, 3104 0.0f,
1560 1.0f); 3105 1.0f);
1562 font, 3107 font,
1563 "Drag, zoom, pin, and combine primitives\nacross one continuous workspace.", 3108 "Drag, zoom, pin, and combine primitives\nacross one continuous workspace.",
1564 (Vector2){bounds.x + 16.0f, bounds.y + 76.0f}, 3109 (Vector2){bounds.x + 16.0f, bounds.y + 76.0f},
1565 14.0f, 3110 14.0f,
1566 0.1f, 3111 0.1f,
1567 Fade(p_theme->text_muted, body_alpha)); 3112 Canvas_Color_Fade(p_theme->text_muted, body_alpha));
1568 } 3113 }
1569 } 3114 }
1570 break; 3115 break;
1571 } 3116 }
1572 case CANVAS_ENTITY_CARD: { 3117 case CANVAS_ENTITY_CARD: {
1583 bounds.width - 24.0f, 92.0f}, 3128 bounds.width - 24.0f, 92.0f},
1584 0.08f, 3129 0.08f,
1585 12, 3130 12,
1586 p_entity->active ? 3131 p_entity->active ?
1587 p_theme->accent_soft : 3132 p_theme->accent_soft :
1588 Fade(p_entity->color, 0.22f)); 3133 Canvas_Color_Fade(p_entity->color, 0.22f));
1589 DrawCircleV( 3134 DrawCircleV(
1590 (Vector2){bounds.x + 44.0f, bounds.y + 58.0f}, 3135 (Vector2){bounds.x + 44.0f, bounds.y + 58.0f},
1591 18.0f, 3136 18.0f,
1592 p_entity->active ? (Color){37, 99, 235, 255} : p_entity->color); 3137 p_entity->active ?
3138 p_theme->accent :
3139 p_entity->color);
1593 DrawTextEx( 3140 DrawTextEx(
1594 font, 3141 font,
1595 p_entity->text, 3142 p_entity->text,
1596 (Vector2){bounds.x + 16.0f, bounds.y + 122.0f}, 3143 (Vector2){bounds.x + 16.0f, bounds.y + 122.0f},
1597 20.0f, 3144 20.0f,
1664 label, 3211 label,
1665 (Vector2){center.x - label_size.x * 0.5f, 3212 (Vector2){center.x - label_size.x * 0.5f,
1666 center.y - label_size.y * 0.5f}, 3213 center.y - label_size.y * 0.5f},
1667 13.0f, 3214 13.0f,
1668 0.0f, 3215 0.0f,
1669 day == p_entity->value ? WHITE : p_theme->text); 3216 day == p_entity->value ?
3217 p_theme->on_accent :
3218 p_theme->text);
1670 } 3219 }
1671 break; 3220 break;
1672 } 3221 }
1673 case CANVAS_ENTITY_SWITCH: { 3222 case CANVAS_ENTITY_SWITCH: {
1674 float amount = p_entity->animation_amount; 3223 float amount = p_entity->animation_amount;
1677 p_entity->position.x, 3226 p_entity->position.x,
1678 p_entity->position.y, 3227 p_entity->position.y,
1679 p_entity->size.x, 3228 p_entity->size.x,
1680 p_entity->size.y, 3229 p_entity->size.y,
1681 }; 3230 };
1682 Color track = Canvas_Color_Lerp( 3231 Color off_track = Canvas_Color_Lerp(
1683 p_theme->border, 3232 p_theme->switch_track_off,
1684 p_theme->accent, 3233 p_theme->switch_track_off_hover,
1685 eased); 3234 p_entity->hover_amount);
1686 if (!p_entity->active) { 3235 Color on_track = Canvas_Color_Lerp(
1687 track = Canvas_Color_Lerp( 3236 p_theme->switch_track_on,
1688 track, 3237 p_theme->switch_track_on_hover,
1689 p_theme->text_muted, 3238 p_entity->hover_amount);
1690 p_entity->hover_amount); 3239 Color track = Canvas_Color_Lerp(off_track, on_track, eased);
1691 }
1692 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.50f, 18, zoom, 1.0f); 3240 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.50f, 18, zoom, 1.0f);
1693 DrawRectangleRounded(bounds, 0.50f, 18, track); 3241 DrawRectangleRounded(bounds, 0.50f, 18, track);
1694 float knob_x = bounds.x + 18.0f + 3242 float knob_x = bounds.x + 18.0f +
1695 eased * (bounds.width - 36.0f); 3243 eased * (bounds.width - 36.0f);
1696 float knob_radius = pressed ? 12.5f : 14.0f; 3244 float knob_radius = pressed ? 12.5f : 14.0f;
1697 DrawCircleV( 3245 DrawCircleV(
1698 (Vector2){knob_x, bounds.y + bounds.height * 0.5f}, 3246 (Vector2){knob_x, bounds.y + bounds.height * 0.5f},
1699 knob_radius, 3247 knob_radius,
1700 WHITE); 3248 p_theme->switch_thumb);
1701 break; 3249 break;
1702 } 3250 }
1703 case CANVAS_ENTITY_TABLE: { 3251 case CANVAS_ENTITY_TABLE: {
1704 Rectangle bounds = { 3252 Rectangle bounds = {
1705 p_entity->position.x, 3253 p_entity->position.x,
1782 opacity); 3330 opacity);
1783 DrawRectangleRounded( 3331 DrawRectangleRounded(
1784 bounds, 3332 bounds,
1785 0.20f, 3333 0.20f,
1786 16, 3334 16,
1787 Fade(p_theme->toast_surface, opacity)); 3335 Canvas_Color_Fade(p_theme->toast_surface, opacity));
1788 DrawRectangleRoundedLinesEx( 3336 DrawRectangleRoundedLinesEx(
1789 bounds, 3337 bounds,
1790 0.20f, 3338 0.20f,
1791 16, 3339 16,
1792 1.0f / zoom, 3340 1.0f / zoom,
1793 Fade(p_theme->toast_border, opacity)); 3341 Canvas_Color_Fade(p_theme->toast_border, opacity));
1794 DrawTextEx( 3342 DrawTextEx(
1795 font, 3343 font,
1796 p_entity->text, 3344 p_entity->text,
1797 (Vector2){bounds.x + 16.0f * unit, bounds.y + 18.0f * unit}, 3345 (Vector2){bounds.x + 16.0f * unit, bounds.y + 18.0f * unit},
1798 15.0f * unit, 3346 15.0f * unit,
1799 0.0f, 3347 0.0f,
1800 Fade(p_theme->text, opacity)); 3348 Canvas_Color_Fade(p_theme->text, opacity));
1801 DrawTextEx( 3349 DrawTextEx(
1802 font, 3350 font,
1803 "Sunday, December 3 at 9:00 AM", 3351 "Sunday, December 3 at 9:00 AM",
1804 (Vector2){bounds.x + 16.0f * unit, bounds.y + 45.0f * unit}, 3352 (Vector2){bounds.x + 16.0f * unit, bounds.y + 45.0f * unit},
1805 14.0f * unit, 3353 14.0f * unit,
1806 0.0f, 3354 0.0f,
1807 Fade(p_theme->text_muted, opacity)); 3355 Canvas_Color_Fade(p_theme->text_muted, opacity));
1808 Rectangle undo = { 3356 Rectangle undo = {
1809 bounds.x + bounds.width - 108.0f * unit, 3357 bounds.x + bounds.width - 108.0f * unit,
1810 bounds.y + 24.0f * unit, 3358 bounds.y + 24.0f * unit,
1811 54.0f * unit, 3359 54.0f * unit,
1812 34.0f * unit, 3360 34.0f * unit,
1813 }; 3361 };
1814 DrawRectangleRounded( 3362 DrawRectangleRounded(
1815 undo, 3363 undo,
1816 0.26f, 3364 0.26f,
1817 10, 3365 10,
1818 Fade(p_theme->surface_muted, opacity)); 3366 Canvas_Color_Fade(p_theme->surface_muted, opacity));
1819 DrawRectangleRoundedLinesEx( 3367 DrawRectangleRoundedLinesEx(
1820 undo, 3368 undo,
1821 0.26f, 3369 0.26f,
1822 10, 3370 10,
1823 1.0f / zoom, 3371 1.0f / zoom,
1824 Fade(p_theme->border, opacity)); 3372 Canvas_Color_Fade(p_theme->border, opacity));
1825 DrawTextEx( 3373 DrawTextEx(
1826 font, 3374 font,
1827 "Undo", 3375 "Undo",
1828 (Vector2){undo.x + 12.0f * unit, undo.y + 10.0f * unit}, 3376 (Vector2){undo.x + 12.0f * unit, undo.y + 10.0f * unit},
1829 13.0f * unit, 3377 13.0f * unit,
1830 0.0f, 3378 0.0f,
1831 Fade(p_theme->text, opacity)); 3379 Canvas_Color_Fade(p_theme->text, opacity));
1832 Vector2 close = { 3380 Vector2 close = {
1833 bounds.x + bounds.width - 24.0f * unit, 3381 bounds.x + bounds.width - 24.0f * unit,
1834 bounds.y + bounds.height * 0.5f, 3382 bounds.y + bounds.height * 0.5f,
1835 }; 3383 };
1836 DrawLineEx( 3384 DrawLineEx(
1837 (Vector2){close.x - 4.0f * unit, close.y - 4.0f * unit}, 3385 (Vector2){close.x - 4.0f * unit, close.y - 4.0f * unit},
1838 (Vector2){close.x + 4.0f * unit, close.y + 4.0f * unit}, 3386 (Vector2){close.x + 4.0f * unit, close.y + 4.0f * unit},
1839 1.5f / zoom, 3387 1.5f / zoom,
1840 Fade(p_theme->text_muted, opacity)); 3388 Canvas_Color_Fade(p_theme->text_muted, opacity));
1841 DrawLineEx( 3389 DrawLineEx(
1842 (Vector2){close.x + 4.0f * unit, close.y - 4.0f * unit}, 3390 (Vector2){close.x + 4.0f * unit, close.y - 4.0f * unit},
1843 (Vector2){close.x - 4.0f * unit, close.y + 4.0f * unit}, 3391 (Vector2){close.x - 4.0f * unit, close.y + 4.0f * unit},
1844 1.5f / zoom, 3392 1.5f / zoom,
1845 Fade(p_theme->text_muted, opacity)); 3393 Canvas_Color_Fade(p_theme->text_muted, opacity));
1846 break; 3394 break;
1847 } 3395 }
1848 case CANVAS_ENTITY_SCROLL_AREA: { 3396 case CANVAS_ENTITY_SCROLL_AREA: {
1849 Rectangle bounds = { 3397 Rectangle bounds = {
1850 p_entity->position.x, 3398 p_entity->position.x,
1894 ((float)p_entity->value / 240.0f) * (bounds.height - 106.0f); 3442 ((float)p_entity->value / 240.0f) * (bounds.height - 106.0f);
1895 DrawRectangleRounded( 3443 DrawRectangleRounded(
1896 (Rectangle){bounds.x + bounds.width - 14.0f, thumb_y, 5.0f, 42.0f}, 3444 (Rectangle){bounds.x + bounds.width - 14.0f, thumb_y, 5.0f, 42.0f},
1897 0.50f, 3445 0.50f,
1898 6, 3446 6,
1899 Fade(p_theme->text_muted, 0.82f)); 3447 Canvas_Color_Fade(p_theme->text_muted, 0.82f));
1900 break; 3448 break;
1901 } 3449 }
1902 case CANVAS_ENTITY_IMAGE: { 3450 case CANVAS_ENTITY_IMAGE: {
1903 Rectangle bounds = { 3451 Rectangle bounds = {
1904 p_entity->position.x, 3452 p_entity->position.x,
1955 0.1f, 3503 0.1f,
1956 p_theme->text_muted); 3504 p_theme->text_muted);
1957 #endif 3505 #endif
1958 break; 3506 break;
1959 } 3507 }
3508 case CANVAS_ENTITY_LUCIDE_GALLERY: {
3509 Rectangle bounds = {
3510 p_entity->position.x,
3511 p_entity->position.y,
3512 p_entity->size.x,
3513 p_entity->size.y,
3514 };
3515 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 14, zoom, 1.0f);
3516 DrawRectangleRounded(bounds, 0.04f, 14, p_theme->surface);
3517 DrawRectangleRoundedLinesEx(
3518 bounds,
3519 0.04f,
3520 14,
3521 1.0f / zoom,
3522 p_theme->border);
3523 DrawTextEx(
3524 font,
3525 "Lucide icons",
3526 (Vector2){bounds.x + 24.0f, bounds.y + 22.0f},
3527 26.0f,
3528 0.0f,
3529 p_theme->text);
3530 DrawTextEx(
3531 font,
3532 TextFormat(
3533 "%d of %d icons - Raylib vectors",
3534 Canvas_Lucide_Count_Matches(p_entity->text),
3535 CANVAS_LUCIDE_ICON_COUNT),
3536 (Vector2){bounds.x + 24.0f, bounds.y + 56.0f},
3537 13.0f,
3538 0.0f,
3539 p_theme->text_muted);
3540 Rectangle search = Canvas_Lucide_Search_Bounds(p_entity);
3541 DrawRectangleRounded(
3542 search,
3543 0.24f,
3544 10,
3545 p_entity->active ? p_theme->surface : p_theme->surface_muted);
3546 DrawRectangleRoundedLinesEx(
3547 search,
3548 0.24f,
3549 10,
3550 (p_entity->active ? 2.0f : 1.0f) / zoom,
3551 p_entity->active ? p_theme->accent : p_theme->border);
3552 const char *p_search_text =
3553 p_entity->text[0] ? p_entity->text : "Search icons";
3554 DrawTextEx(
3555 font,
3556 p_search_text,
3557 (Vector2){search.x + 16.0f, search.y + 13.0f},
3558 14.0f,
3559 0.0f,
3560 p_entity->text[0] ? p_theme->text : p_theme->text_muted);
3561 if (p_entity->active) {
3562 Vector2 query_size = MeasureTextEx(
3563 font,
3564 p_entity->text,
3565 14.0f,
3566 0.0f);
3567 float caret_x = search.x + 16.0f + query_size.x + 1.0f;
3568 DrawLineEx(
3569 (Vector2){caret_x, search.y + 11.0f},
3570 (Vector2){caret_x, search.y + 31.0f},
3571 1.0f / zoom,
3572 p_theme->accent);
3573 }
3574 DrawLineEx(
3575 (Vector2){bounds.x, bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT},
3576 (Vector2){
3577 bounds.x + bounds.width,
3578 bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT,
3579 },
3580 1.0f / zoom,
3581 p_theme->border);
3582
3583 float content_top = bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT;
3584 float content_bottom = bounds.y + bounds.height - CANVAS_LUCIDE_PADDING;
3585 int32 visible_index = 0;
3586 for (int32 icon_index = 0;
3587 icon_index < CANVAS_LUCIDE_ICON_COUNT;
3588 icon_index++) {
3589 const Canvas_Lucide_Icon *p_icon =
3590 &CANVAS_LUCIDE_ICONS[icon_index];
3591 if (!Canvas_Lucide_Name_Matches(
3592 p_icon->p_name,
3593 p_entity->text)) {
3594 continue;
3595 }
3596 int32 row = visible_index / CANVAS_LUCIDE_COLUMNS;
3597 int32 column = visible_index % CANVAS_LUCIDE_COLUMNS;
3598 visible_index++;
3599 float card_x =
3600 bounds.x + CANVAS_LUCIDE_PADDING +
3601 (float)column *
3602 (CANVAS_LUCIDE_CARD_WIDTH + CANVAS_LUCIDE_GAP);
3603 float card_y =
3604 content_top + CANVAS_LUCIDE_PADDING +
3605 (float)row *
3606 (CANVAS_LUCIDE_CARD_HEIGHT + CANVAS_LUCIDE_GAP) -
3607 (float)p_entity->value;
3608 if (card_y < content_top ||
3609 card_y + CANVAS_LUCIDE_CARD_HEIGHT > content_bottom) {
3610 continue;
3611 }
3612 Rectangle card = {
3613 card_x,
3614 card_y,
3615 CANVAS_LUCIDE_CARD_WIDTH,
3616 CANVAS_LUCIDE_CARD_HEIGHT,
3617 };
3618 DrawRectangleRounded(card, 0.10f, 8, p_theme->surface_muted);
3619 DrawRectangleRoundedLinesEx(
3620 card,
3621 0.10f,
3622 8,
3623 1.0f / zoom,
3624 p_theme->border);
3625
3626 float icon_scale = 2.0f;
3627 Vector2 icon_origin = {
3628 card.x + (card.width - 24.0f * icon_scale) * 0.5f,
3629 card.y + 13.0f,
3630 };
3631 Canvas_Lucide_Draw_Icon(
3632 p_icon,
3633 icon_origin,
3634 icon_scale,
3635 1.8f,
3636 p_theme->text);
3637 Vector2 label_size = MeasureTextEx(
3638 font,
3639 p_icon->p_name,
3640 11.0f,
3641 0.0f);
3642 float label_x = card.x + (card.width - label_size.x) * 0.5f;
3643 if (label_x < card.x + 5.0f) label_x = card.x + 5.0f;
3644 DrawTextEx(
3645 font,
3646 p_icon->p_name,
3647 (Vector2){label_x, card.y + 73.0f},
3648 11.0f,
3649 0.0f,
3650 p_theme->text_muted);
3651 }
3652
3653 float max_scroll = Canvas_Lucide_Max_Scroll(p_entity);
3654 if (max_scroll > 0.0f) {
3655 float track_height = bounds.height - CANVAS_LUCIDE_HEADER_HEIGHT - 24.0f;
3656 float thumb_height = fmaxf(
3657 36.0f,
3658 track_height * bounds.height /
3659 Canvas_Lucide_Content_Height(p_entity->text));
3660 float thumb_y =
3661 content_top + 12.0f +
3662 ((float)p_entity->value / max_scroll) *
3663 (track_height - thumb_height);
3664 DrawRectangleRounded(
3665 (Rectangle){
3666 bounds.x + bounds.width - 10.0f,
3667 thumb_y,
3668 5.0f,
3669 thumb_height,
3670 },
3671 0.50f,
3672 6,
3673 Canvas_Color_Fade(p_theme->text_muted, 0.72f));
3674 }
3675 break;
3676 }
1960 default: 3677 default:
1961 break; 3678 break;
1962 } 3679 }
1963 3680
1964 if (p_entity->pinned) { 3681 if (p_entity->pinned) {
1965 float radius = 7.0f / zoom; 3682 const Canvas_Lucide_Icon *p_pin = Canvas_Lucide_Find_Icon("pin");
1966 Vector2 center = { 3683 float scale = 0.72f / zoom;
1967 p_entity->position.x + p_entity->size.x - 10.0f / zoom, 3684 Vector2 origin = {
1968 p_entity->position.y + 10.0f / zoom, 3685 p_entity->position.x + p_entity->size.x - 21.0f / zoom,
3686 p_entity->position.y + 3.0f / zoom,
1969 }; 3687 };
1970 DrawCircleV(center, radius, p_theme->accent); 3688 Canvas_Lucide_Draw_Icon(
1971 DrawLineEx( 3689 p_pin,
1972 (Vector2){center.x, center.y - 3.5f / zoom}, 3690 origin,
1973 (Vector2){center.x, center.y + 4.0f / zoom}, 3691 scale,
1974 1.5f / zoom, 3692 1.8f / zoom,
1975 WHITE); 3693 p_theme->pin);
1976 } 3694 }
1977 if (selected && p_entity->type != CANVAS_ENTITY_NOTIFICATION) { 3695 if (selected && p_entity->type != CANVAS_ENTITY_NOTIFICATION) {
1978 Canvas_Draw_Selection(p_entity, zoom); 3696 Canvas_Draw_Selection(p_entity, zoom, p_theme->selection);
1979 } 3697 }
1980 } 3698 }
1981 3699
1982 static void Canvas_Draw_Dropdown_Menu( 3700 static void Canvas_Draw_Dropdown_Menu(
1983 const Canvas_Entity *p_entity, 3701 const Canvas_Entity *p_entity,
1984 Font font, 3702 Font font,
1985 Vector2 world_pointer, 3703 Vector2 world_pointer,
1986 float zoom, 3704 float zoom,
1987 const Canvas_Theme *p_theme) 3705 const Canvas_Theme *p_theme)
1988 { 3706 {
3707 float opacity = Canvas_Entity_Visibility(p_entity);
3708 if (opacity <= 0.001f) return;
3709 Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity);
3710 p_theme = &faded_theme;
3711
1989 static const char *options[] = { 3712 static const char *options[] = {
1990 "Design", 3713 "Design",
1991 "Engineering", 3714 "Engineering",
1992 "Research", 3715 "Research",
1993 }; 3716 };
2036 p_theme->accent); 3759 p_theme->accent);
2037 } 3760 }
2038 } 3761 }
2039 } 3762 }
2040 3763
2041 void Canvas_Draw_World( 3764 static Camera2D Canvas_Raylib_Camera(const Canvas_Camera *p_camera)
2042 const Canvas_Camera *p_camera, 3765 {
2043 const Canvas_Scene *p_scene, 3766 return (Camera2D){
2044 Font font,
2045 const Canvas_Theme *p_theme)
2046 {
2047 Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition());
2048 Camera2D camera = {
2049 .offset = { 3767 .offset = {
2050 (float)p_camera->viewport_width * 0.5f, 3768 (float)p_camera->viewport_width * 0.5f,
2051 (float)p_camera->viewport_height * 0.5f, 3769 (float)p_camera->viewport_height * 0.5f,
2052 }, 3770 },
2053 .target = p_camera->target, 3771 .target = p_camera->target,
2054 .rotation = 0.0f, 3772 .rotation = 0.0f,
2055 .zoom = p_camera->zoom, 3773 .zoom = p_camera->zoom,
2056 }; 3774 };
2057 3775 }
2058 BeginMode2D(camera); 3776
3777 void Canvas_Draw_World_Background(
3778 const Canvas_Camera *p_camera,
3779 const Canvas_Scene *p_scene,
3780 const Canvas_Theme *p_theme)
3781 {
3782 BeginMode2D(Canvas_Raylib_Camera(p_camera));
2059 if (p_scene->show_grid) Canvas_Draw_Grid(p_camera, p_theme); 3783 if (p_scene->show_grid) Canvas_Draw_Grid(p_camera, p_theme);
2060 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { 3784 EndMode2D();
2061 Canvas_Draw_Entity( 3785 }
2062 &p_scene->p_entities[index], 3786
2063 font, 3787 void Canvas_Draw_World_Entity(
2064 p_camera->zoom, 3788 const Canvas_Camera *p_camera,
2065 (int32)index == p_scene->selected_index, 3789 const Canvas_Scene *p_scene,
2066 (int32)index == p_scene->pressed_index && IsMouseButtonDown(MOUSE_BUTTON_LEFT), 3790 size_t entity_index,
2067 p_theme); 3791 Font font,
2068 } 3792 const Canvas_Theme *p_theme)
3793 {
3794 if (entity_index >= Dowa_Array_Length(p_scene->p_entities)) return;
3795 BeginMode2D(Canvas_Raylib_Camera(p_camera));
3796 Canvas_Draw_Entity(
3797 &p_scene->p_entities[entity_index],
3798 font,
3799 p_camera->zoom,
3800 (int32)entity_index == p_scene->selected_index,
3801 (int32)entity_index == p_scene->pressed_index &&
3802 IsMouseButtonDown(MOUSE_BUTTON_LEFT),
3803 p_theme);
3804 EndMode2D();
3805 }
3806
3807 void Canvas_Draw_Web_Chrome_Entity(
3808 const Canvas_Camera *p_camera,
3809 const Canvas_Scene *p_scene,
3810 size_t entity_index,
3811 Font font,
3812 const Canvas_Theme *p_theme,
3813 const char *p_url,
3814 boolean can_go_back,
3815 boolean can_go_forward)
3816 {
3817 if (entity_index >= Dowa_Array_Length(p_scene->p_entities)) return;
3818 const Canvas_Entity *p_entity = &p_scene->p_entities[entity_index];
3819 if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT ||
3820 p_entity->web_chrome_amount <= 0.01f) {
3821 return;
3822 }
3823
3824 float zoom = p_camera->zoom;
3825 float opacity = Canvas_Entity_Visibility(p_entity);
3826 Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity);
3827 p_theme = &faded_theme;
3828 Rectangle toolbar = Canvas_Web_Toolbar_Bounds(p_camera, p_entity);
3829 Rectangle back = Canvas_Web_Toolbar_Button_Bounds(toolbar, zoom, 0);
3830 Rectangle forward = Canvas_Web_Toolbar_Button_Bounds(toolbar, zoom, 1);
3831 Rectangle address = Canvas_Web_Address_Bounds(toolbar, zoom);
3832 Vector2 pointer = Canvas_Camera_Screen_To_World(
3833 p_camera,
3834 GetMousePosition());
3835
3836 BeginMode2D(Canvas_Raylib_Camera(p_camera));
3837 Vector2 toolbar_screen = Canvas_Camera_World_To_Screen(
3838 p_camera,
3839 (Vector2){toolbar.x, toolbar.y});
3840 BeginScissorMode(
3841 (int32)toolbar_screen.x,
3842 (int32)toolbar_screen.y,
3843 (int32)(toolbar.width * zoom),
3844 (int32)fmaxf(1.0f, toolbar.height * zoom));
3845 DrawRectangleRounded(toolbar, 0.18f, 10, p_theme->surface);
3846 DrawRectangleRoundedLinesEx(
3847 toolbar,
3848 0.18f,
3849 10,
3850 1.0f / zoom,
3851 p_theme->border);
3852
3853 Rectangle buttons[] = {back, forward};
3854 boolean enabled[] = {can_go_back, can_go_forward};
3855 const char *icon_names[] = {"arrow-left", "arrow-right"};
3856 for (int32 index = 0; index < 2; index++) {
3857 boolean hovered =
3858 CheckCollisionPointRec(pointer, buttons[index]) && enabled[index];
3859 if (hovered) {
3860 DrawRectangleRounded(
3861 buttons[index],
3862 0.28f,
3863 8,
3864 p_theme->accent_soft);
3865 }
3866 Canvas_Lucide_Draw_Icon(
3867 Canvas_Lucide_Find_Icon(icon_names[index]),
3868 (Vector2){
3869 buttons[index].x + 5.5f / zoom,
3870 buttons[index].y + 5.5f / zoom,
3871 },
3872 0.71f / zoom,
3873 1.8f / zoom,
3874 enabled[index] ? p_theme->text : p_theme->text_muted);
3875 }
3876
3877 DrawRectangleRounded(address, 0.32f, 8, p_theme->surface_muted);
3878 DrawRectangleRoundedLinesEx(
3879 address,
3880 0.32f,
3881 8,
3882 1.0f / zoom,
3883 p_theme->border);
3884 boolean editing =
3885 p_scene->editing_web_url &&
3886 p_scene->web_url_entity_id == p_entity->id;
3887 const char *p_display_url = editing ?
3888 p_scene->web_url_draft :
3889 p_url;
3890 char visible_url[CANVAS_ENTITY_TEXT_CAPACITY] = {0};
3891 snprintf(
3892 visible_url,
3893 sizeof(visible_url),
3894 "%s",
3895 p_display_url && p_display_url[0] ? p_display_url : "about:blank");
3896 float font_size = 13.0f / zoom;
3897 float max_width = address.width - 18.0f / zoom;
3898 size_t url_length = strlen(visible_url);
3899 while (!editing && url_length > 3 &&
3900 MeasureTextEx(font, visible_url, font_size, 0.0f).x > max_width) {
3901 url_length--;
3902 visible_url[url_length] = '\0';
3903 }
3904 if (!editing &&
3905 p_display_url &&
3906 strlen(p_display_url) > url_length &&
3907 url_length > 3) {
3908 visible_url[url_length - 3] = '.';
3909 visible_url[url_length - 2] = '.';
3910 visible_url[url_length - 1] = '.';
3911 }
3912 Vector2 address_screen = Canvas_Camera_World_To_Screen(
3913 p_camera,
3914 (Vector2){address.x, address.y});
3915 float address_clip_height = fminf(
3916 address.height,
3917 toolbar.y + toolbar.height - address.y);
3918 if (address_clip_height < 1.0f / zoom) {
3919 EndScissorMode();
3920 EndMode2D();
3921 return;
3922 }
3923 BeginScissorMode(
3924 (int32)address_screen.x,
3925 (int32)address_screen.y,
3926 (int32)(address.width * zoom),
3927 (int32)(address_clip_height * zoom));
3928 float text_x = address.x + 9.0f / zoom -
3929 (editing ? p_scene->web_url_scroll_x : 0.0f);
3930 if (editing &&
3931 p_scene->web_url_cursor != p_scene->web_url_selection_anchor) {
3932 int32 selection_start = 0;
3933 int32 selection_end = 0;
3934 Canvas_Web_URL_Selection_Bounds(
3935 p_scene,
3936 &selection_start,
3937 &selection_end);
3938 char prefix[CANVAS_ENTITY_TEXT_CAPACITY];
3939 char selected[CANVAS_ENTITY_TEXT_CAPACITY];
3940 memcpy(prefix, visible_url, (size_t)selection_start);
3941 prefix[selection_start] = '\0';
3942 memcpy(
3943 selected,
3944 visible_url + selection_start,
3945 (size_t)(selection_end - selection_start));
3946 selected[selection_end - selection_start] = '\0';
3947 float selection_x =
3948 text_x + MeasureTextEx(font, prefix, font_size, 0.0f).x;
3949 float selection_width =
3950 MeasureTextEx(font, selected, font_size, 0.0f).x;
3951 DrawRectangleRec(
3952 (Rectangle){
3953 selection_x,
3954 address.y + 5.0f / zoom,
3955 selection_width,
3956 18.0f / zoom,
3957 },
3958 p_theme->accent_soft);
3959 }
3960 DrawTextEx(
3961 font,
3962 visible_url,
3963 (Vector2){
3964 text_x,
3965 address.y + 7.0f / zoom,
3966 },
3967 font_size,
3968 0.0f,
3969 editing ? p_theme->text : p_theme->text_muted);
3970 if (editing && ((int32)(GetTime() * 2.0) % 2) == 0) {
3971 char prefix[CANVAS_ENTITY_TEXT_CAPACITY];
3972 memcpy(
3973 prefix,
3974 visible_url,
3975 (size_t)p_scene->web_url_cursor);
3976 prefix[p_scene->web_url_cursor] = '\0';
3977 float caret_x =
3978 text_x + MeasureTextEx(font, prefix, font_size, 0.0f).x;
3979 DrawLineEx(
3980 (Vector2){caret_x, address.y + 6.0f / zoom},
3981 (Vector2){caret_x, address.y + 22.0f / zoom},
3982 1.5f / zoom,
3983 p_theme->accent);
3984 }
3985 EndScissorMode();
3986 EndMode2D();
3987 }
3988
3989 void Canvas_Draw_World_Overlays(
3990 const Canvas_Camera *p_camera,
3991 const Canvas_Scene *p_scene,
3992 Font font,
3993 const Canvas_Theme *p_theme)
3994 {
3995 Vector2 world_pointer =
3996 Canvas_Camera_Screen_To_World(p_camera, GetMousePosition());
3997 BeginMode2D(Canvas_Raylib_Camera(p_camera));
2069 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { 3998 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
2070 const Canvas_Entity *p_entity = &p_scene->p_entities[index]; 3999 const Canvas_Entity *p_entity = &p_scene->p_entities[index];
2071 if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { 4000 if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) {
2072 Canvas_Draw_Dropdown_Menu( 4001 Canvas_Draw_Dropdown_Menu(
2073 p_entity, 4002 p_entity,
2076 p_camera->zoom, 4005 p_camera->zoom,
2077 p_theme); 4006 p_theme);
2078 } 4007 }
2079 } 4008 }
2080 EndMode2D(); 4009 EndMode2D();
4010 }
4011
4012 void Canvas_Draw_World(
4013 const Canvas_Camera *p_camera,
4014 const Canvas_Scene *p_scene,
4015 Font font,
4016 const Canvas_Theme *p_theme)
4017 {
4018 Canvas_Draw_World_Background(p_camera, p_scene, p_theme);
4019 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
4020 Canvas_Draw_World_Entity(
4021 p_camera,
4022 p_scene,
4023 index,
4024 font,
4025 p_theme);
4026 }
4027 Canvas_Draw_World_Overlays(p_camera, p_scene, font, p_theme);
2081 } 4028 }
2082 4029
2083 const char *Canvas_Entity_Type_Name(Canvas_Entity_Type type) 4030 const char *Canvas_Entity_Type_Name(Canvas_Entity_Type type)
2084 { 4031 {
2085 switch (type) { 4032 switch (type) {
2097 case CANVAS_ENTITY_TABLE: return "Table"; 4044 case CANVAS_ENTITY_TABLE: return "Table";
2098 case CANVAS_ENTITY_NOTIFICATION: return "Notification"; 4045 case CANVAS_ENTITY_NOTIFICATION: return "Notification";
2099 case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area"; 4046 case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area";
2100 case CANVAS_ENTITY_IMAGE: return "Image"; 4047 case CANVAS_ENTITY_IMAGE: return "Image";
2101 case CANVAS_ENTITY_WEB_CONTENT: return "Web content"; 4048 case CANVAS_ENTITY_WEB_CONTENT: return "Web content";
4049 case CANVAS_ENTITY_LUCIDE_GALLERY: return "Lucide gallery";
2102 default: return "Unknown"; 4050 default: return "Unknown";
2103 } 4051 }
2104 } 4052 }