Mercurial
view infinite_canvas/web_surface_web.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 |
line wrap: on
line source
#include "infinite_canvas/web_surface.h" #include <emscripten/emscripten.h> #include <string.h> EM_JS(void, Canvas_Web_Begin_Frame, (), { document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { surface.dataset.canvasActive = "false"; }); }); EM_JS(void, Canvas_Web_Create_Or_Update, ( uint32 entity_id, const char *p_url, float x, float y, float width, float height, float opacity, int dark_mode), { const canvas = Module.canvas; const surfaceId = `canvas-web-surface-${entity_id}`; let surface = document.getElementById(surfaceId); if (!surface) { surface = document.createElement("div"); surface.id = surfaceId; surface.dataset.canvasWebSurface = "true"; surface.style.position = "fixed"; surface.style.zIndex = "4"; surface.style.overflow = "hidden"; surface.style.borderRadius = "8px"; surface.style.background = dark_mode ? "#0f0f11" : "#fff"; const frame = document.createElement("iframe"); frame.title = `Canvas web content ${entity_id}`; frame.referrerPolicy = "strict-origin-when-cross-origin"; frame.sandbox = "allow-downloads allow-forms allow-modals allow-popups allow-same-origin allow-scripts"; frame.style.width = "100%"; frame.style.height = "100%"; frame.style.border = "0"; frame.style.display = "block"; surface.appendChild(frame); document.body.appendChild(surface); } surface.dataset.canvasActive = "true"; const frame = surface.firstElementChild; const colorScheme = dark_mode ? "dark" : "light"; surface.style.colorScheme = colorScheme; surface.style.background = dark_mode ? "#0f0f11" : "#fff"; frame.style.colorScheme = colorScheme; const url = UTF8ToString(p_url); if (frame.dataset.url !== url) { frame.dataset.url = url; frame.src = url; } const canvasBounds = canvas.getBoundingClientRect(); const scaleX = canvasBounds.width / Math.max(1, canvas.width); const scaleY = canvasBounds.height / Math.max(1, canvas.height); surface.style.left = `${canvasBounds.left + x * scaleX}px`; surface.style.top = `${canvasBounds.top + y * scaleY}px`; surface.style.width = `${Math.max(1, width * scaleX)}px`; surface.style.height = `${Math.max(1, height * scaleY)}px`; surface.style.opacity = `${opacity}`; surface.style.pointerEvents = opacity >= 0.99 ? "auto" : "none"; }); EM_JS(void, Canvas_Web_History_Go, (uint32 entity_id, int direction), { const surface = document.getElementById( `canvas-web-surface-${entity_id}`); const frame = surface && surface.firstElementChild; if (!frame || !frame.contentWindow) return; try { frame.contentWindow.history.go(direction); } catch (error) { console.warn("Canvas iframe history navigation is unavailable", error); } }); EM_JS(int, Canvas_Web_Has_Surface, (uint32 entity_id), { return document.getElementById( `canvas-web-surface-${entity_id}`) ? 1 : 0; }); EM_JS(void, Canvas_Image_Create_Or_Update, ( uint32 entity_id, const char *p_source, float x, float y, float width, float height, float opacity), { const canvas = Module.canvas; const surfaceId = `canvas-web-surface-${entity_id}`; let surface = document.getElementById(surfaceId); if (!surface) { surface = document.createElement("div"); surface.id = surfaceId; surface.dataset.canvasWebSurface = "true"; surface.style.position = "fixed"; surface.style.zIndex = "4"; surface.style.overflow = "hidden"; surface.style.borderRadius = "8px"; surface.style.pointerEvents = "none"; const image = document.createElement("img"); image.alt = "Canvas image"; image.style.width = "100%"; image.style.height = "100%"; image.style.objectFit = "contain"; image.style.display = "block"; surface.appendChild(image); document.body.appendChild(surface); } surface.dataset.canvasActive = "true"; const image = surface.firstElementChild; const source = UTF8ToString(p_source); if (image.dataset.url !== source) { if (image.dataset.blobUrl) URL.revokeObjectURL(image.dataset.blobUrl); image.dataset.url = source; delete image.dataset.blobUrl; if (/^(https?:|data:|blob:)/i.test(source)) { image.src = source; } else { try { const bytes = FS.readFile(source); const extension = source.split(".").pop().toLowerCase(); const mime = extension === "svg" ? "image/svg+xml" : extension === "png" ? "image/png" : extension === "webp" ? "image/webp" : extension === "gif" ? "image/gif" : "image/jpeg"; const blobUrl = URL.createObjectURL(new Blob([bytes], {type: mime})); image.dataset.blobUrl = blobUrl; image.src = blobUrl; } catch (error) { image.src = source; } } } const canvasBounds = canvas.getBoundingClientRect(); const scaleX = canvasBounds.width / Math.max(1, canvas.width); const scaleY = canvasBounds.height / Math.max(1, canvas.height); surface.style.left = `${canvasBounds.left + x * scaleX}px`; surface.style.top = `${canvasBounds.top + y * scaleY}px`; surface.style.width = `${Math.max(1, width * scaleX)}px`; surface.style.height = `${Math.max(1, height * scaleY)}px`; surface.style.opacity = `${opacity}`; }); EM_JS(void, Canvas_Web_End_Frame, (), { document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { if (surface.dataset.canvasActive !== "true") { const content = surface.firstElementChild; if (content && content.dataset.blobUrl) { URL.revokeObjectURL(content.dataset.blobUrl); } surface.remove(); } }); }); EM_JS(void, Canvas_Web_Destroy, (), { document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { const content = surface.firstElementChild; if (content && content.dataset.blobUrl) { URL.revokeObjectURL(content.dataset.blobUrl); } surface.remove(); }); }); static Rectangle Canvas_Web_Content_Bounds( const Canvas_Camera *p_camera, const Canvas_Scene *p_scene, const Canvas_Entity *p_entity) { (void)p_scene; float frame_inset = CANVAS_WEB_FRAME_INSET / p_camera->zoom; float amount = p_entity->web_chrome_amount; if (amount < 0.0f) amount = 0.0f; if (amount > 1.0f) amount = 1.0f; float eased = amount * amount * (3.0f - 2.0f * amount); float toolbar_height = CANVAS_WEB_TOOLBAR_HEIGHT * eased / p_camera->zoom; float resize_handle = CANVAS_WEB_RESIZE_HANDLE / p_camera->zoom; Vector2 position = Canvas_Camera_World_To_Screen( p_camera, (Vector2){ p_entity->position.x + frame_inset, p_entity->position.y + frame_inset + toolbar_height, }); return (Rectangle){ position.x, position.y, (p_entity->size.x - frame_inset - resize_handle) * p_camera->zoom, (p_entity->size.y - frame_inset - resize_handle - toolbar_height) * p_camera->zoom, }; } static boolean Canvas_Web_Is_Visible( const Canvas_Camera *p_camera, Rectangle bounds) { return p_camera->zoom >= 0.30f && bounds.x + bounds.width > 0.0f && bounds.y + bounds.height > 0.0f && bounds.x < (float)p_camera->viewport_width && bounds.y < (float)p_camera->viewport_height; } static float Canvas_Web_Entity_Opacity(const Canvas_Entity *p_entity) { float amount = p_entity->visibility_amount; if (amount < 0.0f) amount = 0.0f; if (amount > 1.0f) amount = 1.0f; return amount * amount * (3.0f - 2.0f * amount); } int32 Canvas_Web_Surface_Execute_Subprocess(int argc, char **p_argv) { (void)argc; (void)p_argv; return -1; } boolean Canvas_Web_Surface_Init( Canvas_Web_Surface *p_surface, Dowa_Arena *p_arena, boolean dark_mode) { memset(p_surface, 0, sizeof(*p_surface)); p_surface->p_arena = p_arena; p_surface->dark_mode = dark_mode; p_surface->initialized = TRUE; return TRUE; } void Canvas_Web_Surface_Set_Dark_Mode( Canvas_Web_Surface *p_surface, boolean dark_mode) { p_surface->dark_mode = dark_mode; } boolean Canvas_Web_Surface_Update_Input( Canvas_Web_Surface *p_surface, const Canvas_Camera *p_camera, const Canvas_Scene *p_scene) { (void)p_surface; (void)p_camera; (void)p_scene; return FALSE; } void Canvas_Web_Surface_Sync( Canvas_Web_Surface *p_surface, const Canvas_Camera *p_camera, const Canvas_Scene *p_scene) { if (!p_surface->initialized) return; Canvas_Web_Begin_Frame(); uint32 visible_views = 0; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities) && visible_views < CANVAS_MAX_WEB_VIEWS; index++) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT && p_entity->type != CANVAS_ENTITY_IMAGE) { continue; } Rectangle bounds = Canvas_Web_Content_Bounds( p_camera, p_scene, p_entity); if (!Canvas_Web_Is_Visible(p_camera, bounds)) continue; float opacity = Canvas_Web_Entity_Opacity(p_entity); if (p_entity->type == CANVAS_ENTITY_IMAGE) { Canvas_Image_Create_Or_Update( p_entity->id, p_entity->text, bounds.x, bounds.y, bounds.width, bounds.height, opacity); } else { Canvas_Web_Create_Or_Update( p_entity->id, p_entity->text, bounds.x, bounds.y, bounds.width, bounds.height, opacity, p_surface->dark_mode ? 1 : 0); } visible_views++; } Canvas_Web_End_Frame(); } void Canvas_Web_Surface_Draw( const Canvas_Web_Surface *p_surface, const Canvas_Camera *p_camera, const Canvas_Scene *p_scene) { (void)p_surface; (void)p_camera; (void)p_scene; } void Canvas_Web_Surface_Draw_Entity( const Canvas_Web_Surface *p_surface, const Canvas_Camera *p_camera, const Canvas_Scene *p_scene, size_t entity_index) { (void)p_surface; (void)p_camera; (void)p_scene; (void)entity_index; } const char *Canvas_Web_Surface_URL( const Canvas_Web_Surface *p_surface, uint32 entity_id) { (void)p_surface; (void)entity_id; return NULL; } boolean Canvas_Web_Surface_Can_Go_Back( const Canvas_Web_Surface *p_surface, uint32 entity_id) { (void)p_surface; return Canvas_Web_Has_Surface(entity_id) ? TRUE : FALSE; } boolean Canvas_Web_Surface_Can_Go_Forward( const Canvas_Web_Surface *p_surface, uint32 entity_id) { (void)p_surface; return Canvas_Web_Has_Surface(entity_id) ? TRUE : FALSE; } void Canvas_Web_Surface_Go_Back( Canvas_Web_Surface *p_surface, uint32 entity_id) { (void)p_surface; Canvas_Web_History_Go(entity_id, -1); } void Canvas_Web_Surface_Go_Forward( Canvas_Web_Surface *p_surface, uint32 entity_id) { (void)p_surface; Canvas_Web_History_Go(entity_id, 1); } boolean Canvas_Web_Surface_Is_Focused(const Canvas_Web_Surface *p_surface) { (void)p_surface; return FALSE; } void Canvas_Web_Surface_Shutdown(Canvas_Web_Surface *p_surface) { if (!p_surface->initialized) return; Canvas_Web_Destroy(); p_surface->initialized = FALSE; }