Mercurial
comparison infinite_canvas/canvas_test.c @ 278:8d560f50ed4c
Improve infinite canvas interactions and browser chrome
Render Lucide icons directly with Raylib, add searchable icon browsing, robust text editing, entity lifecycle animations, z-order-safe input, semantic themes, and animated editable browser controls. Document rendering, pinning, context, and component extension for future agents.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:16:14 -0700 |
| parents | b55c22cff335 |
| children | 49e9e591c9bb |
comparison
equal
deleted
inserted
replaced
| 277:1d99147f520c | 278:8d560f50ed4c |
|---|---|
| 63 assert(light.background.r == 255); | 63 assert(light.background.r == 255); |
| 64 assert(light.toast_surface.r == 255); | 64 assert(light.toast_surface.r == 255); |
| 65 assert(dark.resolved_mode == CANVAS_THEME_DARK); | 65 assert(dark.resolved_mode == CANVAS_THEME_DARK); |
| 66 assert(dark.background.r == 15); | 66 assert(dark.background.r == 15); |
| 67 assert(dark.toast_surface.r == 24); | 67 assert(dark.toast_surface.r == 24); |
| 68 assert(light.switch_track_on.g > light.switch_track_on.b); | |
| 69 assert(dark.switch_track_on.g > dark.switch_track_on.b); | |
| 70 assert(light.switch_track_on.r != light.accent.r); | |
| 71 assert(dark.switch_track_on.r != dark.accent.r); | |
| 72 assert(light.pin.r > light.pin.b); | |
| 73 assert(dark.pin.r > dark.pin.b); | |
| 74 assert(light.pin.r != light.accent.r); | |
| 75 assert(dark.pin.r != dark.accent.r); | |
| 76 assert(light.button.r != dark.button.r); | |
| 77 assert(light.selection.r != dark.selection.r); | |
| 78 assert(light.danger.r != dark.danger.r); | |
| 68 assert(Canvas_Theme_Mode_From_String("light") == CANVAS_THEME_LIGHT); | 79 assert(Canvas_Theme_Mode_From_String("light") == CANVAS_THEME_LIGHT); |
| 69 assert(Canvas_Theme_Mode_From_String("dark") == CANVAS_THEME_DARK); | 80 assert(Canvas_Theme_Mode_From_String("dark") == CANVAS_THEME_DARK); |
| 70 assert(Canvas_Theme_Mode_From_String("invalid") == CANVAS_THEME_AUTO); | 81 assert(Canvas_Theme_Mode_From_String("invalid") == CANVAS_THEME_AUTO); |
| 71 | 82 |
| 72 Canvas_Theme_Toggle(&light); | 83 Canvas_Theme_Toggle(&light); |
| 155 assert(Canvas_Scene_Pick(&scene, (Vector2){5500.0f, 500.0f}, 1.0f) == -1); | 166 assert(Canvas_Scene_Pick(&scene, (Vector2){5500.0f, 500.0f}, 1.0f) == -1); |
| 156 | 167 |
| 157 Dowa_Arena_Free(p_arena); | 168 Dowa_Arena_Free(p_arena); |
| 158 } | 169 } |
| 159 | 170 |
| 171 static void Test_Overlapping_Entities_Use_Topmost_Z_Order(void) | |
| 172 { | |
| 173 Dowa_Arena *p_arena = Dowa_Arena_Create(4 * ONE_MEGA_BYTE); | |
| 174 assert(p_arena); | |
| 175 | |
| 176 Canvas_Scene scene; | |
| 177 Canvas_Scene_Init(&scene, p_arena); | |
| 178 Vector2 position = {0.0f, 0.0f}; | |
| 179 Vector2 pointer = {40.0f, 30.0f}; | |
| 180 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_BUTTON, position)); | |
| 181 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_BUTTON, position)); | |
| 182 assert(Canvas_Scene_Pick(&scene, pointer, 1.0f) == 1); | |
| 183 assert(Canvas_Scene_Topmost_Visual(&scene, pointer, 1.0f) == 1); | |
| 184 | |
| 185 scene.p_entities[1].removing = TRUE; | |
| 186 assert(Canvas_Scene_Pick(&scene, pointer, 1.0f) == 0); | |
| 187 assert(Canvas_Scene_Topmost_Visual(&scene, pointer, 1.0f) == 0); | |
| 188 | |
| 189 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, position)); | |
| 190 assert(Canvas_Scene_Topmost_Visual(&scene, pointer, 1.0f) == 2); | |
| 191 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_BUTTON, position)); | |
| 192 assert(Canvas_Scene_Pick(&scene, pointer, 1.0f) == 3); | |
| 193 assert(Canvas_Scene_Topmost_Visual(&scene, pointer, 1.0f) == 3); | |
| 194 | |
| 195 Dowa_Arena_Free(p_arena); | |
| 196 } | |
| 197 | |
| 160 static void Test_New_Primitive_Defaults(void) | 198 static void Test_New_Primitive_Defaults(void) |
| 161 { | 199 { |
| 162 Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE); | 200 Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE); |
| 163 assert(p_arena); | 201 assert(p_arena); |
| 164 | 202 |
| 204 | 242 |
| 205 Canvas_Scene scene; | 243 Canvas_Scene scene; |
| 206 Canvas_Scene_Init(&scene, p_arena); | 244 Canvas_Scene_Init(&scene, p_arena); |
| 207 assert(!Canvas_Scene_Is_Text_Editing(&scene)); | 245 assert(!Canvas_Scene_Is_Text_Editing(&scene)); |
| 208 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_TEXT_AREA, (Vector2){0.0f, 0.0f})); | 246 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_TEXT_AREA, (Vector2){0.0f, 0.0f})); |
| 247 assert(sizeof(scene.p_entities[0].text) == CANVAS_ENTITY_TEXT_CAPACITY); | |
| 248 assert(scene.p_entities[0].text_cursor == 0); | |
| 249 assert(scene.p_entities[0].text_selection_anchor == 0); | |
| 209 | 250 |
| 210 scene.selected_index = 0; | 251 scene.selected_index = 0; |
| 211 assert(!Canvas_Scene_Is_Text_Editing(&scene)); | 252 assert(!Canvas_Scene_Is_Text_Editing(&scene)); |
| 212 scene.p_entities[0].active = TRUE; | 253 scene.p_entities[0].active = TRUE; |
| 213 assert(Canvas_Scene_Is_Text_Editing(&scene)); | 254 assert(Canvas_Scene_Is_Text_Editing(&scene)); |
| 227 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){0.0f, 0.0f})); | 268 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){0.0f, 0.0f})); |
| 228 uint32 first_id = scene.p_entities[0].id; | 269 uint32 first_id = scene.p_entities[0].id; |
| 229 Canvas_Scene_Clear(&scene); | 270 Canvas_Scene_Clear(&scene); |
| 230 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){0.0f, 0.0f})); | 271 assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){0.0f, 0.0f})); |
| 231 assert(scene.p_entities[0].id != first_id); | 272 assert(scene.p_entities[0].id != first_id); |
| 273 | |
| 274 Dowa_Arena_Free(p_arena); | |
| 275 } | |
| 276 | |
| 277 static void Test_Entity_Fade_Lifecycle_Commands(void) | |
| 278 { | |
| 279 Dowa_Arena *p_arena = Dowa_Arena_Create(4 * ONE_MEGA_BYTE); | |
| 280 assert(p_arena); | |
| 281 | |
| 282 Canvas_Scene scene; | |
| 283 Canvas_Scene_Init(&scene, p_arena); | |
| 284 assert(Canvas_Scene_Add( | |
| 285 &scene, | |
| 286 CANVAS_ENTITY_RECTANGLE, | |
| 287 (Vector2){0.0f, 0.0f})); | |
| 288 assert(Canvas_Scene_Add( | |
| 289 &scene, | |
| 290 CANVAS_ENTITY_TEXT, | |
| 291 (Vector2){200.0f, 0.0f})); | |
| 292 assert(Near(scene.p_entities[0].visibility_amount, 0.0f)); | |
| 293 assert(!scene.p_entities[0].removing); | |
| 294 | |
| 295 scene.selected_index = 0; | |
| 296 Canvas_Scene_Fade_Out_Selected(&scene); | |
| 297 assert(scene.p_entities[0].removing); | |
| 298 assert(scene.selected_index == -1); | |
| 299 assert(!scene.p_entities[1].removing); | |
| 300 | |
| 301 Canvas_Scene_Fade_Out_All(&scene); | |
| 302 assert(scene.p_entities[0].removing); | |
| 303 assert(scene.p_entities[1].removing); | |
| 232 | 304 |
| 233 Dowa_Arena_Free(p_arena); | 305 Dowa_Arena_Free(p_arena); |
| 234 } | 306 } |
| 235 | 307 |
| 236 static void Test_Browser_Grid(void) | 308 static void Test_Browser_Grid(void) |
| 249 } | 321 } |
| 250 | 322 |
| 251 Dowa_Arena_Free(p_arena); | 323 Dowa_Arena_Free(p_arena); |
| 252 } | 324 } |
| 253 | 325 |
| 326 static void Test_Lucide_Gallery_Reuses_Entity_And_Fits_Camera(void) | |
| 327 { | |
| 328 Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE); | |
| 329 assert(p_arena); | |
| 330 | |
| 331 Canvas_Camera camera; | |
| 332 Canvas_Camera_Init(&camera, 1920, 1200); | |
| 333 Canvas_Scene scene; | |
| 334 Canvas_Scene_Init(&scene, p_arena); | |
| 335 | |
| 336 assert(Canvas_Scene_Show_Lucide_Gallery(&scene, &camera)); | |
| 337 assert(Dowa_Array_Length(scene.p_entities) == 1); | |
| 338 assert(scene.selected_index == 0); | |
| 339 Canvas_Entity *p_gallery = &scene.p_entities[0]; | |
| 340 assert(p_gallery->type == CANVAS_ENTITY_LUCIDE_GALLERY); | |
| 341 assert(strcmp(p_gallery->text, "") == 0); | |
| 342 assert(Near(p_gallery->size.x, 1440.0f)); | |
| 343 assert(Near(p_gallery->size.y, 1040.0f)); | |
| 344 | |
| 345 Vector2 screen_min = | |
| 346 Canvas_Camera_World_To_Screen(&camera, p_gallery->position); | |
| 347 Vector2 screen_max = Canvas_Camera_World_To_Screen( | |
| 348 &camera, | |
| 349 (Vector2){ | |
| 350 p_gallery->position.x + p_gallery->size.x, | |
| 351 p_gallery->position.y + p_gallery->size.y, | |
| 352 }); | |
| 353 assert(screen_min.x >= 319.0f); | |
| 354 assert(screen_min.y >= 55.0f); | |
| 355 assert(screen_max.x <= 1865.0f); | |
| 356 assert(screen_max.y <= 1145.0f); | |
| 357 | |
| 358 assert(Canvas_Scene_Show_Lucide_Gallery(&scene, &camera)); | |
| 359 assert(Dowa_Array_Length(scene.p_entities) == 1); | |
| 360 | |
| 361 Dowa_Arena_Free(p_arena); | |
| 362 } | |
| 363 | |
| 364 static void Test_Lucide_Search_Is_Case_Insensitive(void) | |
| 365 { | |
| 366 assert(Canvas_Lucide_Count_Matches("") == 1767); | |
| 367 assert(Canvas_Lucide_Count_Matches("pin") > 0); | |
| 368 assert(Canvas_Lucide_Count_Matches("PIN") == | |
| 369 Canvas_Lucide_Count_Matches("pin")); | |
| 370 assert(Canvas_Lucide_Count_Matches("not-a-real-lucide-icon") == 0); | |
| 371 } | |
| 372 | |
| 254 static void Test_Demo_Contains_Every_Entity_Type(void) | 373 static void Test_Demo_Contains_Every_Entity_Type(void) |
| 255 { | 374 { |
| 256 Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE); | 375 Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE); |
| 257 assert(p_arena); | 376 assert(p_arena); |
| 258 | 377 |
| 259 Canvas_Scene scene; | 378 Canvas_Scene scene; |
| 260 Canvas_Scene_Init(&scene, p_arena); | 379 Canvas_Scene_Init(&scene, p_arena); |
| 261 Canvas_Scene_Add_Demo(&scene); | 380 Canvas_Scene_Add_Demo(&scene); |
| 262 assert(Dowa_Array_Length(scene.p_entities) == CANVAS_ENTITY_TYPE_COUNT + 2); | 381 assert(Dowa_Array_Length(scene.p_entities) == CANVAS_ENTITY_TYPE_COUNT + 1); |
| 263 | 382 |
| 264 int32 counts[CANVAS_ENTITY_TYPE_COUNT] = {0}; | 383 int32 counts[CANVAS_ENTITY_TYPE_COUNT] = {0}; |
| 265 for (size_t index = 0; index < Dowa_Array_Length(scene.p_entities); index++) { | 384 for (size_t index = 0; index < Dowa_Array_Length(scene.p_entities); index++) { |
| 266 counts[scene.p_entities[index].type]++; | 385 counts[scene.p_entities[index].type]++; |
| 267 } | 386 } |
| 268 for (int32 type = 0; type < CANVAS_ENTITY_TYPE_COUNT; type++) { | 387 for (int32 type = 0; type < CANVAS_ENTITY_TYPE_COUNT; type++) { |
| 388 if (type == CANVAS_ENTITY_LUCIDE_GALLERY) { | |
| 389 assert(counts[type] == 0); | |
| 390 continue; | |
| 391 } | |
| 269 int32 expected = type == CANVAS_ENTITY_NOTIFICATION ? 3 : 1; | 392 int32 expected = type == CANVAS_ENTITY_NOTIFICATION ? 3 : 1; |
| 270 assert(counts[type] == expected); | 393 assert(counts[type] == expected); |
| 271 } | 394 } |
| 272 for (size_t index = 0; index < Dowa_Array_Length(scene.p_entities); index++) { | 395 for (size_t index = 0; index < Dowa_Array_Length(scene.p_entities); index++) { |
| 273 if (scene.p_entities[index].type == CANVAS_ENTITY_NOTIFICATION) continue; | 396 if (scene.p_entities[index].type == CANVAS_ENTITY_NOTIFICATION) continue; |
| 346 CANVAS_ENTITY_SWITCH, | 469 CANVAS_ENTITY_SWITCH, |
| 347 (Vector2){700.0f, 0.0f})); | 470 (Vector2){700.0f, 0.0f})); |
| 348 | 471 |
| 349 snprintf(scene.p_entities[0].text, sizeof(scene.p_entities[0].text), "Visible note"); | 472 snprintf(scene.p_entities[0].text, sizeof(scene.p_entities[0].text), "Visible note"); |
| 350 scene.p_entities[0].active = TRUE; | 473 scene.p_entities[0].active = TRUE; |
| 474 scene.p_entities[0].text_cursor = 7; | |
| 475 scene.p_entities[0].text_selection_anchor = 2; | |
| 351 scene.p_entities[1].value = 2; | 476 scene.p_entities[1].value = 2; |
| 352 scene.p_entities[1].active = TRUE; | 477 scene.p_entities[1].active = TRUE; |
| 353 scene.p_entities[2].active = TRUE; | 478 scene.p_entities[2].active = TRUE; |
| 354 | 479 |
| 355 char context[4096]; | 480 char context[4096]; |
| 360 sizeof(context)); | 485 sizeof(context)); |
| 361 assert(snapshot.visible_count == 1); | 486 assert(snapshot.visible_count == 1); |
| 362 assert(!snapshot.truncated); | 487 assert(!snapshot.truncated); |
| 363 assert(strstr(context, "Visible note")); | 488 assert(strstr(context, "Visible note")); |
| 364 assert(strstr(context, "editing=true")); | 489 assert(strstr(context, "editing=true")); |
| 490 assert(strstr(context, "cursor=7")); | |
| 491 assert(strstr(context, "selection_anchor=2")); | |
| 365 assert(!strstr(context, "Research")); | 492 assert(!strstr(context, "Research")); |
| 366 assert(!strstr(context, "checked=true")); | 493 assert(!strstr(context, "checked=true")); |
| 367 | 494 |
| 368 camera.zoom = 0.25f; | 495 camera.zoom = 0.25f; |
| 369 snapshot = Canvas_Scene_Build_Visible_Context( | 496 snapshot = Canvas_Scene_Build_Visible_Context( |
| 405 Test_Camera_Round_Trip(); | 532 Test_Camera_Round_Trip(); |
| 406 Test_Anchored_Zoom(); | 533 Test_Anchored_Zoom(); |
| 407 Test_Viewport_Bounds(); | 534 Test_Viewport_Bounds(); |
| 408 Test_Grid_Spacing(); | 535 Test_Grid_Spacing(); |
| 409 Test_Entity_Picking(); | 536 Test_Entity_Picking(); |
| 537 Test_Overlapping_Entities_Use_Topmost_Z_Order(); | |
| 410 Test_New_Primitive_Defaults(); | 538 Test_New_Primitive_Defaults(); |
| 411 Test_Text_Editing_Focus(); | 539 Test_Text_Editing_Focus(); |
| 412 Test_Entity_Ids_Remain_Unique_After_Clear(); | 540 Test_Entity_Ids_Remain_Unique_After_Clear(); |
| 541 Test_Entity_Fade_Lifecycle_Commands(); | |
| 413 Test_Browser_Grid(); | 542 Test_Browser_Grid(); |
| 543 Test_Lucide_Gallery_Reuses_Entity_And_Fits_Camera(); | |
| 544 Test_Lucide_Search_Is_Case_Insensitive(); | |
| 414 Test_Demo_Contains_Every_Entity_Type(); | 545 Test_Demo_Contains_Every_Entity_Type(); |
| 415 Test_Pinned_Entity_Stays_In_Screen_Space(); | 546 Test_Pinned_Entity_Stays_In_Screen_Space(); |
| 416 Test_Visible_Context_Tracks_Camera_And_Values(); | 547 Test_Visible_Context_Tracks_Camera_And_Values(); |
| 417 printf("Infinite canvas tests passed.\n"); | 548 printf("Infinite canvas tests passed.\n"); |
| 418 return 0; | 549 return 0; |