Mercurial
comparison infinite_canvas/canvas.c @ 280:49e9e591c9bb
Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 19:14:53 -0700 |
| parents | 8d560f50ed4c |
| children | c57149ad216e |
comparison
equal
deleted
inserted
replaced
| 279:b3b547563ec7 | 280:49e9e591c9bb |
|---|---|
| 36 #define CANVAS_LUCIDE_SEARCH_HEIGHT 42.0f | 36 #define CANVAS_LUCIDE_SEARCH_HEIGHT 42.0f |
| 37 #define CANVAS_TEXT_AREA_PADDING 16.0f | 37 #define CANVAS_TEXT_AREA_PADDING 16.0f |
| 38 #define CANVAS_TEXT_AREA_FONT_SIZE 16.0f | 38 #define CANVAS_TEXT_AREA_FONT_SIZE 16.0f |
| 39 #define CANVAS_TEXT_AREA_SPACING 0.1f | 39 #define CANVAS_TEXT_AREA_SPACING 0.1f |
| 40 #define CANVAS_TEXT_AREA_LINE_HEIGHT 22.0f | 40 #define CANVAS_TEXT_AREA_LINE_HEIGHT 22.0f |
| 41 #define CANVAS_CONVERSATION_EXPANDED_HEIGHT 300.0f | |
| 42 #define CANVAS_CONVERSATION_COLLAPSED_HEIGHT 58.0f | |
| 43 #define CANVAS_PARKING_LOT_X 2400.0f | |
| 44 #define CANVAS_PARKING_LOT_Y -600.0f | |
| 41 | 45 |
| 42 typedef struct { | 46 typedef struct { |
| 43 int32 start; | 47 int32 start; |
| 44 int32 end; | 48 int32 end; |
| 45 int32 next; | 49 int32 next; |
| 46 float width; | 50 float width; |
| 47 } Canvas_Text_Line; | 51 } Canvas_Text_Line; |
| 52 | |
| 53 static int32 Canvas_Text_Build_Lines( | |
| 54 Font font, | |
| 55 const char *p_text, | |
| 56 float max_width, | |
| 57 Canvas_Text_Line *p_lines, | |
| 58 int32 line_capacity); | |
| 48 | 59 |
| 49 static float Canvas_Clamp(float value, float min_value, float max_value) | 60 static float Canvas_Clamp(float value, float min_value, float max_value) |
| 50 { | 61 { |
| 51 if (value < min_value) return min_value; | 62 if (value < min_value) return min_value; |
| 52 if (value > max_value) return max_value; | 63 if (value > max_value) return max_value; |
| 741 world_center.y - | 752 world_center.y - |
| 742 (desired_screen_center.y - viewport_center.y) / p_camera->zoom, | 753 (desired_screen_center.y - viewport_center.y) / p_camera->zoom, |
| 743 }; | 754 }; |
| 744 } | 755 } |
| 745 | 756 |
| 757 static Rectangle Canvas_Conversation_Collapse_Bounds( | |
| 758 const Canvas_Entity *p_entity) | |
| 759 { | |
| 760 return (Rectangle){ | |
| 761 p_entity->position.x + p_entity->size.x - 74.0f, | |
| 762 p_entity->position.y + 12.0f, | |
| 763 28.0f, | |
| 764 28.0f, | |
| 765 }; | |
| 766 } | |
| 767 | |
| 768 static Rectangle Canvas_Conversation_Park_Bounds( | |
| 769 const Canvas_Entity *p_entity) | |
| 770 { | |
| 771 return (Rectangle){ | |
| 772 p_entity->position.x + p_entity->size.x - 40.0f, | |
| 773 p_entity->position.y + 12.0f, | |
| 774 28.0f, | |
| 775 28.0f, | |
| 776 }; | |
| 777 } | |
| 778 | |
| 746 static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity) | 779 static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity) |
| 747 { | 780 { |
| 748 if (p_entity->type == CANVAS_ENTITY_CIRCLE) { | 781 if (p_entity->type == CANVAS_ENTITY_CIRCLE) { |
| 749 return (Rectangle){ | 782 return (Rectangle){ |
| 750 p_entity->position.x - p_entity->size.x, | 783 p_entity->position.x - p_entity->size.x, |
| 751 p_entity->position.y - p_entity->size.x, | 784 p_entity->position.y - p_entity->size.x, |
| 752 p_entity->size.x * 2.0f, | 785 p_entity->size.x * 2.0f, |
| 753 p_entity->size.x * 2.0f, | 786 p_entity->size.x * 2.0f, |
| 754 }; | 787 }; |
| 755 } | 788 } |
| 789 | |
| 756 if (p_entity->type == CANVAS_ENTITY_LINE) { | 790 if (p_entity->type == CANVAS_ENTITY_LINE) { |
| 757 float end_x = p_entity->position.x + p_entity->size.x; | 791 float end_x = p_entity->position.x + p_entity->size.x; |
| 758 float end_y = p_entity->position.y + p_entity->size.y; | 792 float end_y = p_entity->position.y + p_entity->size.y; |
| 759 return (Rectangle){ | 793 return (Rectangle){ |
| 760 fminf(p_entity->position.x, end_x), | 794 fminf(p_entity->position.x, end_x), |
| 947 break; | 981 break; |
| 948 case CANVAS_ENTITY_WEB_CONTENT: | 982 case CANVAS_ENTITY_WEB_CONTENT: |
| 949 Canvas_Context_Append(p_writer, "url="); | 983 Canvas_Context_Append(p_writer, "url="); |
| 950 Canvas_Context_Append_Quoted(p_writer, p_entity->text); | 984 Canvas_Context_Append_Quoted(p_writer, p_entity->text); |
| 951 break; | 985 break; |
| 986 case CANVAS_ENTITY_CONVERSATION: | |
| 987 Canvas_Context_Append(p_writer, "title="); | |
| 988 Canvas_Context_Append_Quoted(p_writer, p_entity->label); | |
| 989 Canvas_Context_Append(p_writer, " transcript="); | |
| 990 Canvas_Context_Append_Quoted(p_writer, p_entity->text); | |
| 991 Canvas_Context_Append( | |
| 992 p_writer, | |
| 993 " collapsed=%s", | |
| 994 p_entity->active ? "true" : "false"); | |
| 995 break; | |
| 952 case CANVAS_ENTITY_LUCIDE_GALLERY: | 996 case CANVAS_ENTITY_LUCIDE_GALLERY: |
| 953 Canvas_Context_Append( | 997 Canvas_Context_Append( |
| 954 p_writer, | 998 p_writer, |
| 955 "icons=%d query=", | 999 "icons=%d query=", |
| 956 Canvas_Lucide_Count_Matches(p_entity->text)); | 1000 Canvas_Lucide_Count_Matches(p_entity->text)); |
| 993 Canvas_Context_Snapshot snapshot = {0}; | 1037 Canvas_Context_Snapshot snapshot = {0}; |
| 994 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { | 1038 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { |
| 995 const Canvas_Entity *p_entity = &p_scene->p_entities[index]; | 1039 const Canvas_Entity *p_entity = &p_scene->p_entities[index]; |
| 996 if (p_entity->type == CANVAS_ENTITY_NOTIFICATION && | 1040 if (p_entity->type == CANVAS_ENTITY_NOTIFICATION && |
| 997 p_entity->animation_amount <= 0.01f) { | 1041 p_entity->animation_amount <= 0.01f) { |
| 1042 continue; | |
| 1043 } | |
| 1044 if (p_entity->type == CANVAS_ENTITY_TEXT_AREA && | |
| 1045 strcmp(p_entity->label, "Dictation") == 0) { | |
| 1046 continue; | |
| 1047 } | |
| 1048 if (p_entity->type == CANVAS_ENTITY_CONVERSATION && | |
| 1049 p_entity->parked) { | |
| 998 continue; | 1050 continue; |
| 999 } | 1051 } |
| 1000 if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue; | 1052 if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue; |
| 1001 | 1053 |
| 1002 snapshot.visible_count++; | 1054 snapshot.visible_count++; |
| 1122 if (type == CANVAS_ENTITY_WEB_CONTENT) { | 1174 if (type == CANVAS_ENTITY_WEB_CONTENT) { |
| 1123 entity.size = (Vector2){720.0f, 460.0f}; | 1175 entity.size = (Vector2){720.0f, 460.0f}; |
| 1124 entity.active = TRUE; | 1176 entity.active = TRUE; |
| 1125 snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com"); | 1177 snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com"); |
| 1126 } | 1178 } |
| 1179 if (type == CANVAS_ENTITY_CONVERSATION) { | |
| 1180 entity.size = (Vector2){ | |
| 1181 320.0f, | |
| 1182 CANVAS_CONVERSATION_EXPANDED_HEIGHT, | |
| 1183 }; | |
| 1184 entity.animation_amount = 1.0f; | |
| 1185 snprintf(entity.label, sizeof(entity.label), "Agent session"); | |
| 1186 } | |
| 1127 if (type == CANVAS_ENTITY_LUCIDE_GALLERY) { | 1187 if (type == CANVAS_ENTITY_LUCIDE_GALLERY) { |
| 1128 entity.size = (Vector2){1440.0f, 1040.0f}; | 1188 entity.size = (Vector2){1440.0f, 1040.0f}; |
| 1129 } | 1189 } |
| 1130 | 1190 |
| 1131 Dowa_Array_Push_Arena(p_scene->p_entities, entity, p_scene->p_arena); | 1191 Dowa_Array_Push_Arena(p_scene->p_entities, entity, p_scene->p_arena); |
| 1192 return TRUE; | |
| 1193 } | |
| 1194 | |
| 1195 uint32 Canvas_Scene_Add_Conversation( | |
| 1196 Canvas_Scene *p_scene, | |
| 1197 const char *p_title, | |
| 1198 const char *p_prompt, | |
| 1199 const char *p_response, | |
| 1200 Vector2 position) | |
| 1201 { | |
| 1202 if (!Canvas_Scene_Add( | |
| 1203 p_scene, | |
| 1204 CANVAS_ENTITY_CONVERSATION, | |
| 1205 position)) { | |
| 1206 return 0; | |
| 1207 } | |
| 1208 Canvas_Entity *p_entity = | |
| 1209 &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1]; | |
| 1210 p_entity->size = (Vector2){420.0f, 300.0f}; | |
| 1211 snprintf( | |
| 1212 p_entity->label, | |
| 1213 sizeof(p_entity->label), | |
| 1214 "%s", | |
| 1215 p_title && p_title[0] ? p_title : "New agent session"); | |
| 1216 snprintf( | |
| 1217 p_entity->text, | |
| 1218 sizeof(p_entity->text), | |
| 1219 "You\n%s\n\nCopilot\n%s", | |
| 1220 p_prompt, | |
| 1221 p_response); | |
| 1222 p_scene->selected_index = | |
| 1223 (int32)Dowa_Array_Length(p_scene->p_entities) - 1; | |
| 1224 return p_entity->id; | |
| 1225 } | |
| 1226 | |
| 1227 boolean Canvas_Scene_Append_Conversation( | |
| 1228 Canvas_Scene *p_scene, | |
| 1229 uint32 entity_id, | |
| 1230 const char *p_prompt, | |
| 1231 const char *p_response) | |
| 1232 { | |
| 1233 for (size_t index = 0; | |
| 1234 index < Dowa_Array_Length(p_scene->p_entities); | |
| 1235 index++) { | |
| 1236 Canvas_Entity *p_entity = &p_scene->p_entities[index]; | |
| 1237 if (p_entity->id != entity_id || | |
| 1238 p_entity->type != CANVAS_ENTITY_CONVERSATION) { | |
| 1239 continue; | |
| 1240 } | |
| 1241 size_t length = strlen(p_entity->text); | |
| 1242 snprintf( | |
| 1243 p_entity->text + length, | |
| 1244 sizeof(p_entity->text) - length, | |
| 1245 "\n\nYou\n%s\n\nCopilot\n%s", | |
| 1246 p_prompt, | |
| 1247 p_response); | |
| 1248 p_scene->selected_index = (int32)index; | |
| 1249 return TRUE; | |
| 1250 } | |
| 1251 return FALSE; | |
| 1252 } | |
| 1253 | |
| 1254 static float Canvas_Conversation_Max_Scroll( | |
| 1255 const Canvas_Entity *p_entity, | |
| 1256 Font font) | |
| 1257 { | |
| 1258 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; | |
| 1259 int32 line_count = Canvas_Text_Build_Lines( | |
| 1260 font, | |
| 1261 p_entity->text, | |
| 1262 p_entity->size.x - 32.0f, | |
| 1263 lines, | |
| 1264 CANVAS_ENTITY_TEXT_CAPACITY); | |
| 1265 return fmaxf( | |
| 1266 0.0f, | |
| 1267 (float)line_count * 18.0f - | |
| 1268 (CANVAS_CONVERSATION_EXPANDED_HEIGHT - 82.0f)); | |
| 1269 } | |
| 1270 | |
| 1271 void Canvas_Conversation_Scroll_To_End( | |
| 1272 Canvas_Entity *p_entity, | |
| 1273 Font font) | |
| 1274 { | |
| 1275 if (!p_entity || p_entity->type != CANVAS_ENTITY_CONVERSATION) return; | |
| 1276 p_entity->value = (int32)Canvas_Conversation_Max_Scroll(p_entity, font); | |
| 1277 } | |
| 1278 | |
| 1279 void Canvas_Conversation_Set_Collapsed( | |
| 1280 Canvas_Entity *p_entity, | |
| 1281 boolean collapsed) | |
| 1282 { | |
| 1283 if (!p_entity || p_entity->type != CANVAS_ENTITY_CONVERSATION) return; | |
| 1284 p_entity->active = collapsed; | |
| 1285 } | |
| 1286 | |
| 1287 size_t Canvas_Scene_Parked_Conversation_Count( | |
| 1288 const Canvas_Scene *p_scene) | |
| 1289 { | |
| 1290 size_t count = 0; | |
| 1291 for (size_t index = 0; | |
| 1292 index < Dowa_Array_Length(p_scene->p_entities); | |
| 1293 index++) { | |
| 1294 const Canvas_Entity *p_entity = &p_scene->p_entities[index]; | |
| 1295 if (p_entity->type == CANVAS_ENTITY_CONVERSATION && | |
| 1296 p_entity->parked) { | |
| 1297 count++; | |
| 1298 } | |
| 1299 } | |
| 1300 return count; | |
| 1301 } | |
| 1302 | |
| 1303 boolean Canvas_Scene_Set_Conversation_Parked( | |
| 1304 Canvas_Scene *p_scene, | |
| 1305 uint32 entity_id, | |
| 1306 boolean parked) | |
| 1307 { | |
| 1308 Canvas_Entity *p_entity = NULL; | |
| 1309 for (size_t index = 0; | |
| 1310 index < Dowa_Array_Length(p_scene->p_entities); | |
| 1311 index++) { | |
| 1312 if (p_scene->p_entities[index].id == entity_id && | |
| 1313 p_scene->p_entities[index].type == CANVAS_ENTITY_CONVERSATION) { | |
| 1314 p_entity = &p_scene->p_entities[index]; | |
| 1315 break; | |
| 1316 } | |
| 1317 } | |
| 1318 if (!p_entity || p_entity->parked == parked) return FALSE; | |
| 1319 if (parked) { | |
| 1320 size_t slot = Canvas_Scene_Parked_Conversation_Count(p_scene); | |
| 1321 p_entity->parked_restore_position = p_entity->position; | |
| 1322 p_entity->position = (Vector2){ | |
| 1323 CANVAS_PARKING_LOT_X + (float)(slot % 3) * 460.0f, | |
| 1324 CANVAS_PARKING_LOT_Y + (float)(slot / 3) * 340.0f, | |
| 1325 }; | |
| 1326 p_entity->parked = TRUE; | |
| 1327 p_entity->pinned = FALSE; | |
| 1328 } else { | |
| 1329 p_entity->position = p_entity->parked_restore_position; | |
| 1330 p_entity->parked = FALSE; | |
| 1331 } | |
| 1332 return TRUE; | |
| 1333 } | |
| 1334 | |
| 1335 boolean Canvas_Scene_Show_Parking_Lot( | |
| 1336 Canvas_Scene *p_scene, | |
| 1337 Canvas_Camera *p_camera) | |
| 1338 { | |
| 1339 Rectangle bounds = {0}; | |
| 1340 boolean found = FALSE; | |
| 1341 for (size_t index = 0; | |
| 1342 index < Dowa_Array_Length(p_scene->p_entities); | |
| 1343 index++) { | |
| 1344 Canvas_Entity *p_entity = &p_scene->p_entities[index]; | |
| 1345 if (p_entity->type != CANVAS_ENTITY_CONVERSATION || | |
| 1346 !p_entity->parked) { | |
| 1347 continue; | |
| 1348 } | |
| 1349 Rectangle entity_bounds = Canvas_Entity_Bounds(p_entity); | |
| 1350 if (!found) { | |
| 1351 bounds = entity_bounds; | |
| 1352 found = TRUE; | |
| 1353 } else { | |
| 1354 float right = fmaxf( | |
| 1355 bounds.x + bounds.width, | |
| 1356 entity_bounds.x + entity_bounds.width); | |
| 1357 float bottom = fmaxf( | |
| 1358 bounds.y + bounds.height, | |
| 1359 entity_bounds.y + entity_bounds.height); | |
| 1360 bounds.x = fminf(bounds.x, entity_bounds.x); | |
| 1361 bounds.y = fminf(bounds.y, entity_bounds.y); | |
| 1362 bounds.width = right - bounds.x; | |
| 1363 bounds.height = bottom - bounds.y; | |
| 1364 } | |
| 1365 } | |
| 1366 if (!found) return FALSE; | |
| 1367 Canvas_Camera_Fit_Bounds( | |
| 1368 p_camera, | |
| 1369 bounds, | |
| 1370 80.0f, | |
| 1371 80.0f, | |
| 1372 80.0f, | |
| 1373 80.0f); | |
| 1132 return TRUE; | 1374 return TRUE; |
| 1133 } | 1375 } |
| 1134 | 1376 |
| 1135 void Canvas_Scene_Clear(Canvas_Scene *p_scene) | 1377 void Canvas_Scene_Clear(Canvas_Scene *p_scene) |
| 1136 { | 1378 { |
| 1203 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, 90.0f}); | 1445 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, 90.0f}); |
| 1204 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SCROLL_AREA, (Vector2){610.0f, -130.0f}); | 1446 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SCROLL_AREA, (Vector2){610.0f, -130.0f}); |
| 1205 | 1447 |
| 1206 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_IMAGE, (Vector2){-550.0f, 220.0f}); | 1448 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_IMAGE, (Vector2){-550.0f, 220.0f}); |
| 1207 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){-150.0f, 120.0f}); | 1449 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){-150.0f, 120.0f}); |
| 1450 Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CONVERSATION, (Vector2){610.0f, 220.0f}); | |
| 1451 Canvas_Entity *p_conversation = | |
| 1452 &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1]; | |
| 1453 snprintf( | |
| 1454 p_conversation->label, | |
| 1455 sizeof(p_conversation->label), | |
| 1456 "Spatial agent sessions"); | |
| 1457 snprintf( | |
| 1458 p_conversation->text, | |
| 1459 sizeof(p_conversation->text), | |
| 1460 "System\nPress M to dictate and Enter to send, or select the text " | |
| 1461 "area and press Ctrl/Cmd+Enter to ask Copilot."); | |
| 1208 } | 1462 } |
| 1209 | 1463 |
| 1210 void Canvas_Scene_Add_Browser_Grid(Canvas_Scene *p_scene) | 1464 void Canvas_Scene_Add_Browser_Grid(Canvas_Scene *p_scene) |
| 1211 { | 1465 { |
| 1212 Canvas_Scene_Clear(p_scene); | 1466 Canvas_Scene_Clear(p_scene); |
| 1320 case CANVAS_ENTITY_CARD: | 1574 case CANVAS_ENTITY_CARD: |
| 1321 case CANVAS_ENTITY_CALENDAR: | 1575 case CANVAS_ENTITY_CALENDAR: |
| 1322 case CANVAS_ENTITY_SWITCH: | 1576 case CANVAS_ENTITY_SWITCH: |
| 1323 case CANVAS_ENTITY_TABLE: | 1577 case CANVAS_ENTITY_TABLE: |
| 1324 case CANVAS_ENTITY_SCROLL_AREA: | 1578 case CANVAS_ENTITY_SCROLL_AREA: |
| 1325 case CANVAS_ENTITY_IMAGE: { | 1579 case CANVAS_ENTITY_IMAGE: |
| 1580 case CANVAS_ENTITY_CONVERSATION: { | |
| 1326 float height = p_entity->size.y; | 1581 float height = p_entity->size.y; |
| 1327 if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { | 1582 if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { |
| 1328 height += 8.0f + 3.0f * 40.0f; | 1583 height += 8.0f + 3.0f * 40.0f; |
| 1329 } | 1584 } |
| 1330 return CheckCollisionPointRec( | 1585 return CheckCollisionPointRec( |
| 1611 } | 1866 } |
| 1612 | 1867 |
| 1613 static Rectangle Canvas_Text_Area_Content_Bounds( | 1868 static Rectangle Canvas_Text_Area_Content_Bounds( |
| 1614 const Canvas_Entity *p_entity) | 1869 const Canvas_Entity *p_entity) |
| 1615 { | 1870 { |
| 1871 float header_height = p_entity->label[0] ? 26.0f : 0.0f; | |
| 1616 return (Rectangle){ | 1872 return (Rectangle){ |
| 1617 p_entity->position.x + CANVAS_TEXT_AREA_PADDING, | 1873 p_entity->position.x + CANVAS_TEXT_AREA_PADDING, |
| 1618 p_entity->position.y + CANVAS_TEXT_AREA_PADDING, | 1874 p_entity->position.y + CANVAS_TEXT_AREA_PADDING + header_height, |
| 1619 p_entity->size.x - CANVAS_TEXT_AREA_PADDING * 2.0f, | 1875 p_entity->size.x - CANVAS_TEXT_AREA_PADDING * 2.0f, |
| 1620 p_entity->size.y - CANVAS_TEXT_AREA_PADDING * 2.0f, | 1876 p_entity->size.y - CANVAS_TEXT_AREA_PADDING * 2.0f - header_height, |
| 1621 }; | 1877 }; |
| 1622 } | 1878 } |
| 1623 | 1879 |
| 1624 static int32 Canvas_Text_Line_For_Cursor( | 1880 static int32 Canvas_Text_Line_For_Cursor( |
| 1625 const Canvas_Text_Line *p_lines, | 1881 const Canvas_Text_Line *p_lines, |
| 1796 p_entity->text_scroll_y = cursor_top; | 2052 p_entity->text_scroll_y = cursor_top; |
| 1797 } else if (cursor_bottom > | 2053 } else if (cursor_bottom > |
| 1798 p_entity->text_scroll_y + content.height) { | 2054 p_entity->text_scroll_y + content.height) { |
| 1799 p_entity->text_scroll_y = cursor_bottom - content.height; | 2055 p_entity->text_scroll_y = cursor_bottom - content.height; |
| 1800 } | 2056 } |
| 2057 | |
| 1801 float max_scroll = fmaxf( | 2058 float max_scroll = fmaxf( |
| 1802 0.0f, | 2059 0.0f, |
| 1803 (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height); | 2060 (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height); |
| 1804 p_entity->text_scroll_y = Canvas_Clamp( | 2061 p_entity->text_scroll_y = Canvas_Clamp( |
| 1805 p_entity->text_scroll_y, | 2062 p_entity->text_scroll_y, |
| 1806 0.0f, | 2063 0.0f, |
| 1807 max_scroll); | 2064 max_scroll); |
| 2065 } | |
| 2066 | |
| 2067 void Canvas_Text_Area_Set_Content( | |
| 2068 Canvas_Entity *p_entity, | |
| 2069 Font font, | |
| 2070 const char *p_text, | |
| 2071 boolean muted) | |
| 2072 { | |
| 2073 if (!p_entity || p_entity->type != CANVAS_ENTITY_TEXT_AREA) return; | |
| 2074 snprintf(p_entity->text, sizeof(p_entity->text), "%s", p_text ? p_text : ""); | |
| 2075 p_entity->text_cursor = (int32)strlen(p_entity->text); | |
| 2076 p_entity->text_selection_anchor = p_entity->text_cursor; | |
| 2077 p_entity->text_muted = muted; | |
| 2078 | |
| 2079 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; | |
| 2080 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity); | |
| 2081 int32 line_count = Canvas_Text_Build_Lines( | |
| 2082 font, | |
| 2083 p_entity->text, | |
| 2084 content.width, | |
| 2085 lines, | |
| 2086 CANVAS_ENTITY_TEXT_CAPACITY); | |
| 2087 p_entity->text_scroll_y = fmaxf( | |
| 2088 0.0f, | |
| 2089 (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height); | |
| 1808 } | 2090 } |
| 1809 | 2091 |
| 1810 static void Canvas_Text_Update_Keyboard(Canvas_Entity *p_entity, Font font) | 2092 static void Canvas_Text_Update_Keyboard(Canvas_Entity *p_entity, Font font) |
| 1811 { | 2093 { |
| 1812 boolean shortcut = Canvas_Control_Is_Down(); | 2094 boolean shortcut = Canvas_Control_Is_Down(); |
| 2128 const Canvas_Camera *p_camera) | 2410 const Canvas_Camera *p_camera) |
| 2129 { | 2411 { |
| 2130 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { | 2412 for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { |
| 2131 Canvas_Entity *p_entity = &p_scene->p_entities[index]; | 2413 Canvas_Entity *p_entity = &p_scene->p_entities[index]; |
| 2132 if (p_entity->type != CANVAS_ENTITY_ACCORDION && | 2414 if (p_entity->type != CANVAS_ENTITY_ACCORDION && |
| 2133 p_entity->type != CANVAS_ENTITY_SWITCH) { | 2415 p_entity->type != CANVAS_ENTITY_SWITCH && |
| 2416 p_entity->type != CANVAS_ENTITY_CONVERSATION) { | |
| 2134 continue; | 2417 continue; |
| 2135 } | 2418 } |
| 2136 | 2419 |
| 2137 float target = p_entity->active ? 1.0f : 0.0f; | 2420 float target = p_entity->type == CANVAS_ENTITY_CONVERSATION |
| 2421 ? (p_entity->active ? 0.0f : 1.0f) | |
| 2422 : (p_entity->active ? 1.0f : 0.0f); | |
| 2138 float speed = p_entity->type == CANVAS_ENTITY_SWITCH ? 7.0f : 4.5f; | 2423 float speed = p_entity->type == CANVAS_ENTITY_SWITCH ? 7.0f : 4.5f; |
| 2139 float step = GetFrameTime() * speed; | 2424 float step = GetFrameTime() * speed; |
| 2140 if (p_entity->animation_amount < target) { | 2425 if (p_entity->animation_amount < target) { |
| 2141 p_entity->animation_amount = fminf( | 2426 p_entity->animation_amount = fminf( |
| 2142 target, | 2427 target, |
| 2147 p_entity->animation_amount - step); | 2432 p_entity->animation_amount - step); |
| 2148 } | 2433 } |
| 2149 float amount = p_entity->animation_amount; | 2434 float amount = p_entity->animation_amount; |
| 2150 float eased = amount * amount * (3.0f - 2.0f * amount); | 2435 float eased = amount * amount * (3.0f - 2.0f * amount); |
| 2151 if (p_entity->type == CANVAS_ENTITY_SWITCH) continue; | 2436 if (p_entity->type == CANVAS_ENTITY_SWITCH) continue; |
| 2437 if (p_entity->type == CANVAS_ENTITY_CONVERSATION) { | |
| 2438 float height = CANVAS_CONVERSATION_COLLAPSED_HEIGHT + | |
| 2439 (CANVAS_CONVERSATION_EXPANDED_HEIGHT - | |
| 2440 CANVAS_CONVERSATION_COLLAPSED_HEIGHT) * eased; | |
| 2441 if (p_entity->pinned) { | |
| 2442 float pinned_scale = | |
| 2443 p_entity->pinned_screen_size.x / p_entity->size.x; | |
| 2444 p_entity->pinned_screen_size.y = height * pinned_scale; | |
| 2445 p_entity->size.y = | |
| 2446 p_entity->pinned_screen_size.y / p_camera->zoom; | |
| 2447 } else { | |
| 2448 p_entity->size.y = height; | |
| 2449 } | |
| 2450 continue; | |
| 2451 } | |
| 2152 if (p_entity->pinned) { | 2452 if (p_entity->pinned) { |
| 2153 float pinned_scale = p_entity->pinned_screen_size.x / 300.0f; | 2453 float pinned_scale = p_entity->pinned_screen_size.x / 300.0f; |
| 2154 p_entity->pinned_screen_size.y = | 2454 p_entity->pinned_screen_size.y = |
| 2155 (56.0f + 108.0f * eased) * pinned_scale; | 2455 (56.0f + 108.0f * eased) * pinned_scale; |
| 2156 p_entity->size.y = | 2456 p_entity->size.y = |
| 2215 (float)p_hovered->value - wheel * 28.0f, | 2515 (float)p_hovered->value - wheel * 28.0f, |
| 2216 0.0f, | 2516 0.0f, |
| 2217 240.0f); | 2517 240.0f); |
| 2218 consumed_scroll = TRUE; | 2518 consumed_scroll = TRUE; |
| 2219 } | 2519 } |
| 2520 } else if (p_hovered->type == CANVAS_ENTITY_CONVERSATION && | |
| 2521 !control_down) { | |
| 2522 float wheel = GetMouseWheelMove(); | |
| 2523 if (wheel != 0.0f) { | |
| 2524 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; | |
| 2525 int32 line_count = Canvas_Text_Build_Lines( | |
| 2526 font, | |
| 2527 p_hovered->text, | |
| 2528 p_hovered->size.x - 32.0f, | |
| 2529 lines, | |
| 2530 CANVAS_ENTITY_TEXT_CAPACITY); | |
| 2531 float max_scroll = fmaxf( | |
| 2532 0.0f, | |
| 2533 (float)line_count * 18.0f - | |
| 2534 (p_hovered->size.y - 82.0f)); | |
| 2535 p_hovered->value = (int32)Canvas_Clamp( | |
| 2536 (float)p_hovered->value - wheel * 36.0f, | |
| 2537 0.0f, | |
| 2538 max_scroll); | |
| 2539 consumed_scroll = TRUE; | |
| 2540 } | |
| 2220 } else if (p_hovered->type == CANVAS_ENTITY_LUCIDE_GALLERY && | 2541 } else if (p_hovered->type == CANVAS_ENTITY_LUCIDE_GALLERY && |
| 2221 !control_down) { | 2542 !control_down) { |
| 2222 float wheel = GetMouseWheelMove(); | 2543 float wheel = GetMouseWheelMove(); |
| 2223 if (wheel != 0.0f) { | 2544 if (wheel != 0.0f) { |
| 2224 p_hovered->value = (int32)Canvas_Clamp( | 2545 p_hovered->value = (int32)Canvas_Clamp( |
| 2254 } | 2575 } |
| 2255 } | 2576 } |
| 2256 | 2577 |
| 2257 if (p_scene->selected_index >= 0) { | 2578 if (p_scene->selected_index >= 0) { |
| 2258 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; | 2579 Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; |
| 2580 if (p_entity->type == CANVAS_ENTITY_CONVERSATION && | |
| 2581 CheckCollisionPointRec( | |
| 2582 world_pointer, | |
| 2583 Canvas_Conversation_Park_Bounds(p_entity))) { | |
| 2584 Canvas_Scene_Set_Conversation_Parked( | |
| 2585 p_scene, | |
| 2586 p_entity->id, | |
| 2587 p_entity->parked ? FALSE : TRUE); | |
| 2588 p_scene->pressed_index = -1; | |
| 2589 return TRUE; | |
| 2590 } | |
| 2591 if (p_entity->type == CANVAS_ENTITY_CONVERSATION && | |
| 2592 CheckCollisionPointRec( | |
| 2593 world_pointer, | |
| 2594 Canvas_Conversation_Collapse_Bounds(p_entity))) { | |
| 2595 Canvas_Conversation_Set_Collapsed( | |
| 2596 p_entity, | |
| 2597 p_entity->active ? FALSE : TRUE); | |
| 2598 p_scene->pressed_index = -1; | |
| 2599 return TRUE; | |
| 2600 } | |
| 2259 if (p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) { | 2601 if (p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) { |
| 2260 p_entity->active = CheckCollisionPointRec( | 2602 p_entity->active = CheckCollisionPointRec( |
| 2261 world_pointer, | 2603 world_pointer, |
| 2262 Canvas_Lucide_Search_Bounds(p_entity)) ? TRUE : FALSE; | 2604 Canvas_Lucide_Search_Bounds(p_entity)) ? TRUE : FALSE; |
| 2263 } | 2605 } |
| 2674 case CANVAS_ENTITY_CALENDAR: | 3016 case CANVAS_ENTITY_CALENDAR: |
| 2675 case CANVAS_ENTITY_SWITCH: | 3017 case CANVAS_ENTITY_SWITCH: |
| 2676 case CANVAS_ENTITY_TABLE: | 3018 case CANVAS_ENTITY_TABLE: |
| 2677 case CANVAS_ENTITY_NOTIFICATION: | 3019 case CANVAS_ENTITY_NOTIFICATION: |
| 2678 case CANVAS_ENTITY_SCROLL_AREA: | 3020 case CANVAS_ENTITY_SCROLL_AREA: |
| 2679 case CANVAS_ENTITY_IMAGE: { | 3021 case CANVAS_ENTITY_IMAGE: |
| 3022 case CANVAS_ENTITY_CONVERSATION: { | |
| 2680 DrawRectangleRoundedLinesEx( | 3023 DrawRectangleRoundedLinesEx( |
| 2681 (Rectangle){ | 3024 (Rectangle){ |
| 2682 p_entity->position.x - padding, | 3025 p_entity->position.x - padding, |
| 2683 p_entity->position.y - padding, | 3026 p_entity->position.y - padding, |
| 2684 p_entity->size.x + padding * 2.0f, | 3027 p_entity->size.x + padding * 2.0f, |
| 2749 Font font, | 3092 Font font, |
| 2750 const Canvas_Theme *p_theme) | 3093 const Canvas_Theme *p_theme) |
| 2751 { | 3094 { |
| 2752 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity); | 3095 Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity); |
| 2753 if (!p_entity->text[0]) { | 3096 if (!p_entity->text[0]) { |
| 3097 boolean dictation = strcmp(p_entity->label, "Dictation") == 0; | |
| 2754 DrawTextEx( | 3098 DrawTextEx( |
| 2755 font, | 3099 font, |
| 2756 "Write something...", | 3100 dictation ? "Speak now..." : "Type a thought for Copilot...", |
| 2757 (Vector2){content.x, content.y}, | 3101 (Vector2){content.x, content.y}, |
| 2758 CANVAS_TEXT_AREA_FONT_SIZE, | 3102 CANVAS_TEXT_AREA_FONT_SIZE, |
| 2759 CANVAS_TEXT_AREA_SPACING, | 3103 CANVAS_TEXT_AREA_SPACING, |
| 2760 p_theme->text_muted); | 3104 p_theme->text_muted); |
| 3105 DrawTextEx( | |
| 3106 font, | |
| 3107 dictation | |
| 3108 ? "Enter to send | Backspace to clear" | |
| 3109 : "Ctrl/Cmd+Enter to send", | |
| 3110 (Vector2){content.x, content.y + 22.0f}, | |
| 3111 12.0f, | |
| 3112 0.0f, | |
| 3113 Canvas_Color_Fade(p_theme->text_muted, 0.72f)); | |
| 2761 if (p_entity->active && fmod(GetTime(), 1.0) < 0.55) { | 3114 if (p_entity->active && fmod(GetTime(), 1.0) < 0.55) { |
| 2762 DrawLineEx( | 3115 DrawLineEx( |
| 2763 (Vector2){content.x, content.y}, | 3116 (Vector2){content.x, content.y}, |
| 2764 (Vector2){ | 3117 (Vector2){ |
| 2765 content.x, | 3118 content.x, |
| 2838 font, | 3191 font, |
| 2839 line_text, | 3192 line_text, |
| 2840 (Vector2){content.x, y}, | 3193 (Vector2){content.x, y}, |
| 2841 CANVAS_TEXT_AREA_FONT_SIZE, | 3194 CANVAS_TEXT_AREA_FONT_SIZE, |
| 2842 CANVAS_TEXT_AREA_SPACING, | 3195 CANVAS_TEXT_AREA_SPACING, |
| 2843 p_theme->text); | 3196 p_entity->text_muted ? p_theme->text_muted : p_theme->text); |
| 2844 | 3197 |
| 2845 if (p_entity->active && | 3198 if (p_entity->active && |
| 2846 line == cursor_line && | 3199 line == cursor_line && |
| 2847 fmod(GetTime(), 1.0) < 0.55) { | 3200 fmod(GetTime(), 1.0) < 0.55) { |
| 2848 float cursor_x = content.x + Canvas_Text_Measure_Range( | 3201 float cursor_x = content.x + Canvas_Text_Measure_Range( |
| 3010 bounds, | 3363 bounds, |
| 3011 0.10f, | 3364 0.10f, |
| 3012 14, | 3365 14, |
| 3013 (p_entity->active ? 2.0f : 1.0f) / zoom, | 3366 (p_entity->active ? 2.0f : 1.0f) / zoom, |
| 3014 p_entity->active ? p_theme->accent : p_theme->border); | 3367 p_entity->active ? p_theme->accent : p_theme->border); |
| 3368 if (p_entity->label[0]) { | |
| 3369 DrawTextEx( | |
| 3370 font, | |
| 3371 p_entity->label, | |
| 3372 (Vector2){ | |
| 3373 bounds.x + CANVAS_TEXT_AREA_PADDING, | |
| 3374 bounds.y + 11.0f, | |
| 3375 }, | |
| 3376 12.0f, | |
| 3377 0.0f, | |
| 3378 p_theme->text_muted); | |
| 3379 DrawLineEx( | |
| 3380 (Vector2){ | |
| 3381 bounds.x + bounds.width - 38.0f, | |
| 3382 bounds.y + 17.0f, | |
| 3383 }, | |
| 3384 (Vector2){ | |
| 3385 bounds.x + bounds.width - 16.0f, | |
| 3386 bounds.y + 17.0f, | |
| 3387 }, | |
| 3388 2.0f / zoom, | |
| 3389 p_theme->border); | |
| 3390 } | |
| 3015 Canvas_Draw_Text_Area_Content(p_entity, font, p_theme); | 3391 Canvas_Draw_Text_Area_Content(p_entity, font, p_theme); |
| 3016 break; | 3392 break; |
| 3017 } | 3393 } |
| 3018 case CANVAS_ENTITY_DROPDOWN: { | 3394 case CANVAS_ENTITY_DROPDOWN: { |
| 3019 Rectangle bounds = { | 3395 Rectangle bounds = { |
| 3503 0.1f, | 3879 0.1f, |
| 3504 p_theme->text_muted); | 3880 p_theme->text_muted); |
| 3505 #endif | 3881 #endif |
| 3506 break; | 3882 break; |
| 3507 } | 3883 } |
| 3884 case CANVAS_ENTITY_CONVERSATION: { | |
| 3885 Rectangle bounds = { | |
| 3886 p_entity->position.x, | |
| 3887 p_entity->position.y, | |
| 3888 p_entity->size.x, | |
| 3889 p_entity->size.y, | |
| 3890 }; | |
| 3891 Canvas_Theme_Draw_Shadow( | |
| 3892 p_theme, | |
| 3893 bounds, | |
| 3894 0.06f, | |
| 3895 14, | |
| 3896 zoom, | |
| 3897 1.0f); | |
| 3898 DrawRectangleRounded(bounds, 0.06f, 14, p_theme->surface); | |
| 3899 DrawRectangleRoundedLinesEx( | |
| 3900 bounds, | |
| 3901 0.06f, | |
| 3902 14, | |
| 3903 1.0f / zoom, | |
| 3904 p_theme->border); | |
| 3905 DrawRectangleRounded( | |
| 3906 (Rectangle){bounds.x, bounds.y, bounds.width, 54.0f}, | |
| 3907 0.06f, | |
| 3908 14, | |
| 3909 p_theme->surface_muted); | |
| 3910 Canvas_Lucide_Draw_Icon( | |
| 3911 Canvas_Lucide_Find_Icon( | |
| 3912 p_entity->agent_working ? "sparkles" : "messages-square"), | |
| 3913 (Vector2){bounds.x + 16.0f, bounds.y + 15.0f}, | |
| 3914 0.82f, | |
| 3915 1.8f, | |
| 3916 p_entity->agent_working ? | |
| 3917 p_theme->accent : | |
| 3918 p_theme->text); | |
| 3919 DrawTextEx( | |
| 3920 font, | |
| 3921 p_entity->label, | |
| 3922 (Vector2){bounds.x + 48.0f, bounds.y + 12.0f}, | |
| 3923 17.0f, | |
| 3924 0.0f, | |
| 3925 p_theme->text); | |
| 3926 DrawTextEx( | |
| 3927 font, | |
| 3928 p_entity->parked | |
| 3929 ? "Parked session" | |
| 3930 : (p_entity->agent_working | |
| 3931 ? "Thinking..." | |
| 3932 : "Agent session"), | |
| 3933 (Vector2){bounds.x + 48.0f, bounds.y + 33.0f}, | |
| 3934 11.0f, | |
| 3935 0.0f, | |
| 3936 p_theme->text_muted); | |
| 3937 Rectangle collapse = | |
| 3938 Canvas_Conversation_Collapse_Bounds(p_entity); | |
| 3939 Rectangle park = Canvas_Conversation_Park_Bounds(p_entity); | |
| 3940 Canvas_Lucide_Draw_Icon( | |
| 3941 Canvas_Lucide_Find_Icon( | |
| 3942 p_entity->active ? "chevron-down" : "chevron-up"), | |
| 3943 (Vector2){collapse.x + 5.0f, collapse.y + 5.0f}, | |
| 3944 0.68f, | |
| 3945 1.6f, | |
| 3946 p_theme->text_muted); | |
| 3947 Canvas_Lucide_Draw_Icon( | |
| 3948 Canvas_Lucide_Find_Icon( | |
| 3949 p_entity->parked ? "archive-restore" : "archive"), | |
| 3950 (Vector2){park.x + 5.0f, park.y + 5.0f}, | |
| 3951 0.68f, | |
| 3952 1.6f, | |
| 3953 p_theme->text_muted); | |
| 3954 | |
| 3955 if (p_entity->size.y <= 82.0f) break; | |
| 3956 Rectangle content = { | |
| 3957 bounds.x + 16.0f, | |
| 3958 bounds.y + 68.0f, | |
| 3959 bounds.width - 32.0f, | |
| 3960 bounds.height - 82.0f, | |
| 3961 }; | |
| 3962 Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; | |
| 3963 int32 line_count = Canvas_Text_Build_Lines( | |
| 3964 font, | |
| 3965 p_entity->text, | |
| 3966 content.width, | |
| 3967 lines, | |
| 3968 CANVAS_ENTITY_TEXT_CAPACITY); | |
| 3969 for (int32 line_index = 0; | |
| 3970 line_index < line_count; | |
| 3971 line_index++) { | |
| 3972 float y = content.y + (float)line_index * 18.0f - | |
| 3973 (float)p_entity->value; | |
| 3974 if (y + 18.0f < content.y || | |
| 3975 y > content.y + content.height) { | |
| 3976 continue; | |
| 3977 } | |
| 3978 char line_text[CANVAS_ENTITY_TEXT_CAPACITY]; | |
| 3979 int32 length = | |
| 3980 lines[line_index].end - lines[line_index].start; | |
| 3981 memcpy( | |
| 3982 line_text, | |
| 3983 p_entity->text + lines[line_index].start, | |
| 3984 (size_t)length); | |
| 3985 line_text[length] = '\0'; | |
| 3986 boolean speaker = | |
| 3987 strcmp(line_text, "You") == 0 || | |
| 3988 strcmp(line_text, "Copilot") == 0 || | |
| 3989 strcmp(line_text, "System") == 0; | |
| 3990 DrawTextEx( | |
| 3991 font, | |
| 3992 line_text, | |
| 3993 (Vector2){content.x, y}, | |
| 3994 14.0f, | |
| 3995 0.0f, | |
| 3996 speaker ? p_theme->accent : p_theme->text); | |
| 3997 } | |
| 3998 break; | |
| 3999 } | |
| 3508 case CANVAS_ENTITY_LUCIDE_GALLERY: { | 4000 case CANVAS_ENTITY_LUCIDE_GALLERY: { |
| 3509 Rectangle bounds = { | 4001 Rectangle bounds = { |
| 3510 p_entity->position.x, | 4002 p_entity->position.x, |
| 3511 p_entity->position.y, | 4003 p_entity->position.y, |
| 3512 p_entity->size.x, | 4004 p_entity->size.x, |
| 4044 case CANVAS_ENTITY_TABLE: return "Table"; | 4536 case CANVAS_ENTITY_TABLE: return "Table"; |
| 4045 case CANVAS_ENTITY_NOTIFICATION: return "Notification"; | 4537 case CANVAS_ENTITY_NOTIFICATION: return "Notification"; |
| 4046 case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area"; | 4538 case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area"; |
| 4047 case CANVAS_ENTITY_IMAGE: return "Image"; | 4539 case CANVAS_ENTITY_IMAGE: return "Image"; |
| 4048 case CANVAS_ENTITY_WEB_CONTENT: return "Web content"; | 4540 case CANVAS_ENTITY_WEB_CONTENT: return "Web content"; |
| 4541 case CANVAS_ENTITY_CONVERSATION: return "Agent conversation"; | |
| 4049 case CANVAS_ENTITY_LUCIDE_GALLERY: return "Lucide gallery"; | 4542 case CANVAS_ENTITY_LUCIDE_GALLERY: return "Lucide gallery"; |
| 4050 default: return "Unknown"; | 4543 default: return "Unknown"; |
| 4051 } | 4544 } |
| 4052 } | 4545 } |