Mercurial
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 } |