comparison infinite_canvas/canvas.c @ 276:b55c22cff335

Add interactive infinite canvas prototype
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 16:57:56 -0700
parents
children 8d560f50ed4c
comparison
equal deleted inserted replaced
274:c9be578316a6 276:b55c22cff335
1 #include "infinite_canvas/canvas.h"
2
3 #include <math.h>
4 #include <stdarg.h>
5 #include <stdio.h>
6 #include <string.h>
7
8 static const char *CANVAS_DROPDOWN_OPTIONS[] = {
9 "Design",
10 "Engineering",
11 "Research",
12 };
13
14 static const char *CANVAS_TABLE_NAMES[] = {
15 "Canvas",
16 "Browser",
17 "Notes",
18 "Assets",
19 };
20
21 static const char *CANVAS_TABLE_STATUSES[] = {
22 "Ready",
23 "Live",
24 "Draft",
25 "Synced",
26 };
27
28 static float Canvas_Clamp(float value, float min_value, float max_value)
29 {
30 if (value < min_value) return min_value;
31 if (value > max_value) return max_value;
32 return value;
33 }
34
35 static Color Canvas_Entity_Color(uint32 index)
36 {
37 static const Color colors[] = {
38 {96, 165, 250, 255},
39 {251, 191, 36, 255},
40 {167, 139, 250, 255},
41 {45, 212, 191, 255},
42 {251, 113, 133, 255},
43 };
44 return colors[index % (sizeof(colors) / sizeof(colors[0]))];
45 }
46
47 static Color Canvas_Color_Lerp(Color from, Color to, float amount)
48 {
49 amount = Canvas_Clamp(amount, 0.0f, 1.0f);
50 return (Color){
51 (uint8)((float)from.r + ((float)to.r - (float)from.r) * amount),
52 (uint8)((float)from.g + ((float)to.g - (float)from.g) * amount),
53 (uint8)((float)from.b + ((float)to.b - (float)from.b) * amount),
54 (uint8)((float)from.a + ((float)to.a - (float)from.a) * amount),
55 };
56 }
57
58 void Canvas_Camera_Init(Canvas_Camera *p_camera, int32 width, int32 height)
59 {
60 *p_camera = (Canvas_Camera){
61 .target = {0.0f, 0.0f},
62 .zoom = 1.0f,
63 .viewport_width = width,
64 .viewport_height = height,
65 .min_zoom = 0.01f,
66 .max_zoom = 64.0f,
67 };
68 }
69
70 void Canvas_Camera_Set_Viewport(Canvas_Camera *p_camera, int32 width, int32 height)
71 {
72 p_camera->viewport_width = width;
73 p_camera->viewport_height = height;
74 }
75
76 Vector2 Canvas_Camera_Screen_To_World(const Canvas_Camera *p_camera, Vector2 screen)
77 {
78 Vector2 center = {
79 (float)p_camera->viewport_width * 0.5f,
80 (float)p_camera->viewport_height * 0.5f,
81 };
82 return (Vector2){
83 p_camera->target.x + (screen.x - center.x) / p_camera->zoom,
84 p_camera->target.y + (screen.y - center.y) / p_camera->zoom,
85 };
86 }
87
88 Vector2 Canvas_Camera_World_To_Screen(const Canvas_Camera *p_camera, Vector2 world)
89 {
90 Vector2 center = {
91 (float)p_camera->viewport_width * 0.5f,
92 (float)p_camera->viewport_height * 0.5f,
93 };
94 return (Vector2){
95 center.x + (world.x - p_camera->target.x) * p_camera->zoom,
96 center.y + (world.y - p_camera->target.y) * p_camera->zoom,
97 };
98 }
99
100 void Canvas_Camera_Pan(Canvas_Camera *p_camera, Vector2 screen_delta)
101 {
102 p_camera->target.x -= screen_delta.x / p_camera->zoom;
103 p_camera->target.y -= screen_delta.y / p_camera->zoom;
104 }
105
106 void Canvas_Camera_Zoom_At(
107 Canvas_Camera *p_camera,
108 Vector2 screen_anchor,
109 float zoom_multiplier)
110 {
111 Vector2 world_anchor = Canvas_Camera_Screen_To_World(p_camera, screen_anchor);
112 p_camera->zoom = Canvas_Clamp(
113 p_camera->zoom * zoom_multiplier,
114 p_camera->min_zoom,
115 p_camera->max_zoom);
116 Vector2 moved_anchor = Canvas_Camera_Screen_To_World(p_camera, screen_anchor);
117 p_camera->target.x += world_anchor.x - moved_anchor.x;
118 p_camera->target.y += world_anchor.y - moved_anchor.y;
119 }
120
121 Rectangle Canvas_Camera_Viewport_Bounds(const Canvas_Camera *p_camera)
122 {
123 Vector2 min = Canvas_Camera_Screen_To_World(p_camera, (Vector2){0.0f, 0.0f});
124 Vector2 max = Canvas_Camera_Screen_To_World(
125 p_camera,
126 (Vector2){(float)p_camera->viewport_width, (float)p_camera->viewport_height});
127 return (Rectangle){
128 .x = min.x,
129 .y = min.y,
130 .width = max.x - min.x,
131 .height = max.y - min.y,
132 };
133 }
134
135 static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity)
136 {
137 if (p_entity->type == CANVAS_ENTITY_CIRCLE) {
138 return (Rectangle){
139 p_entity->position.x - p_entity->size.x,
140 p_entity->position.y - p_entity->size.x,
141 p_entity->size.x * 2.0f,
142 p_entity->size.x * 2.0f,
143 };
144 }
145 if (p_entity->type == CANVAS_ENTITY_LINE) {
146 float end_x = p_entity->position.x + p_entity->size.x;
147 float end_y = p_entity->position.y + p_entity->size.y;
148 return (Rectangle){
149 fminf(p_entity->position.x, end_x),
150 fminf(p_entity->position.y, end_y),
151 fmaxf(fabsf(p_entity->size.x), 0.001f),
152 fmaxf(fabsf(p_entity->size.y), 0.001f),
153 };
154 }
155
156 Rectangle bounds = {
157 p_entity->position.x,
158 p_entity->position.y,
159 p_entity->size.x,
160 p_entity->size.y,
161 };
162 if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) {
163 bounds.height += 128.0f;
164 }
165 return bounds;
166 }
167
168 typedef struct {
169 char *p_buffer;
170 size_t capacity;
171 size_t length;
172 boolean truncated;
173 } Canvas_Context_Writer;
174
175 static void Canvas_Context_Append(
176 Canvas_Context_Writer *p_writer,
177 const char *p_format,
178 ...)
179 {
180 if (p_writer->truncated || p_writer->capacity == 0) {
181 p_writer->truncated = TRUE;
182 return;
183 }
184
185 va_list arguments;
186 va_start(arguments, p_format);
187 int32 written = vsnprintf(
188 p_writer->p_buffer + p_writer->length,
189 p_writer->capacity - p_writer->length,
190 p_format,
191 arguments);
192 va_end(arguments);
193 if (written < 0) {
194 p_writer->truncated = TRUE;
195 return;
196 }
197
198 size_t available = p_writer->capacity - p_writer->length;
199 if ((size_t)written >= available) {
200 p_writer->length = p_writer->capacity - 1;
201 p_writer->truncated = TRUE;
202 return;
203 }
204 p_writer->length += (size_t)written;
205 }
206
207 static void Canvas_Context_Append_Quoted(
208 Canvas_Context_Writer *p_writer,
209 const char *p_text)
210 {
211 Canvas_Context_Append(p_writer, "\"");
212 for (const char *p_cursor = p_text; *p_cursor && !p_writer->truncated; p_cursor++) {
213 if (*p_cursor == '"' || *p_cursor == '\\') {
214 Canvas_Context_Append(p_writer, "\\%c", *p_cursor);
215 } else if (*p_cursor == '\n') {
216 Canvas_Context_Append(p_writer, "\\n");
217 } else if (*p_cursor == '\r') {
218 continue;
219 } else {
220 Canvas_Context_Append(p_writer, "%c", *p_cursor);
221 }
222 }
223 Canvas_Context_Append(p_writer, "\"");
224 }
225
226 static void Canvas_Context_Append_Entity_State(
227 Canvas_Context_Writer *p_writer,
228 const Canvas_Entity *p_entity)
229 {
230 switch (p_entity->type) {
231 case CANVAS_ENTITY_RECTANGLE:
232 case CANVAS_ENTITY_CIRCLE:
233 case CANVAS_ENTITY_LINE:
234 Canvas_Context_Append(
235 p_writer,
236 "color=rgba(%u,%u,%u,%u)",
237 p_entity->color.r,
238 p_entity->color.g,
239 p_entity->color.b,
240 p_entity->color.a);
241 break;
242 case CANVAS_ENTITY_TEXT:
243 Canvas_Context_Append(p_writer, "content=");
244 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
245 break;
246 case CANVAS_ENTITY_BUTTON:
247 Canvas_Context_Append(p_writer, "label=");
248 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
249 Canvas_Context_Append(
250 p_writer,
251 " activated=%s",
252 p_entity->active ? "true" : "false");
253 break;
254 case CANVAS_ENTITY_TEXT_AREA:
255 Canvas_Context_Append(p_writer, "content=");
256 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
257 Canvas_Context_Append(
258 p_writer,
259 " editing=%s",
260 p_entity->active ? "true" : "false");
261 break;
262 case CANVAS_ENTITY_DROPDOWN:
263 Canvas_Context_Append(p_writer, "selected=");
264 Canvas_Context_Append_Quoted(
265 p_writer,
266 CANVAS_DROPDOWN_OPTIONS[p_entity->value]);
267 Canvas_Context_Append(
268 p_writer,
269 " open=%s",
270 p_entity->active ? "true" : "false");
271 break;
272 case CANVAS_ENTITY_ACCORDION:
273 Canvas_Context_Append(p_writer, "title=");
274 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
275 Canvas_Context_Append(
276 p_writer,
277 " expanded=%s",
278 p_entity->active ? "true" : "false");
279 break;
280 case CANVAS_ENTITY_CARD:
281 Canvas_Context_Append(p_writer, "title=");
282 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
283 Canvas_Context_Append(
284 p_writer,
285 " emphasized=%s",
286 p_entity->active ? "true" : "false");
287 break;
288 case CANVAS_ENTITY_CALENDAR:
289 Canvas_Context_Append(
290 p_writer,
291 "selected_date=\"2026-08-%02d\"",
292 p_entity->value);
293 break;
294 case CANVAS_ENTITY_SWITCH:
295 Canvas_Context_Append(
296 p_writer,
297 "checked=%s",
298 p_entity->active ? "true" : "false");
299 break;
300 case CANVAS_ENTITY_TABLE:
301 Canvas_Context_Append(p_writer, "selected_row={name=");
302 Canvas_Context_Append_Quoted(
303 p_writer,
304 CANVAS_TABLE_NAMES[p_entity->value]);
305 Canvas_Context_Append(p_writer, ", status=");
306 Canvas_Context_Append_Quoted(
307 p_writer,
308 CANVAS_TABLE_STATUSES[p_entity->value]);
309 Canvas_Context_Append(p_writer, "}");
310 break;
311 case CANVAS_ENTITY_NOTIFICATION:
312 Canvas_Context_Append(p_writer, "title=");
313 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
314 Canvas_Context_Append(
315 p_writer,
316 " active=%s",
317 p_entity->active ? "true" : "false");
318 break;
319 case CANVAS_ENTITY_SCROLL_AREA: {
320 int32 first_item = p_entity->value / 42 + 1;
321 int32 last_item = first_item + 3;
322 if (last_item > 10) last_item = 10;
323 Canvas_Context_Append(
324 p_writer,
325 "scroll_offset=%d visible_items=%d-%d",
326 p_entity->value,
327 first_item,
328 last_item);
329 break;
330 }
331 case CANVAS_ENTITY_IMAGE:
332 Canvas_Context_Append(p_writer, "source=");
333 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
334 break;
335 case CANVAS_ENTITY_WEB_CONTENT:
336 Canvas_Context_Append(p_writer, "url=");
337 Canvas_Context_Append_Quoted(p_writer, p_entity->text);
338 break;
339 default:
340 Canvas_Context_Append(p_writer, "value=%d", p_entity->value);
341 break;
342 }
343 }
344
345 Canvas_Context_Snapshot Canvas_Scene_Build_Visible_Context(
346 const Canvas_Scene *p_scene,
347 const Canvas_Camera *p_camera,
348 char *p_buffer,
349 size_t buffer_size)
350 {
351 Canvas_Context_Writer writer = {
352 .p_buffer = p_buffer,
353 .capacity = buffer_size,
354 };
355 if (buffer_size > 0) p_buffer[0] = '\0';
356
357 Rectangle viewport = Canvas_Camera_Viewport_Bounds(p_camera);
358 Canvas_Context_Append(
359 &writer,
360 "camera_context zoom=%.2f viewport=[%.1f, %.1f, %.1f, %.1f]\n"
361 "entities:\n",
362 p_camera->zoom,
363 viewport.x,
364 viewport.y,
365 viewport.width,
366 viewport.height);
367
368 Canvas_Context_Snapshot snapshot = {0};
369 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
370 const Canvas_Entity *p_entity = &p_scene->p_entities[index];
371 if (p_entity->type == CANVAS_ENTITY_NOTIFICATION &&
372 p_entity->animation_amount <= 0.01f) {
373 continue;
374 }
375 if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue;
376
377 snapshot.visible_count++;
378 Canvas_Context_Append(
379 &writer,
380 "- id=%u type=\"%s\" position=[%.1f, %.1f] size=[%.1f, %.1f] pinned=%s\n"
381 " state: ",
382 p_entity->id,
383 Canvas_Entity_Type_Name(p_entity->type),
384 p_entity->position.x,
385 p_entity->position.y,
386 p_entity->size.x,
387 p_entity->size.y,
388 p_entity->pinned ? "true" : "false");
389 Canvas_Context_Append_Entity_State(&writer, p_entity);
390 Canvas_Context_Append(&writer, "\n");
391 }
392 if (snapshot.visible_count == 0) {
393 Canvas_Context_Append(&writer, " (none)\n");
394 }
395
396 snapshot.bytes_written = writer.length;
397 snapshot.truncated = writer.truncated;
398 return snapshot;
399 }
400
401 float Canvas_Grid_Spacing(float zoom)
402 {
403 float spacing = 64.0f;
404 while ((spacing * zoom < 48.0f) && (spacing < 1048576.0f)) spacing *= 2.0f;
405 while ((spacing * zoom > 96.0f) && (spacing > 0.0009765625f)) spacing *= 0.5f;
406 return spacing;
407 }
408
409 void Canvas_Scene_Init(Canvas_Scene *p_scene, Dowa_Arena *p_arena)
410 {
411 memset(p_scene, 0, sizeof(*p_scene));
412 p_scene->p_arena = p_arena;
413 p_scene->selected_index = -1;
414 p_scene->pressed_index = -1;
415 p_scene->hover_index = -1;
416 p_scene->show_grid = FALSE;
417 Dowa_Array_Reserve_Arena(p_scene->p_entities, CANVAS_MAX_ENTITIES, p_arena);
418 }
419
420 boolean Canvas_Scene_Add(Canvas_Scene *p_scene, Canvas_Entity_Type type, Vector2 position)
421 {
422 if (Dowa_Array_Length(p_scene->p_entities) >= CANVAS_MAX_ENTITIES) return FALSE;
423
424 uint32 index = p_scene->next_entity++;
425 Canvas_Entity entity = {
426 .id = ++p_scene->next_entity_id,
427 .type = type,
428 .position = position,
429 .size = {160.0f, 100.0f},
430 .color = Canvas_Entity_Color(index),
431 .text = {0},
432 .value = 0,
433 .active = FALSE,
434 .pinned = FALSE,
435 .hover_amount = 0.0f,
436 .animation_amount = 0.0f,
437 };
438
439 if (type == CANVAS_ENTITY_CIRCLE) entity.size = (Vector2){56.0f, 56.0f};
440 if (type == CANVAS_ENTITY_LINE) entity.size = (Vector2){180.0f, 80.0f};
441 if (type == CANVAS_ENTITY_TEXT) {
442 entity.size = (Vector2){180.0f, 32.0f};
443 snprintf(entity.text, sizeof(entity.text), "Entity %u", index + 1);
444 }
445 if (type == CANVAS_ENTITY_BUTTON) {
446 entity.size = (Vector2){156.0f, 48.0f};
447 snprintf(entity.text, sizeof(entity.text), "Continue");
448 }
449 if (type == CANVAS_ENTITY_TEXT_AREA) {
450 entity.size = (Vector2){280.0f, 148.0f};
451 }
452 if (type == CANVAS_ENTITY_DROPDOWN) {
453 entity.size = (Vector2){224.0f, 48.0f};
454 }
455 if (type == CANVAS_ENTITY_ACCORDION) {
456 entity.size = (Vector2){300.0f, 56.0f};
457 snprintf(entity.text, sizeof(entity.text), "How does the canvas work?");
458 }
459 if (type == CANVAS_ENTITY_CARD) {
460 entity.size = (Vector2){280.0f, 220.0f};
461 snprintf(entity.text, sizeof(entity.text), "Infinite workspace");
462 }
463 if (type == CANVAS_ENTITY_CALENDAR) {
464 entity.size = (Vector2){280.0f, 300.0f};
465 entity.value = 17;
466 }
467 if (type == CANVAS_ENTITY_SWITCH) {
468 entity.size = (Vector2){64.0f, 36.0f};
469 }
470 if (type == CANVAS_ENTITY_TABLE) {
471 entity.size = (Vector2){420.0f, 230.0f};
472 entity.value = 1;
473 }
474 if (type == CANVAS_ENTITY_NOTIFICATION) {
475 entity.size = (Vector2){384.0f, 82.0f};
476 entity.active = TRUE;
477 entity.animation_amount = 1.0f;
478 snprintf(entity.text, sizeof(entity.text), "Event created");
479 }
480 if (type == CANVAS_ENTITY_SCROLL_AREA) {
481 entity.size = (Vector2){320.0f, 220.0f};
482 }
483 if (type == CANVAS_ENTITY_IMAGE) {
484 entity.size = (Vector2){360.0f, 240.0f};
485 snprintf(
486 entity.text,
487 sizeof(entity.text),
488 "infinite_canvas/assets/image-placeholder.svg");
489 }
490 if (type == CANVAS_ENTITY_WEB_CONTENT) {
491 entity.size = (Vector2){720.0f, 460.0f};
492 entity.active = TRUE;
493 snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com");
494 }
495
496 Dowa_Array_Push_Arena(p_scene->p_entities, entity, p_scene->p_arena);
497 return TRUE;
498 }
499
500 void Canvas_Scene_Clear(Canvas_Scene *p_scene)
501 {
502 Dowa_Array_Clear(p_scene->p_entities);
503 p_scene->next_entity = 0;
504 p_scene->selected_index = -1;
505 p_scene->pressed_index = -1;
506 p_scene->hover_index = -1;
507 p_scene->dragging = FALSE;
508 p_scene->resizing = FALSE;
509 p_scene->notification_stack_amount = 0.0f;
510 }
511
512 void Canvas_Scene_Add_Demo(Canvas_Scene *p_scene)
513 {
514 Canvas_Scene_Clear(p_scene);
515 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_RECTANGLE, (Vector2){-550.0f, -520.0f});
516 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CIRCLE, (Vector2){-300.0f, -470.0f});
517 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_LINE, (Vector2){-180.0f, -500.0f});
518 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TEXT, (Vector2){50.0f, -500.0f});
519 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_BUTTON, (Vector2){280.0f, -510.0f});
520 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SWITCH, (Vector2){500.0f, -504.0f});
521
522 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TEXT_AREA, (Vector2){-550.0f, -350.0f});
523 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_DROPDOWN, (Vector2){-230.0f, -350.0f});
524 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_ACCORDION, (Vector2){30.0f, -350.0f});
525 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CARD, (Vector2){360.0f, -350.0f});
526
527 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CALENDAR, (Vector2){-550.0f, -130.0f});
528 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TABLE, (Vector2){-230.0f, -130.0f});
529 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, -130.0f});
530 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, -20.0f});
531 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, 90.0f});
532 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SCROLL_AREA, (Vector2){610.0f, -130.0f});
533
534 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_IMAGE, (Vector2){-550.0f, 220.0f});
535 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){-150.0f, 120.0f});
536 }
537
538 void Canvas_Scene_Add_Browser_Grid(Canvas_Scene *p_scene)
539 {
540 Canvas_Scene_Clear(p_scene);
541 for (int32 row = 0; row < 2; row++) {
542 for (int32 column = 0; column < 5; column++) {
543 Vector2 position = {
544 -614.0f + (float)column * 248.0f,
545 -182.0f + (float)row * 188.0f,
546 };
547 if (!Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, position)) return;
548 Canvas_Entity *p_entity =
549 &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1];
550 p_entity->size = (Vector2){236.0f, 176.0f};
551 }
552 }
553 }
554
555 static float Canvas_Distance_To_Segment(Vector2 point, Vector2 start, Vector2 end)
556 {
557 Vector2 segment = {end.x - start.x, end.y - start.y};
558 Vector2 relative = {point.x - start.x, point.y - start.y};
559 float length_squared = segment.x * segment.x + segment.y * segment.y;
560 if (length_squared <= 0.0001f) {
561 return sqrtf(relative.x * relative.x + relative.y * relative.y);
562 }
563
564 float projection = (relative.x * segment.x + relative.y * segment.y) / length_squared;
565 projection = Canvas_Clamp(projection, 0.0f, 1.0f);
566 Vector2 closest = {
567 start.x + segment.x * projection,
568 start.y + segment.y * projection,
569 };
570 float dx = point.x - closest.x;
571 float dy = point.y - closest.y;
572 return sqrtf(dx * dx + dy * dy);
573 }
574
575 static boolean Canvas_Entity_Contains(
576 const Canvas_Entity *p_entity,
577 Vector2 point,
578 float zoom)
579 {
580 switch (p_entity->type) {
581 case CANVAS_ENTITY_RECTANGLE:
582 return CheckCollisionPointRec(
583 point,
584 (Rectangle){
585 p_entity->position.x,
586 p_entity->position.y,
587 p_entity->size.x,
588 p_entity->size.y,
589 }) ? TRUE : FALSE;
590 case CANVAS_ENTITY_CIRCLE: {
591 float dx = point.x - p_entity->position.x;
592 float dy = point.y - p_entity->position.y;
593 return (dx * dx + dy * dy <= p_entity->size.x * p_entity->size.x) ? TRUE : FALSE;
594 }
595 case CANVAS_ENTITY_LINE: {
596 Vector2 end = {
597 p_entity->position.x + p_entity->size.x,
598 p_entity->position.y + p_entity->size.y,
599 };
600 return Canvas_Distance_To_Segment(point, p_entity->position, end) <= 10.0f / zoom;
601 }
602 case CANVAS_ENTITY_TEXT:
603 case CANVAS_ENTITY_BUTTON:
604 case CANVAS_ENTITY_TEXT_AREA:
605 case CANVAS_ENTITY_DROPDOWN:
606 case CANVAS_ENTITY_ACCORDION:
607 case CANVAS_ENTITY_CARD:
608 case CANVAS_ENTITY_CALENDAR:
609 case CANVAS_ENTITY_SWITCH:
610 case CANVAS_ENTITY_TABLE:
611 case CANVAS_ENTITY_SCROLL_AREA:
612 case CANVAS_ENTITY_IMAGE: {
613 float height = p_entity->size.y;
614 if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) {
615 height += 8.0f + 3.0f * 40.0f;
616 }
617 return CheckCollisionPointRec(
618 point,
619 (Rectangle){
620 p_entity->position.x,
621 p_entity->position.y,
622 p_entity->size.x,
623 height,
624 }) ? TRUE : FALSE;
625 }
626 case CANVAS_ENTITY_NOTIFICATION:
627 if (p_entity->animation_amount <= 0.01f) return FALSE;
628 return CheckCollisionPointRec(
629 point,
630 (Rectangle){
631 p_entity->position.x,
632 p_entity->position.y,
633 p_entity->size.x,
634 p_entity->size.y,
635 }) ? TRUE : FALSE;
636 case CANVAS_ENTITY_WEB_CONTENT: {
637 float frame_inset = CANVAS_WEB_FRAME_INSET / zoom;
638 float resize_handle = CANVAS_WEB_RESIZE_HANDLE / zoom;
639 Rectangle outer = {
640 p_entity->position.x,
641 p_entity->position.y,
642 p_entity->size.x,
643 p_entity->size.y,
644 };
645 Rectangle content = {
646 outer.x + frame_inset,
647 outer.y + frame_inset,
648 outer.width - frame_inset - resize_handle,
649 outer.height - frame_inset - resize_handle,
650 };
651 return CheckCollisionPointRec(point, outer) &&
652 !CheckCollisionPointRec(point, content);
653 }
654 default:
655 return FALSE;
656 }
657 }
658
659 static boolean Canvas_Web_Resize_Handle_Contains(
660 const Canvas_Entity *p_entity,
661 Vector2 point,
662 float zoom)
663 {
664 if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT &&
665 p_entity->type != CANVAS_ENTITY_IMAGE) {
666 return FALSE;
667 }
668 float handle_size = CANVAS_WEB_RESIZE_HANDLE / zoom;
669 return CheckCollisionPointRec(
670 point,
671 (Rectangle){
672 p_entity->position.x + p_entity->size.x - handle_size,
673 p_entity->position.y + p_entity->size.y - handle_size,
674 handle_size,
675 handle_size,
676 }) ? TRUE : FALSE;
677 }
678
679 int32 Canvas_Scene_Pick(
680 const Canvas_Scene *p_scene,
681 Vector2 world_pointer,
682 float zoom)
683 {
684 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];
686 if (p_entity->type == CANVAS_ENTITY_DROPDOWN &&
687 p_entity->active &&
688 Canvas_Entity_Contains(p_entity, world_pointer, zoom)) {
689 return index;
690 }
691 }
692
693 for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) {
694 if (Canvas_Entity_Contains(&p_scene->p_entities[index], world_pointer, zoom)) return index;
695 }
696 return -1;
697 }
698
699 static void Canvas_Scene_Activate(Canvas_Scene *p_scene, int32 index, Vector2 world_pointer)
700 {
701 if (index < 0 || index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return;
702
703 Canvas_Entity *p_entity = &p_scene->p_entities[index];
704 switch (p_entity->type) {
705 case CANVAS_ENTITY_BUTTON:
706 p_entity->active = !p_entity->active;
707 break;
708 case CANVAS_ENTITY_TEXT_AREA:
709 p_entity->active = TRUE;
710 break;
711 case CANVAS_ENTITY_DROPDOWN:
712 if (world_pointer.y <= p_entity->position.y + p_entity->size.y) {
713 p_entity->active = !p_entity->active;
714 } else if (p_entity->active) {
715 float options_y = p_entity->position.y + p_entity->size.y + 8.0f;
716 int32 option = (int32)((world_pointer.y - options_y) / 40.0f);
717 if (option >= 0 && option < 3) p_entity->value = option;
718 p_entity->active = FALSE;
719 }
720 break;
721 case CANVAS_ENTITY_ACCORDION:
722 p_entity->active = !p_entity->active;
723 break;
724 case CANVAS_ENTITY_CARD:
725 case CANVAS_ENTITY_SWITCH:
726 p_entity->active = !p_entity->active;
727 break;
728 case CANVAS_ENTITY_NOTIFICATION: {
729 float local_x =
730 (world_pointer.x - p_entity->position.x) / p_entity->size.x;
731 if (local_x >= 0.68f) p_entity->active = FALSE;
732 break;
733 }
734 case CANVAS_ENTITY_CALENDAR: {
735 float grid_x = p_entity->position.x + 14.0f;
736 float grid_y = p_entity->position.y + 82.0f;
737 if (world_pointer.x < grid_x ||
738 world_pointer.y < grid_y ||
739 world_pointer.x >= grid_x + 252.0f ||
740 world_pointer.y >= grid_y + 192.0f) {
741 break;
742 }
743 int32 column = (int32)((world_pointer.x - grid_x) / 36.0f);
744 int32 row = (int32)((world_pointer.y - grid_y) / 32.0f);
745 int32 day = row * 7 + column - 6 + 1;
746 if (column >= 0 && column < 7 &&
747 row >= 0 && row < 6 &&
748 day >= 1 && day <= 31) {
749 p_entity->value = day;
750 }
751 break;
752 }
753 case CANVAS_ENTITY_TABLE: {
754 float rows_y = p_entity->position.y + 62.0f;
755 int32 row = (int32)((world_pointer.y - rows_y) / 38.0f);
756 if (world_pointer.y >= rows_y && row >= 0 && row < 4) {
757 p_entity->value = row;
758 }
759 break;
760 }
761 default:
762 break;
763 }
764 }
765
766 static void Canvas_Scene_Update_Text_Input(Canvas_Scene *p_scene)
767 {
768 if (p_scene->selected_index < 0 ||
769 p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return;
770
771 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;
773
774 size_t length = strlen(p_entity->text);
775 int32 codepoint = GetCharPressed();
776 while (codepoint > 0) {
777 if (codepoint >= 32 && codepoint <= 126 && length + 1 < sizeof(p_entity->text)) {
778 p_entity->text[length++] = (char)codepoint;
779 p_entity->text[length] = '\0';
780 }
781 codepoint = GetCharPressed();
782 }
783
784 if (IsKeyPressed(KEY_ENTER) && length + 1 < sizeof(p_entity->text)) {
785 p_entity->text[length++] = '\n';
786 p_entity->text[length] = '\0';
787 }
788 if (IsKeyPressed(KEY_BACKSPACE) && length > 0) {
789 p_entity->text[length - 1] = '\0';
790 }
791 }
792
793 static void Canvas_Entity_Capture_Pin(
794 Canvas_Entity *p_entity,
795 const Canvas_Camera *p_camera)
796 {
797 p_entity->pinned_screen_position =
798 Canvas_Camera_World_To_Screen(p_camera, p_entity->position);
799 p_entity->pinned_screen_size = (Vector2){
800 p_entity->size.x * p_camera->zoom,
801 p_entity->size.y * p_camera->zoom,
802 };
803 }
804
805 static void Canvas_Scene_Update_Animations(
806 Canvas_Scene *p_scene,
807 const Canvas_Camera *p_camera)
808 {
809 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
810 Canvas_Entity *p_entity = &p_scene->p_entities[index];
811 if (p_entity->type != CANVAS_ENTITY_ACCORDION &&
812 p_entity->type != CANVAS_ENTITY_SWITCH) {
813 continue;
814 }
815
816 float target = p_entity->active ? 1.0f : 0.0f;
817 float speed = p_entity->type == CANVAS_ENTITY_SWITCH ? 7.0f : 4.5f;
818 float step = GetFrameTime() * speed;
819 if (p_entity->animation_amount < target) {
820 p_entity->animation_amount = fminf(
821 target,
822 p_entity->animation_amount + step);
823 } else if (p_entity->animation_amount > target) {
824 p_entity->animation_amount = fmaxf(
825 target,
826 p_entity->animation_amount - step);
827 }
828 float amount = p_entity->animation_amount;
829 float eased = amount * amount * (3.0f - 2.0f * amount);
830 if (p_entity->type == CANVAS_ENTITY_SWITCH) continue;
831 if (p_entity->pinned) {
832 float pinned_scale = p_entity->pinned_screen_size.x / 300.0f;
833 p_entity->pinned_screen_size.y =
834 (56.0f + 108.0f * eased) * pinned_scale;
835 p_entity->size.y =
836 p_entity->pinned_screen_size.y / p_camera->zoom;
837 } else {
838 p_entity->size.y = 56.0f + 108.0f * eased;
839 }
840 }
841 }
842
843 boolean Canvas_Scene_Update_Interaction(
844 Canvas_Scene *p_scene,
845 const Canvas_Camera *p_camera,
846 boolean block_pointer_input)
847 {
848 Canvas_Scene_Update_Text_Input(p_scene);
849 Canvas_Scene_Update_Animations(p_scene, p_camera);
850
851 if (block_pointer_input && p_scene->pressed_index < 0) {
852 return FALSE;
853 }
854
855 Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition());
856 p_scene->hover_index = block_pointer_input ?
857 -1 :
858 Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom);
859 boolean consumed_scroll = FALSE;
860 if (p_scene->hover_index >= 0) {
861 Canvas_Entity *p_hovered = &p_scene->p_entities[p_scene->hover_index];
862 if (p_hovered->type == CANVAS_ENTITY_SCROLL_AREA) {
863 float wheel = GetMouseWheelMove();
864 if (wheel != 0.0f) {
865 p_hovered->value = (int32)Canvas_Clamp(
866 (float)p_hovered->value - wheel * 28.0f,
867 0.0f,
868 240.0f);
869 consumed_scroll = TRUE;
870 }
871 }
872 }
873
874 float animation_step = Canvas_Clamp(GetFrameTime() * 12.0f, 0.0f, 1.0f);
875 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
876 float target = ((int32)index == p_scene->hover_index) ? 1.0f : 0.0f;
877 Canvas_Entity *p_entity = &p_scene->p_entities[index];
878 p_entity->hover_amount += (target - p_entity->hover_amount) * animation_step;
879 }
880
881 if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && !IsKeyDown(KEY_SPACE)) {
882 p_scene->selected_index = Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom);
883 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
884 if ((int32)index != p_scene->selected_index &&
885 p_scene->p_entities[index].type == CANVAS_ENTITY_TEXT_AREA) {
886 p_scene->p_entities[index].active = FALSE;
887 }
888 if ((int32)index != p_scene->selected_index &&
889 p_scene->p_entities[index].type == CANVAS_ENTITY_DROPDOWN) {
890 p_scene->p_entities[index].active = FALSE;
891 }
892 }
893
894 if (p_scene->selected_index >= 0) {
895 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
896 p_scene->pressed_index = p_scene->selected_index;
897 p_scene->dragging = FALSE;
898 p_scene->resizing = Canvas_Web_Resize_Handle_Contains(
899 p_entity,
900 world_pointer,
901 p_camera->zoom);
902 p_scene->pointer_start = world_pointer;
903 p_scene->entity_start = p_entity->position;
904 p_scene->entity_size_start = p_entity->size;
905 }
906 }
907
908 if (p_scene->pressed_index >= 0 && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
909 Vector2 delta = {
910 world_pointer.x - p_scene->pointer_start.x,
911 world_pointer.y - p_scene->pointer_start.y,
912 };
913 float screen_distance_squared =
914 (delta.x * p_camera->zoom) * (delta.x * p_camera->zoom) +
915 (delta.y * p_camera->zoom) * (delta.y * p_camera->zoom);
916 if (screen_distance_squared >= 16.0f) p_scene->dragging = TRUE;
917
918 if (p_scene->dragging) {
919 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index];
920 if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) {
921 p_scene->dragging = FALSE;
922 } else if (p_scene->resizing) {
923 p_entity->size = (Vector2){
924 fmaxf(CANVAS_WEB_MIN_WIDTH, p_scene->entity_size_start.x + delta.x),
925 fmaxf(CANVAS_WEB_MIN_HEIGHT, p_scene->entity_size_start.y + delta.y),
926 };
927 } else {
928 p_entity->position = (Vector2){
929 p_scene->entity_start.x + delta.x,
930 p_scene->entity_start.y + delta.y,
931 };
932 }
933 if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera);
934 }
935 }
936
937 if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
938 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];
941 if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera);
942 }
943 p_scene->pressed_index = -1;
944 p_scene->dragging = FALSE;
945 p_scene->resizing = FALSE;
946 }
947 return p_scene->pressed_index >= 0 || consumed_scroll;
948 }
949
950 boolean Canvas_Scene_Is_Text_Editing(const Canvas_Scene *p_scene)
951 {
952 if (p_scene->selected_index < 0 ||
953 p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) {
954 return FALSE;
955 }
956
957 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;
959 }
960
961 void Canvas_Scene_Toggle_Selected_Pin(
962 Canvas_Scene *p_scene,
963 const Canvas_Camera *p_camera)
964 {
965 if (p_scene->selected_index < 0 ||
966 p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) {
967 return;
968 }
969
970 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
971 if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) return;
972 p_entity->pinned = !p_entity->pinned;
973 if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera);
974 }
975
976 void Canvas_Scene_Update_Notification_Stack(
977 Canvas_Scene *p_scene,
978 const Canvas_Camera *p_camera)
979 {
980 const float width = 384.0f;
981 const float height = 82.0f;
982 const float margin = 24.0f;
983 const float gap = 12.0f;
984 float delta = GetFrameTime();
985 float follow = 1.0f - expf(-delta * 16.0f);
986 Vector2 mouse = GetMousePosition();
987 boolean hovered = FALSE;
988 int32 active_count = 0;
989
990 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
991 Canvas_Entity *p_entity = &p_scene->p_entities[index];
992 if (p_entity->type != CANVAS_ENTITY_NOTIFICATION) continue;
993 float visibility_target = p_entity->active ? 1.0f : 0.0f;
994 if (p_entity->animation_amount < visibility_target) {
995 p_entity->animation_amount = fminf(
996 visibility_target,
997 p_entity->animation_amount + delta * 7.0f);
998 } else if (p_entity->animation_amount > visibility_target) {
999 p_entity->animation_amount = fmaxf(
1000 visibility_target,
1001 p_entity->animation_amount - delta * 7.0f);
1002 }
1003 if (p_entity->active) active_count++;
1004 if (p_entity->screen_space_initialized &&
1005 p_entity->animation_amount > 0.01f &&
1006 CheckCollisionPointRec(
1007 mouse,
1008 (Rectangle){
1009 p_entity->pinned_screen_position.x,
1010 p_entity->pinned_screen_position.y,
1011 p_entity->pinned_screen_size.x,
1012 p_entity->pinned_screen_size.y,
1013 })) {
1014 hovered = TRUE;
1015 }
1016 }
1017
1018 if (!hovered &&
1019 active_count > 1 &&
1020 p_scene->notification_stack_amount > 0.08f) {
1021 float right = (float)p_camera->viewport_width - margin;
1022 float bottom = (float)p_camera->viewport_height - margin;
1023 float top = bottom - (float)active_count * height -
1024 (float)(active_count - 1) * gap;
1025 hovered = CheckCollisionPointRec(
1026 mouse,
1027 (Rectangle){right - width, top, width, bottom - top}) ? TRUE : FALSE;
1028 }
1029
1030 float stack_target = hovered ? 1.0f : 0.0f;
1031 p_scene->notification_stack_amount +=
1032 (stack_target - p_scene->notification_stack_amount) * follow;
1033 float amount = p_scene->notification_stack_amount;
1034 float eased = amount * amount * (3.0f - 2.0f * amount);
1035 int32 rank = 0;
1036
1037 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
1038 Canvas_Entity *p_entity = &p_scene->p_entities[index];
1039 if (p_entity->type != CANVAS_ENTITY_NOTIFICATION ||
1040 p_entity->animation_amount <= 0.01f) {
1041 continue;
1042 }
1043
1044 Vector2 target_position = p_entity->pinned_screen_position;
1045 Vector2 target_size = {width, height};
1046 if (p_entity->active) {
1047 int32 depth = active_count - rank - 1;
1048 Vector2 collapsed_position = {
1049 (float)p_camera->viewport_width - margin - width + (float)depth * 8.0f,
1050 (float)p_camera->viewport_height - margin - height - (float)depth * 8.0f,
1051 };
1052 Vector2 expanded_position = {
1053 (float)p_camera->viewport_width - margin - width,
1054 (float)p_camera->viewport_height - margin - height -
1055 (float)depth * (height + gap),
1056 };
1057 target_position = (Vector2){
1058 collapsed_position.x +
1059 (expanded_position.x - collapsed_position.x) * eased,
1060 collapsed_position.y +
1061 (expanded_position.y - collapsed_position.y) * eased,
1062 };
1063 target_size.x = width - (float)depth * 12.0f * (1.0f - eased);
1064 rank++;
1065 } else {
1066 target_position.x = (float)p_camera->viewport_width + 20.0f;
1067 }
1068
1069 if (!p_entity->screen_space_initialized) {
1070 p_entity->pinned_screen_position = target_position;
1071 p_entity->pinned_screen_size = target_size;
1072 p_entity->screen_space_initialized = TRUE;
1073 } else {
1074 p_entity->pinned_screen_position.x +=
1075 (target_position.x - p_entity->pinned_screen_position.x) * follow;
1076 p_entity->pinned_screen_position.y +=
1077 (target_position.y - p_entity->pinned_screen_position.y) * follow;
1078 p_entity->pinned_screen_size.x +=
1079 (target_size.x - p_entity->pinned_screen_size.x) * follow;
1080 p_entity->pinned_screen_size.y +=
1081 (target_size.y - p_entity->pinned_screen_size.y) * follow;
1082 }
1083 p_entity->position = Canvas_Camera_Screen_To_World(
1084 p_camera,
1085 p_entity->pinned_screen_position);
1086 p_entity->size = (Vector2){
1087 p_entity->pinned_screen_size.x / p_camera->zoom,
1088 p_entity->pinned_screen_size.y / p_camera->zoom,
1089 };
1090 }
1091 }
1092
1093 void Canvas_Scene_Sync_Pinned(
1094 Canvas_Scene *p_scene,
1095 const Canvas_Camera *p_camera)
1096 {
1097 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
1098 Canvas_Entity *p_entity = &p_scene->p_entities[index];
1099 if (!p_entity->pinned) continue;
1100
1101 Vector2 min_offset = {0.0f, 0.0f};
1102 Vector2 max_offset = p_entity->pinned_screen_size;
1103 if (p_entity->type == CANVAS_ENTITY_CIRCLE) {
1104 min_offset = (Vector2){
1105 -p_entity->pinned_screen_size.x,
1106 -p_entity->pinned_screen_size.x,
1107 };
1108 max_offset = (Vector2){
1109 p_entity->pinned_screen_size.x,
1110 p_entity->pinned_screen_size.x,
1111 };
1112 } else if (p_entity->type == CANVAS_ENTITY_LINE) {
1113 min_offset = (Vector2){
1114 fminf(0.0f, p_entity->pinned_screen_size.x),
1115 fminf(0.0f, p_entity->pinned_screen_size.y),
1116 };
1117 max_offset = (Vector2){
1118 fmaxf(0.0f, p_entity->pinned_screen_size.x),
1119 fmaxf(0.0f, p_entity->pinned_screen_size.y),
1120 };
1121 }
1122 float min_x = -min_offset.x;
1123 float min_y = -min_offset.y;
1124 float max_x = fmaxf(
1125 min_x,
1126 (float)p_camera->viewport_width - max_offset.x);
1127 float max_y = fmaxf(
1128 min_y,
1129 (float)p_camera->viewport_height - max_offset.y);
1130 p_entity->pinned_screen_position.x = Canvas_Clamp(
1131 p_entity->pinned_screen_position.x,
1132 min_x,
1133 max_x);
1134 p_entity->pinned_screen_position.y = Canvas_Clamp(
1135 p_entity->pinned_screen_position.y,
1136 min_y,
1137 max_y);
1138 p_entity->position = Canvas_Camera_Screen_To_World(
1139 p_camera,
1140 p_entity->pinned_screen_position);
1141 p_entity->size = (Vector2){
1142 p_entity->pinned_screen_size.x / p_camera->zoom,
1143 p_entity->pinned_screen_size.y / p_camera->zoom,
1144 };
1145 }
1146 }
1147
1148 void Canvas_Update_Camera_Input(
1149 Canvas_Camera *p_camera,
1150 boolean block_pointer_input,
1151 boolean block_keyboard_input)
1152 {
1153 boolean keyboard_pan = IsKeyDown(KEY_SPACE);
1154 boolean pointer_pan = IsMouseButtonDown(MOUSE_BUTTON_MIDDLE) ||
1155 (keyboard_pan && IsMouseButtonDown(MOUSE_BUTTON_LEFT));
1156
1157 if (pointer_pan && !block_pointer_input) {
1158 Canvas_Camera_Pan(p_camera, GetMouseDelta());
1159 }
1160
1161 if (!block_pointer_input) {
1162 float wheel = GetMouseWheelMove();
1163 if (wheel != 0.0f) {
1164 Canvas_Camera_Zoom_At(
1165 p_camera,
1166 GetMousePosition(),
1167 powf(1.15f, wheel));
1168 }
1169 }
1170
1171 if (!block_keyboard_input) {
1172 Vector2 keyboard_delta = {0.0f, 0.0f};
1173 float speed = 600.0f * GetFrameTime();
1174 if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) keyboard_delta.x += speed;
1175 if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) keyboard_delta.x -= speed;
1176 if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) keyboard_delta.y += speed;
1177 if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) keyboard_delta.y -= speed;
1178 Canvas_Camera_Pan(p_camera, keyboard_delta);
1179 }
1180 }
1181
1182 static void Canvas_Draw_Grid(
1183 const Canvas_Camera *p_camera,
1184 const Canvas_Theme *p_theme)
1185 {
1186 Rectangle bounds = Canvas_Camera_Viewport_Bounds(p_camera);
1187 float spacing = Canvas_Grid_Spacing(p_camera->zoom);
1188 int32 first_x = (int32)floorf(bounds.x / spacing);
1189 int32 last_x = (int32)ceilf((bounds.x + bounds.width) / spacing);
1190 int32 first_y = (int32)floorf(bounds.y / spacing);
1191 int32 last_y = (int32)ceilf((bounds.y + bounds.height) / spacing);
1192 Color minor = Fade(p_theme->border, 0.36f);
1193 Color major = Fade(p_theme->border, 0.65f);
1194 Color axis = p_theme->text_muted;
1195
1196 for (int32 index = first_x; index <= last_x; index++) {
1197 float x = (float)index * spacing;
1198 Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor);
1199 DrawLineV(
1200 (Vector2){x, bounds.y},
1201 (Vector2){x, bounds.y + bounds.height},
1202 color);
1203 }
1204
1205 for (int32 index = first_y; index <= last_y; index++) {
1206 float y = (float)index * spacing;
1207 Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor);
1208 DrawLineV(
1209 (Vector2){bounds.x, y},
1210 (Vector2){bounds.x + bounds.width, y},
1211 color);
1212 }
1213 }
1214
1215 static void Canvas_Draw_Selection(
1216 const Canvas_Entity *p_entity,
1217 float zoom)
1218 {
1219 float line = 2.0f / zoom;
1220 float padding = 7.0f / zoom;
1221 Color selection = {0, 122, 255, 230};
1222
1223 switch (p_entity->type) {
1224 case CANVAS_ENTITY_RECTANGLE:
1225 DrawRectangleRoundedLinesEx(
1226 (Rectangle){
1227 p_entity->position.x - padding,
1228 p_entity->position.y - padding,
1229 p_entity->size.x + padding * 2.0f,
1230 p_entity->size.y + padding * 2.0f,
1231 },
1232 0.14f,
1233 12,
1234 line,
1235 selection);
1236 break;
1237 case CANVAS_ENTITY_CIRCLE:
1238 DrawCircleLinesV(
1239 p_entity->position,
1240 p_entity->size.x + padding,
1241 selection);
1242 break;
1243 case CANVAS_ENTITY_LINE:
1244 DrawLineEx(
1245 p_entity->position,
1246 (Vector2){
1247 p_entity->position.x + p_entity->size.x,
1248 p_entity->position.y + p_entity->size.y,
1249 },
1250 10.0f / zoom,
1251 Fade(selection, 0.35f));
1252 break;
1253 case CANVAS_ENTITY_TEXT:
1254 case CANVAS_ENTITY_BUTTON:
1255 case CANVAS_ENTITY_TEXT_AREA:
1256 case CANVAS_ENTITY_DROPDOWN:
1257 case CANVAS_ENTITY_ACCORDION:
1258 case CANVAS_ENTITY_CARD:
1259 case CANVAS_ENTITY_CALENDAR:
1260 case CANVAS_ENTITY_SWITCH:
1261 case CANVAS_ENTITY_TABLE:
1262 case CANVAS_ENTITY_NOTIFICATION:
1263 case CANVAS_ENTITY_SCROLL_AREA:
1264 case CANVAS_ENTITY_IMAGE: {
1265 DrawRectangleRoundedLinesEx(
1266 (Rectangle){
1267 p_entity->position.x - padding,
1268 p_entity->position.y - padding,
1269 p_entity->size.x + padding * 2.0f,
1270 p_entity->size.y + padding * 2.0f,
1271 },
1272 0.18f,
1273 10,
1274 line,
1275 selection);
1276 break;
1277 }
1278 case CANVAS_ENTITY_WEB_CONTENT:
1279 DrawRectangleRoundedLinesEx(
1280 (Rectangle){
1281 p_entity->position.x - padding,
1282 p_entity->position.y - padding,
1283 p_entity->size.x + padding * 2.0f,
1284 p_entity->size.y + padding * 2.0f,
1285 },
1286 0.04f,
1287 10,
1288 line,
1289 selection);
1290 DrawRectangleRec(
1291 (Rectangle){
1292 p_entity->position.x + p_entity->size.x -
1293 CANVAS_WEB_RESIZE_HANDLE / zoom,
1294 p_entity->position.y + p_entity->size.y -
1295 CANVAS_WEB_RESIZE_HANDLE / zoom,
1296 CANVAS_WEB_RESIZE_HANDLE / zoom,
1297 CANVAS_WEB_RESIZE_HANDLE / zoom,
1298 },
1299 Fade(selection, 0.18f));
1300 DrawLineEx(
1301 (Vector2){
1302 p_entity->position.x + p_entity->size.x - 11.0f / zoom,
1303 p_entity->position.y + p_entity->size.y - 4.0f / zoom,
1304 },
1305 (Vector2){
1306 p_entity->position.x + p_entity->size.x - 4.0f / zoom,
1307 p_entity->position.y + p_entity->size.y - 11.0f / zoom,
1308 },
1309 1.5f / zoom,
1310 selection);
1311 break;
1312 default:
1313 break;
1314 }
1315
1316 }
1317
1318 static void Canvas_Draw_Entity(
1319 const Canvas_Entity *p_entity,
1320 Font font,
1321 float zoom,
1322 boolean selected,
1323 boolean pressed,
1324 const Canvas_Theme *p_theme)
1325 {
1326 float shadow_offset = 6.0f / zoom;
1327 Color shadow = p_theme->shadow;
1328 static const char *calendar_weekdays[] = {
1329 "S", "M", "T", "W", "T", "F", "S",
1330 };
1331
1332 switch (p_entity->type) {
1333 case CANVAS_ENTITY_RECTANGLE:
1334 Canvas_Theme_Draw_Shadow(
1335 p_theme,
1336 (Rectangle){
1337 p_entity->position.x,
1338 p_entity->position.y,
1339 p_entity->size.x,
1340 p_entity->size.y,
1341 },
1342 0.14f,
1343 12,
1344 zoom,
1345 1.0f);
1346 DrawRectangleRounded(
1347 (Rectangle){
1348 p_entity->position.x,
1349 p_entity->position.y,
1350 p_entity->size.x,
1351 p_entity->size.y,
1352 },
1353 0.14f,
1354 12,
1355 Fade(p_entity->color, 0.92f));
1356 DrawRectangleRoundedLinesEx(
1357 (Rectangle){
1358 p_entity->position.x,
1359 p_entity->position.y,
1360 p_entity->size.x,
1361 p_entity->size.y,
1362 },
1363 0.14f,
1364 12,
1365 1.0f / zoom,
1366 Fade(BLACK, 0.08f));
1367 break;
1368 case CANVAS_ENTITY_CIRCLE:
1369 DrawCircleV(
1370 (Vector2){
1371 p_entity->position.x + shadow_offset,
1372 p_entity->position.y + shadow_offset,
1373 },
1374 p_entity->size.x,
1375 shadow);
1376 DrawCircleV(p_entity->position, p_entity->size.x, p_entity->color);
1377 break;
1378 case CANVAS_ENTITY_LINE:
1379 DrawLineEx(
1380 p_entity->position,
1381 (Vector2){
1382 p_entity->position.x + p_entity->size.x,
1383 p_entity->position.y + p_entity->size.y,
1384 },
1385 5.0f,
1386 p_entity->color);
1387 DrawCircleV(p_entity->position, 2.5f, p_entity->color);
1388 DrawCircleV(
1389 (Vector2){
1390 p_entity->position.x + p_entity->size.x,
1391 p_entity->position.y + p_entity->size.y,
1392 },
1393 2.5f,
1394 p_entity->color);
1395 break;
1396 case CANVAS_ENTITY_TEXT:
1397 DrawTextEx(
1398 font,
1399 p_entity->text,
1400 p_entity->position,
1401 28.0f,
1402 0.5f,
1403 p_theme->text);
1404 break;
1405 case CANVAS_ENTITY_BUTTON: {
1406 float hover = p_entity->hover_amount;
1407 float grow = pressed ? -1.0f : hover * 2.0f;
1408 float lift = pressed ? 0.0f : hover * 2.0f;
1409 Rectangle bounds = {
1410 p_entity->position.x - grow,
1411 p_entity->position.y - grow - lift,
1412 p_entity->size.x + grow * 2.0f,
1413 p_entity->size.y + grow * 2.0f,
1414 };
1415 Color base_fill = p_entity->active ?
1416 (Color){35, 99, 235, 255} :
1417 (Color){17, 24, 39, 255};
1418 Color hover_fill = p_entity->active ?
1419 (Color){59, 130, 246, 255} :
1420 (Color){39, 48, 64, 255};
1421 Color fill = Canvas_Color_Lerp(base_fill, hover_fill, hover);
1422 Canvas_Theme_Draw_Shadow(
1423 p_theme,
1424 bounds,
1425 0.46f,
1426 16,
1427 zoom,
1428 1.0f);
1429 DrawRectangleRounded(bounds, 0.46f, 16, fill);
1430 Vector2 text_size = MeasureTextEx(font, p_entity->text, 16.0f, 0.1f);
1431 DrawTextEx(
1432 font,
1433 p_entity->text,
1434 (Vector2){
1435 bounds.x + (bounds.width - text_size.x) * 0.5f,
1436 bounds.y + (bounds.height - text_size.y) * 0.5f,
1437 },
1438 16.0f,
1439 0.1f,
1440 WHITE);
1441 break;
1442 }
1443 case CANVAS_ENTITY_TEXT_AREA: {
1444 Rectangle bounds = {
1445 p_entity->position.x,
1446 p_entity->position.y,
1447 p_entity->size.x,
1448 p_entity->size.y,
1449 };
1450 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.10f, 14, zoom, 1.0f);
1451 DrawRectangleRounded(bounds, 0.10f, 14, p_theme->surface);
1452 DrawRectangleRoundedLinesEx(
1453 bounds,
1454 0.10f,
1455 14,
1456 (p_entity->active ? 2.0f : 1.0f) / zoom,
1457 p_entity->active ? p_theme->accent : p_theme->border);
1458 DrawTextEx(
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;
1472 }
1473 case CANVAS_ENTITY_DROPDOWN: {
1474 Rectangle bounds = {
1475 p_entity->position.x,
1476 p_entity->position.y,
1477 p_entity->size.x,
1478 p_entity->size.y,
1479 };
1480 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.30f, 14, zoom, 1.0f);
1481 DrawRectangleRounded(bounds, 0.30f, 14, p_theme->surface);
1482 DrawRectangleRoundedLinesEx(
1483 bounds,
1484 0.30f,
1485 14,
1486 1.0f / zoom,
1487 p_theme->border);
1488 DrawTextEx(
1489 font,
1490 CANVAS_DROPDOWN_OPTIONS[p_entity->value],
1491 (Vector2){bounds.x + 16.0f, bounds.y + 14.0f},
1492 16.0f,
1493 0.1f,
1494 p_theme->text);
1495 Vector2 chevron = {bounds.x + bounds.width - 22.0f, bounds.y + bounds.height * 0.5f};
1496 DrawTriangle(
1497 (Vector2){chevron.x - 5.0f, chevron.y - 2.0f},
1498 (Vector2){chevron.x, chevron.y + 4.0f},
1499 (Vector2){chevron.x + 5.0f, chevron.y - 2.0f},
1500 p_theme->text_muted);
1501 break;
1502 }
1503 case CANVAS_ENTITY_ACCORDION: {
1504 Rectangle bounds = {
1505 p_entity->position.x,
1506 p_entity->position.y,
1507 p_entity->size.x,
1508 p_entity->size.y,
1509 };
1510 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.10f, 14, zoom, 1.0f);
1511 DrawRectangleRounded(bounds, 0.10f, 14, p_theme->surface);
1512 DrawRectangleRoundedLinesEx(
1513 bounds,
1514 0.10f,
1515 14,
1516 1.0f / zoom,
1517 p_theme->border);
1518 DrawTextEx(
1519 font,
1520 p_entity->text,
1521 (Vector2){bounds.x + 16.0f, bounds.y + 18.0f},
1522 16.0f,
1523 0.1f,
1524 p_theme->text);
1525 float amount = p_entity->animation_amount;
1526 float angle = amount * PI * 0.5f;
1527 Vector2 chevron = {bounds.x + bounds.width - 22.0f, bounds.y + 28.0f};
1528 Vector2 direction = {cosf(angle), sinf(angle)};
1529 Vector2 normal = {-direction.y, direction.x};
1530 Vector2 tip = {
1531 chevron.x + direction.x * 3.0f,
1532 chevron.y + direction.y * 3.0f,
1533 };
1534 DrawLineEx(
1535 tip,
1536 (Vector2){
1537 chevron.x - direction.x * 3.0f + normal.x * 4.0f,
1538 chevron.y - direction.y * 3.0f + normal.y * 4.0f,
1539 },
1540 1.5f / zoom,
1541 p_theme->text_muted);
1542 DrawLineEx(
1543 tip,
1544 (Vector2){
1545 chevron.x - direction.x * 3.0f - normal.x * 4.0f,
1546 chevron.y - direction.y * 3.0f - normal.y * 4.0f,
1547 },
1548 1.5f / zoom,
1549 p_theme->text_muted);
1550 if (amount > 0.0f) {
1551 DrawLineEx(
1552 (Vector2){bounds.x, bounds.y + 56.0f},
1553 (Vector2){bounds.x + bounds.width, bounds.y + 56.0f},
1554 1.0f / zoom,
1555 Fade(p_theme->border, amount));
1556 if (amount > 0.55f) {
1557 float body_alpha = Canvas_Clamp(
1558 (amount - 0.55f) / 0.45f,
1559 0.0f,
1560 1.0f);
1561 DrawTextEx(
1562 font,
1563 "Drag, zoom, pin, and combine primitives\nacross one continuous workspace.",
1564 (Vector2){bounds.x + 16.0f, bounds.y + 76.0f},
1565 14.0f,
1566 0.1f,
1567 Fade(p_theme->text_muted, body_alpha));
1568 }
1569 }
1570 break;
1571 }
1572 case CANVAS_ENTITY_CARD: {
1573 Rectangle bounds = {
1574 p_entity->position.x,
1575 p_entity->position.y,
1576 p_entity->size.x,
1577 p_entity->size.y,
1578 };
1579 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 14, zoom, 1.0f);
1580 DrawRectangleRounded(bounds, 0.08f, 14, p_theme->surface);
1581 DrawRectangleRounded(
1582 (Rectangle){bounds.x + 12.0f, bounds.y + 12.0f,
1583 bounds.width - 24.0f, 92.0f},
1584 0.08f,
1585 12,
1586 p_entity->active ?
1587 p_theme->accent_soft :
1588 Fade(p_entity->color, 0.22f));
1589 DrawCircleV(
1590 (Vector2){bounds.x + 44.0f, bounds.y + 58.0f},
1591 18.0f,
1592 p_entity->active ? (Color){37, 99, 235, 255} : p_entity->color);
1593 DrawTextEx(
1594 font,
1595 p_entity->text,
1596 (Vector2){bounds.x + 16.0f, bounds.y + 122.0f},
1597 20.0f,
1598 0.1f,
1599 p_theme->text);
1600 DrawTextEx(
1601 font,
1602 "A flexible surface for tools,\ncontent, and live browser views.",
1603 (Vector2){bounds.x + 16.0f, bounds.y + 154.0f},
1604 14.0f,
1605 0.1f,
1606 p_theme->text_muted);
1607 DrawRectangleRoundedLinesEx(
1608 bounds,
1609 0.08f,
1610 14,
1611 (p_entity->active ? 2.0f : 1.0f) / zoom,
1612 p_entity->active ?
1613 p_theme->accent :
1614 p_theme->border);
1615 break;
1616 }
1617 case CANVAS_ENTITY_CALENDAR: {
1618 Rectangle bounds = {
1619 p_entity->position.x,
1620 p_entity->position.y,
1621 p_entity->size.x,
1622 p_entity->size.y,
1623 };
1624 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 14, zoom, 1.0f);
1625 DrawRectangleRounded(bounds, 0.08f, 14, p_theme->surface);
1626 DrawRectangleRoundedLinesEx(
1627 bounds,
1628 0.08f,
1629 14,
1630 1.0f / zoom,
1631 p_theme->border);
1632 DrawTextEx(
1633 font,
1634 "August 2026",
1635 (Vector2){bounds.x + 16.0f, bounds.y + 18.0f},
1636 20.0f,
1637 0.1f,
1638 p_theme->text);
1639 for (int32 column = 0; column < 7; column++) {
1640 float cell_x = bounds.x + 14.0f + (float)column * 36.0f;
1641 DrawTextEx(
1642 font,
1643 calendar_weekdays[column],
1644 (Vector2){cell_x + 12.0f, bounds.y + 56.0f},
1645 12.0f,
1646 0.0f,
1647 p_theme->text_muted);
1648 }
1649 for (int32 day = 1; day <= 31; day++) {
1650 int32 cell = day + 6 - 1;
1651 int32 row = cell / 7;
1652 int32 column = cell % 7;
1653 Vector2 center = {
1654 bounds.x + 14.0f + (float)column * 36.0f + 18.0f,
1655 bounds.y + 82.0f + (float)row * 32.0f + 16.0f,
1656 };
1657 if (day == p_entity->value) {
1658 DrawCircleV(center, 14.0f, p_theme->accent);
1659 }
1660 const char *label = TextFormat("%d", day);
1661 Vector2 label_size = MeasureTextEx(font, label, 13.0f, 0.0f);
1662 DrawTextEx(
1663 font,
1664 label,
1665 (Vector2){center.x - label_size.x * 0.5f,
1666 center.y - label_size.y * 0.5f},
1667 13.0f,
1668 0.0f,
1669 day == p_entity->value ? WHITE : p_theme->text);
1670 }
1671 break;
1672 }
1673 case CANVAS_ENTITY_SWITCH: {
1674 float amount = p_entity->animation_amount;
1675 float eased = amount * amount * (3.0f - 2.0f * amount);
1676 Rectangle bounds = {
1677 p_entity->position.x,
1678 p_entity->position.y,
1679 p_entity->size.x,
1680 p_entity->size.y,
1681 };
1682 Color track = Canvas_Color_Lerp(
1683 p_theme->border,
1684 p_theme->accent,
1685 eased);
1686 if (!p_entity->active) {
1687 track = Canvas_Color_Lerp(
1688 track,
1689 p_theme->text_muted,
1690 p_entity->hover_amount);
1691 }
1692 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.50f, 18, zoom, 1.0f);
1693 DrawRectangleRounded(bounds, 0.50f, 18, track);
1694 float knob_x = bounds.x + 18.0f +
1695 eased * (bounds.width - 36.0f);
1696 float knob_radius = pressed ? 12.5f : 14.0f;
1697 DrawCircleV(
1698 (Vector2){knob_x, bounds.y + bounds.height * 0.5f},
1699 knob_radius,
1700 WHITE);
1701 break;
1702 }
1703 case CANVAS_ENTITY_TABLE: {
1704 Rectangle bounds = {
1705 p_entity->position.x,
1706 p_entity->position.y,
1707 p_entity->size.x,
1708 p_entity->size.y,
1709 };
1710 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.06f, 12, zoom, 1.0f);
1711 DrawRectangleRounded(bounds, 0.06f, 12, p_theme->surface);
1712 DrawRectangleRoundedLinesEx(
1713 bounds,
1714 0.06f,
1715 12,
1716 1.0f / zoom,
1717 p_theme->border);
1718 DrawTextEx(
1719 font,
1720 "Name",
1721 (Vector2){bounds.x + 16.0f, bounds.y + 22.0f},
1722 13.0f,
1723 0.0f,
1724 p_theme->text_muted);
1725 DrawTextEx(
1726 font,
1727 "Status",
1728 (Vector2){bounds.x + 292.0f, bounds.y + 22.0f},
1729 13.0f,
1730 0.0f,
1731 p_theme->text_muted);
1732 DrawLineEx(
1733 (Vector2){bounds.x, bounds.y + 52.0f},
1734 (Vector2){bounds.x + bounds.width, bounds.y + 52.0f},
1735 1.0f / zoom,
1736 p_theme->border);
1737 for (int32 row = 0; row < 4; row++) {
1738 float row_y = bounds.y + 62.0f + (float)row * 38.0f;
1739 if (row == p_entity->value) {
1740 DrawRectangle(
1741 (int32)(bounds.x + 8.0f),
1742 (int32)(row_y - 4.0f),
1743 (int32)(bounds.width - 16.0f),
1744 34,
1745 p_theme->accent_soft);
1746 }
1747 DrawTextEx(
1748 font,
1749 CANVAS_TABLE_NAMES[row],
1750 (Vector2){bounds.x + 16.0f, row_y + 5.0f},
1751 14.0f,
1752 0.0f,
1753 p_theme->text);
1754 DrawTextEx(
1755 font,
1756 CANVAS_TABLE_STATUSES[row],
1757 (Vector2){bounds.x + 292.0f, row_y + 5.0f},
1758 14.0f,
1759 0.0f,
1760 row == p_entity->value ?
1761 p_theme->accent :
1762 p_theme->text_muted);
1763 }
1764 break;
1765 }
1766 case CANVAS_ENTITY_NOTIFICATION: {
1767 if (p_entity->animation_amount <= 0.01f) break;
1768 float unit = 1.0f / zoom;
1769 Rectangle bounds = {
1770 p_entity->position.x,
1771 p_entity->position.y,
1772 p_entity->size.x,
1773 p_entity->size.y,
1774 };
1775 float opacity = p_entity->animation_amount;
1776 Canvas_Theme_Draw_Shadow(
1777 p_theme,
1778 bounds,
1779 0.20f,
1780 16,
1781 zoom,
1782 opacity);
1783 DrawRectangleRounded(
1784 bounds,
1785 0.20f,
1786 16,
1787 Fade(p_theme->toast_surface, opacity));
1788 DrawRectangleRoundedLinesEx(
1789 bounds,
1790 0.20f,
1791 16,
1792 1.0f / zoom,
1793 Fade(p_theme->toast_border, opacity));
1794 DrawTextEx(
1795 font,
1796 p_entity->text,
1797 (Vector2){bounds.x + 16.0f * unit, bounds.y + 18.0f * unit},
1798 15.0f * unit,
1799 0.0f,
1800 Fade(p_theme->text, opacity));
1801 DrawTextEx(
1802 font,
1803 "Sunday, December 3 at 9:00 AM",
1804 (Vector2){bounds.x + 16.0f * unit, bounds.y + 45.0f * unit},
1805 14.0f * unit,
1806 0.0f,
1807 Fade(p_theme->text_muted, opacity));
1808 Rectangle undo = {
1809 bounds.x + bounds.width - 108.0f * unit,
1810 bounds.y + 24.0f * unit,
1811 54.0f * unit,
1812 34.0f * unit,
1813 };
1814 DrawRectangleRounded(
1815 undo,
1816 0.26f,
1817 10,
1818 Fade(p_theme->surface_muted, opacity));
1819 DrawRectangleRoundedLinesEx(
1820 undo,
1821 0.26f,
1822 10,
1823 1.0f / zoom,
1824 Fade(p_theme->border, opacity));
1825 DrawTextEx(
1826 font,
1827 "Undo",
1828 (Vector2){undo.x + 12.0f * unit, undo.y + 10.0f * unit},
1829 13.0f * unit,
1830 0.0f,
1831 Fade(p_theme->text, opacity));
1832 Vector2 close = {
1833 bounds.x + bounds.width - 24.0f * unit,
1834 bounds.y + bounds.height * 0.5f,
1835 };
1836 DrawLineEx(
1837 (Vector2){close.x - 4.0f * unit, close.y - 4.0f * unit},
1838 (Vector2){close.x + 4.0f * unit, close.y + 4.0f * unit},
1839 1.5f / zoom,
1840 Fade(p_theme->text_muted, opacity));
1841 DrawLineEx(
1842 (Vector2){close.x + 4.0f * unit, close.y - 4.0f * unit},
1843 (Vector2){close.x - 4.0f * unit, close.y + 4.0f * unit},
1844 1.5f / zoom,
1845 Fade(p_theme->text_muted, opacity));
1846 break;
1847 }
1848 case CANVAS_ENTITY_SCROLL_AREA: {
1849 Rectangle bounds = {
1850 p_entity->position.x,
1851 p_entity->position.y,
1852 p_entity->size.x,
1853 p_entity->size.y,
1854 };
1855 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 12, zoom, 1.0f);
1856 DrawRectangleRounded(bounds, 0.08f, 12, p_theme->surface);
1857 DrawRectangleRoundedLinesEx(
1858 bounds,
1859 0.08f,
1860 12,
1861 1.0f / zoom,
1862 p_theme->border);
1863 DrawTextEx(
1864 font,
1865 "Scrollable area",
1866 (Vector2){bounds.x + 16.0f, bounds.y + 16.0f},
1867 18.0f,
1868 0.0f,
1869 p_theme->text);
1870 for (int32 item = 0; item < 10; item++) {
1871 float item_y =
1872 bounds.y + 52.0f + (float)item * 42.0f - (float)p_entity->value;
1873 if (item_y < bounds.y + 46.0f ||
1874 item_y + 34.0f > bounds.y + bounds.height - 12.0f) {
1875 continue;
1876 }
1877 DrawRectangleRounded(
1878 (Rectangle){bounds.x + 14.0f, item_y,
1879 bounds.width - 38.0f, 34.0f},
1880 0.16f,
1881 8,
1882 item % 2 == 0 ?
1883 p_theme->surface_muted :
1884 Canvas_Color_Lerp(p_theme->surface, p_theme->surface_muted, 0.5f));
1885 DrawTextEx(
1886 font,
1887 TextFormat("Item %d", item + 1),
1888 (Vector2){bounds.x + 26.0f, item_y + 9.0f},
1889 13.0f,
1890 0.0f,
1891 p_theme->text);
1892 }
1893 float thumb_y = bounds.y + 52.0f +
1894 ((float)p_entity->value / 240.0f) * (bounds.height - 106.0f);
1895 DrawRectangleRounded(
1896 (Rectangle){bounds.x + bounds.width - 14.0f, thumb_y, 5.0f, 42.0f},
1897 0.50f,
1898 6,
1899 Fade(p_theme->text_muted, 0.82f));
1900 break;
1901 }
1902 case CANVAS_ENTITY_IMAGE: {
1903 Rectangle bounds = {
1904 p_entity->position.x,
1905 p_entity->position.y,
1906 p_entity->size.x,
1907 p_entity->size.y,
1908 };
1909 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 12, zoom, 1.0f);
1910 DrawRectangleRounded(bounds, 0.04f, 12, p_theme->surface_muted);
1911 DrawRectangleRoundedLinesEx(
1912 bounds,
1913 0.04f,
1914 12,
1915 1.0f / zoom,
1916 p_theme->border);
1917 DrawTextEx(
1918 font,
1919 "Loading image...",
1920 (Vector2){bounds.x + 16.0f, bounds.y + 16.0f},
1921 14.0f,
1922 0.0f,
1923 p_theme->text_muted);
1924 break;
1925 }
1926 case CANVAS_ENTITY_WEB_CONTENT: {
1927 Rectangle bounds = {
1928 p_entity->position.x,
1929 p_entity->position.y,
1930 p_entity->size.x,
1931 p_entity->size.y,
1932 };
1933 Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 14, zoom, 1.0f);
1934 DrawRectangleRounded(bounds, 0.04f, 14, p_theme->surface_muted);
1935 DrawRectangleRoundedLinesEx(
1936 bounds,
1937 0.04f,
1938 14,
1939 1.0f / zoom,
1940 p_theme->border);
1941 #if !defined(PLATFORM_WEB) && !defined(__EMSCRIPTEN__)
1942 Vector2 label_size = MeasureTextEx(
1943 font,
1944 "Loading browser surface...",
1945 18.0f,
1946 0.1f);
1947 DrawTextEx(
1948 font,
1949 "Loading browser surface...",
1950 (Vector2){
1951 bounds.x + (bounds.width - label_size.x) * 0.5f,
1952 bounds.y + (bounds.height - label_size.y) * 0.5f,
1953 },
1954 18.0f,
1955 0.1f,
1956 p_theme->text_muted);
1957 #endif
1958 break;
1959 }
1960 default:
1961 break;
1962 }
1963
1964 if (p_entity->pinned) {
1965 float radius = 7.0f / zoom;
1966 Vector2 center = {
1967 p_entity->position.x + p_entity->size.x - 10.0f / zoom,
1968 p_entity->position.y + 10.0f / zoom,
1969 };
1970 DrawCircleV(center, radius, p_theme->accent);
1971 DrawLineEx(
1972 (Vector2){center.x, center.y - 3.5f / zoom},
1973 (Vector2){center.x, center.y + 4.0f / zoom},
1974 1.5f / zoom,
1975 WHITE);
1976 }
1977 if (selected && p_entity->type != CANVAS_ENTITY_NOTIFICATION) {
1978 Canvas_Draw_Selection(p_entity, zoom);
1979 }
1980 }
1981
1982 static void Canvas_Draw_Dropdown_Menu(
1983 const Canvas_Entity *p_entity,
1984 Font font,
1985 Vector2 world_pointer,
1986 float zoom,
1987 const Canvas_Theme *p_theme)
1988 {
1989 static const char *options[] = {
1990 "Design",
1991 "Engineering",
1992 "Research",
1993 };
1994 Rectangle menu = {
1995 p_entity->position.x,
1996 p_entity->position.y + p_entity->size.y + 8.0f,
1997 p_entity->size.x,
1998 120.0f,
1999 };
2000 Canvas_Theme_Draw_Shadow(p_theme, menu, 0.12f, 14, zoom, 1.0f);
2001 DrawRectangleRounded(menu, 0.12f, 14, p_theme->surface);
2002 DrawRectangleRoundedLinesEx(
2003 menu,
2004 0.12f,
2005 14,
2006 1.0f / zoom,
2007 p_theme->border);
2008
2009 for (int32 option = 0; option < 3; option++) {
2010 Rectangle row = {
2011 menu.x + 5.0f,
2012 menu.y + 4.0f + (float)option * 38.0f,
2013 menu.width - 10.0f,
2014 36.0f,
2015 };
2016 boolean hovered = CheckCollisionPointRec(world_pointer, row) ? TRUE : FALSE;
2017 boolean selected = p_entity->value == option;
2018 if (hovered || selected) {
2019 DrawRectangleRounded(
2020 row,
2021 0.20f,
2022 10,
2023 hovered ? p_theme->accent_soft : p_theme->surface_muted);
2024 }
2025 DrawTextEx(
2026 font,
2027 options[option],
2028 (Vector2){row.x + 12.0f, row.y + 9.0f},
2029 15.0f,
2030 0.1f,
2031 selected ? p_theme->accent : p_theme->text);
2032 if (selected) {
2033 DrawCircleV(
2034 (Vector2){row.x + row.width - 14.0f, row.y + row.height * 0.5f},
2035 3.0f,
2036 p_theme->accent);
2037 }
2038 }
2039 }
2040
2041 void Canvas_Draw_World(
2042 const Canvas_Camera *p_camera,
2043 const Canvas_Scene *p_scene,
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 = {
2050 (float)p_camera->viewport_width * 0.5f,
2051 (float)p_camera->viewport_height * 0.5f,
2052 },
2053 .target = p_camera->target,
2054 .rotation = 0.0f,
2055 .zoom = p_camera->zoom,
2056 };
2057
2058 BeginMode2D(camera);
2059 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++) {
2061 Canvas_Draw_Entity(
2062 &p_scene->p_entities[index],
2063 font,
2064 p_camera->zoom,
2065 (int32)index == p_scene->selected_index,
2066 (int32)index == p_scene->pressed_index && IsMouseButtonDown(MOUSE_BUTTON_LEFT),
2067 p_theme);
2068 }
2069 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];
2071 if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) {
2072 Canvas_Draw_Dropdown_Menu(
2073 p_entity,
2074 font,
2075 world_pointer,
2076 p_camera->zoom,
2077 p_theme);
2078 }
2079 }
2080 EndMode2D();
2081 }
2082
2083 const char *Canvas_Entity_Type_Name(Canvas_Entity_Type type)
2084 {
2085 switch (type) {
2086 case CANVAS_ENTITY_RECTANGLE: return "Rectangle";
2087 case CANVAS_ENTITY_CIRCLE: return "Circle";
2088 case CANVAS_ENTITY_LINE: return "Line";
2089 case CANVAS_ENTITY_TEXT: return "Text";
2090 case CANVAS_ENTITY_BUTTON: return "Button";
2091 case CANVAS_ENTITY_TEXT_AREA: return "Text area";
2092 case CANVAS_ENTITY_DROPDOWN: return "Dropdown";
2093 case CANVAS_ENTITY_ACCORDION: return "Accordion";
2094 case CANVAS_ENTITY_CARD: return "Card";
2095 case CANVAS_ENTITY_CALENDAR: return "Calendar";
2096 case CANVAS_ENTITY_SWITCH: return "Switch";
2097 case CANVAS_ENTITY_TABLE: return "Table";
2098 case CANVAS_ENTITY_NOTIFICATION: return "Notification";
2099 case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area";
2100 case CANVAS_ENTITY_IMAGE: return "Image";
2101 case CANVAS_ENTITY_WEB_CONTENT: return "Web content";
2102 default: return "Unknown";
2103 }
2104 }