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 }