Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 277:1d99147f520c | 278:8d560f50ed4c |
|---|---|
| 13 uint32 entity_id, | 13 uint32 entity_id, |
| 14 const char *p_url, | 14 const char *p_url, |
| 15 float x, | 15 float x, |
| 16 float y, | 16 float y, |
| 17 float width, | 17 float width, |
| 18 float height), { | 18 float height, |
| 19 float opacity, | |
| 20 int dark_mode), { | |
| 19 const canvas = Module.canvas; | 21 const canvas = Module.canvas; |
| 20 const surfaceId = `canvas-web-surface-${entity_id}`; | 22 const surfaceId = `canvas-web-surface-${entity_id}`; |
| 21 let surface = document.getElementById(surfaceId); | 23 let surface = document.getElementById(surfaceId); |
| 22 if (!surface) { | 24 if (!surface) { |
| 23 surface = document.createElement("div"); | 25 surface = document.createElement("div"); |
| 25 surface.dataset.canvasWebSurface = "true"; | 27 surface.dataset.canvasWebSurface = "true"; |
| 26 surface.style.position = "fixed"; | 28 surface.style.position = "fixed"; |
| 27 surface.style.zIndex = "4"; | 29 surface.style.zIndex = "4"; |
| 28 surface.style.overflow = "hidden"; | 30 surface.style.overflow = "hidden"; |
| 29 surface.style.borderRadius = "8px"; | 31 surface.style.borderRadius = "8px"; |
| 30 surface.style.background = "#fff"; | 32 surface.style.background = dark_mode ? "#0f0f11" : "#fff"; |
| 31 | 33 |
| 32 const frame = document.createElement("iframe"); | 34 const frame = document.createElement("iframe"); |
| 33 frame.title = `Canvas web content ${entity_id}`; | 35 frame.title = `Canvas web content ${entity_id}`; |
| 34 frame.referrerPolicy = "strict-origin-when-cross-origin"; | 36 frame.referrerPolicy = "strict-origin-when-cross-origin"; |
| 35 frame.sandbox = "allow-downloads allow-forms allow-modals allow-popups allow-same-origin allow-scripts"; | 37 frame.sandbox = "allow-downloads allow-forms allow-modals allow-popups allow-same-origin allow-scripts"; |
| 41 document.body.appendChild(surface); | 43 document.body.appendChild(surface); |
| 42 } | 44 } |
| 43 | 45 |
| 44 surface.dataset.canvasActive = "true"; | 46 surface.dataset.canvasActive = "true"; |
| 45 const frame = surface.firstElementChild; | 47 const frame = surface.firstElementChild; |
| 48 const colorScheme = dark_mode ? "dark" : "light"; | |
| 49 surface.style.colorScheme = colorScheme; | |
| 50 surface.style.background = dark_mode ? "#0f0f11" : "#fff"; | |
| 51 frame.style.colorScheme = colorScheme; | |
| 46 const url = UTF8ToString(p_url); | 52 const url = UTF8ToString(p_url); |
| 47 if (frame.dataset.url !== url) { | 53 if (frame.dataset.url !== url) { |
| 48 frame.dataset.url = url; | 54 frame.dataset.url = url; |
| 49 frame.src = url; | 55 frame.src = url; |
| 50 } | 56 } |
| 54 const scaleY = canvasBounds.height / Math.max(1, canvas.height); | 60 const scaleY = canvasBounds.height / Math.max(1, canvas.height); |
| 55 surface.style.left = `${canvasBounds.left + x * scaleX}px`; | 61 surface.style.left = `${canvasBounds.left + x * scaleX}px`; |
| 56 surface.style.top = `${canvasBounds.top + y * scaleY}px`; | 62 surface.style.top = `${canvasBounds.top + y * scaleY}px`; |
| 57 surface.style.width = `${Math.max(1, width * scaleX)}px`; | 63 surface.style.width = `${Math.max(1, width * scaleX)}px`; |
| 58 surface.style.height = `${Math.max(1, height * scaleY)}px`; | 64 surface.style.height = `${Math.max(1, height * scaleY)}px`; |
| 65 surface.style.opacity = `${opacity}`; | |
| 66 surface.style.pointerEvents = opacity >= 0.99 ? "auto" : "none"; | |
| 67 }); | |
| 68 | |
| 69 EM_JS(void, Canvas_Web_History_Go, (uint32 entity_id, int direction), { | |
| 70 const surface = document.getElementById( | |
| 71 `canvas-web-surface-${entity_id}`); | |
| 72 const frame = surface && surface.firstElementChild; | |
| 73 if (!frame || !frame.contentWindow) return; | |
| 74 try { | |
| 75 frame.contentWindow.history.go(direction); | |
| 76 } catch (error) { | |
| 77 console.warn("Canvas iframe history navigation is unavailable", error); | |
| 78 } | |
| 79 }); | |
| 80 | |
| 81 EM_JS(int, Canvas_Web_Has_Surface, (uint32 entity_id), { | |
| 82 return document.getElementById( | |
| 83 `canvas-web-surface-${entity_id}`) ? 1 : 0; | |
| 59 }); | 84 }); |
| 60 | 85 |
| 61 EM_JS(void, Canvas_Image_Create_Or_Update, ( | 86 EM_JS(void, Canvas_Image_Create_Or_Update, ( |
| 62 uint32 entity_id, | 87 uint32 entity_id, |
| 63 const char *p_source, | 88 const char *p_source, |
| 64 float x, | 89 float x, |
| 65 float y, | 90 float y, |
| 66 float width, | 91 float width, |
| 67 float height), { | 92 float height, |
| 93 float opacity), { | |
| 68 const canvas = Module.canvas; | 94 const canvas = Module.canvas; |
| 69 const surfaceId = `canvas-web-surface-${entity_id}`; | 95 const surfaceId = `canvas-web-surface-${entity_id}`; |
| 70 let surface = document.getElementById(surfaceId); | 96 let surface = document.getElementById(surfaceId); |
| 71 if (!surface) { | 97 if (!surface) { |
| 72 surface = document.createElement("div"); | 98 surface = document.createElement("div"); |
| 119 const scaleY = canvasBounds.height / Math.max(1, canvas.height); | 145 const scaleY = canvasBounds.height / Math.max(1, canvas.height); |
| 120 surface.style.left = `${canvasBounds.left + x * scaleX}px`; | 146 surface.style.left = `${canvasBounds.left + x * scaleX}px`; |
| 121 surface.style.top = `${canvasBounds.top + y * scaleY}px`; | 147 surface.style.top = `${canvasBounds.top + y * scaleY}px`; |
| 122 surface.style.width = `${Math.max(1, width * scaleX)}px`; | 148 surface.style.width = `${Math.max(1, width * scaleX)}px`; |
| 123 surface.style.height = `${Math.max(1, height * scaleY)}px`; | 149 surface.style.height = `${Math.max(1, height * scaleY)}px`; |
| 150 surface.style.opacity = `${opacity}`; | |
| 124 }); | 151 }); |
| 125 | 152 |
| 126 EM_JS(void, Canvas_Web_End_Frame, (), { | 153 EM_JS(void, Canvas_Web_End_Frame, (), { |
| 127 document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { | 154 document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { |
| 128 if (surface.dataset.canvasActive !== "true") { | 155 if (surface.dataset.canvasActive !== "true") { |
| 145 }); | 172 }); |
| 146 }); | 173 }); |
| 147 | 174 |
| 148 static Rectangle Canvas_Web_Content_Bounds( | 175 static Rectangle Canvas_Web_Content_Bounds( |
| 149 const Canvas_Camera *p_camera, | 176 const Canvas_Camera *p_camera, |
| 177 const Canvas_Scene *p_scene, | |
| 150 const Canvas_Entity *p_entity) | 178 const Canvas_Entity *p_entity) |
| 151 { | 179 { |
| 180 (void)p_scene; | |
| 152 float frame_inset = CANVAS_WEB_FRAME_INSET / p_camera->zoom; | 181 float frame_inset = CANVAS_WEB_FRAME_INSET / p_camera->zoom; |
| 182 float amount = p_entity->web_chrome_amount; | |
| 183 if (amount < 0.0f) amount = 0.0f; | |
| 184 if (amount > 1.0f) amount = 1.0f; | |
| 185 float eased = amount * amount * (3.0f - 2.0f * amount); | |
| 186 float toolbar_height = | |
| 187 CANVAS_WEB_TOOLBAR_HEIGHT * eased / p_camera->zoom; | |
| 153 float resize_handle = CANVAS_WEB_RESIZE_HANDLE / p_camera->zoom; | 188 float resize_handle = CANVAS_WEB_RESIZE_HANDLE / p_camera->zoom; |
| 154 Vector2 position = Canvas_Camera_World_To_Screen( | 189 Vector2 position = Canvas_Camera_World_To_Screen( |
| 155 p_camera, | 190 p_camera, |
| 156 (Vector2){ | 191 (Vector2){ |
| 157 p_entity->position.x + frame_inset, | 192 p_entity->position.x + frame_inset, |
| 158 p_entity->position.y + frame_inset, | 193 p_entity->position.y + frame_inset + toolbar_height, |
| 159 }); | 194 }); |
| 160 return (Rectangle){ | 195 return (Rectangle){ |
| 161 position.x, | 196 position.x, |
| 162 position.y, | 197 position.y, |
| 163 (p_entity->size.x - frame_inset - resize_handle) * p_camera->zoom, | 198 (p_entity->size.x - frame_inset - resize_handle) * p_camera->zoom, |
| 164 (p_entity->size.y - frame_inset - resize_handle) * p_camera->zoom, | 199 (p_entity->size.y - frame_inset - resize_handle - toolbar_height) * |
| 200 p_camera->zoom, | |
| 165 }; | 201 }; |
| 166 } | 202 } |
| 167 | 203 |
| 168 static boolean Canvas_Web_Is_Visible( | 204 static boolean Canvas_Web_Is_Visible( |
| 169 const Canvas_Camera *p_camera, | 205 const Canvas_Camera *p_camera, |
| 174 bounds.y + bounds.height > 0.0f && | 210 bounds.y + bounds.height > 0.0f && |
| 175 bounds.x < (float)p_camera->viewport_width && | 211 bounds.x < (float)p_camera->viewport_width && |
| 176 bounds.y < (float)p_camera->viewport_height; | 212 bounds.y < (float)p_camera->viewport_height; |
| 177 } | 213 } |
| 178 | 214 |
| 215 static float Canvas_Web_Entity_Opacity(const Canvas_Entity *p_entity) | |
| 216 { | |
| 217 float amount = p_entity->visibility_amount; | |
| 218 if (amount < 0.0f) amount = 0.0f; | |
| 219 if (amount > 1.0f) amount = 1.0f; | |
| 220 return amount * amount * (3.0f - 2.0f * amount); | |
| 221 } | |
| 222 | |
| 179 int32 Canvas_Web_Surface_Execute_Subprocess(int argc, char **p_argv) | 223 int32 Canvas_Web_Surface_Execute_Subprocess(int argc, char **p_argv) |
| 180 { | 224 { |
| 181 (void)argc; | 225 (void)argc; |
| 182 (void)p_argv; | 226 (void)p_argv; |
| 183 return -1; | 227 return -1; |
| 184 } | 228 } |
| 185 | 229 |
| 186 boolean Canvas_Web_Surface_Init( | 230 boolean Canvas_Web_Surface_Init( |
| 187 Canvas_Web_Surface *p_surface, | 231 Canvas_Web_Surface *p_surface, |
| 188 Dowa_Arena *p_arena) | 232 Dowa_Arena *p_arena, |
| 233 boolean dark_mode) | |
| 189 { | 234 { |
| 190 memset(p_surface, 0, sizeof(*p_surface)); | 235 memset(p_surface, 0, sizeof(*p_surface)); |
| 191 p_surface->p_arena = p_arena; | 236 p_surface->p_arena = p_arena; |
| 237 p_surface->dark_mode = dark_mode; | |
| 192 p_surface->initialized = TRUE; | 238 p_surface->initialized = TRUE; |
| 193 return TRUE; | 239 return TRUE; |
| 240 } | |
| 241 | |
| 242 void Canvas_Web_Surface_Set_Dark_Mode( | |
| 243 Canvas_Web_Surface *p_surface, | |
| 244 boolean dark_mode) | |
| 245 { | |
| 246 p_surface->dark_mode = dark_mode; | |
| 194 } | 247 } |
| 195 | 248 |
| 196 boolean Canvas_Web_Surface_Update_Input( | 249 boolean Canvas_Web_Surface_Update_Input( |
| 197 Canvas_Web_Surface *p_surface, | 250 Canvas_Web_Surface *p_surface, |
| 198 const Canvas_Camera *p_camera, | 251 const Canvas_Camera *p_camera, |
| 220 const Canvas_Entity *p_entity = &p_scene->p_entities[index]; | 273 const Canvas_Entity *p_entity = &p_scene->p_entities[index]; |
| 221 if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT && | 274 if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT && |
| 222 p_entity->type != CANVAS_ENTITY_IMAGE) { | 275 p_entity->type != CANVAS_ENTITY_IMAGE) { |
| 223 continue; | 276 continue; |
| 224 } | 277 } |
| 225 Rectangle bounds = Canvas_Web_Content_Bounds(p_camera, p_entity); | 278 Rectangle bounds = Canvas_Web_Content_Bounds( |
| 279 p_camera, | |
| 280 p_scene, | |
| 281 p_entity); | |
| 226 if (!Canvas_Web_Is_Visible(p_camera, bounds)) continue; | 282 if (!Canvas_Web_Is_Visible(p_camera, bounds)) continue; |
| 283 float opacity = Canvas_Web_Entity_Opacity(p_entity); | |
| 227 if (p_entity->type == CANVAS_ENTITY_IMAGE) { | 284 if (p_entity->type == CANVAS_ENTITY_IMAGE) { |
| 228 Canvas_Image_Create_Or_Update( | 285 Canvas_Image_Create_Or_Update( |
| 229 p_entity->id, | 286 p_entity->id, |
| 230 p_entity->text, | 287 p_entity->text, |
| 231 bounds.x, | 288 bounds.x, |
| 232 bounds.y, | 289 bounds.y, |
| 233 bounds.width, | 290 bounds.width, |
| 234 bounds.height); | 291 bounds.height, |
| 292 opacity); | |
| 235 } else { | 293 } else { |
| 236 Canvas_Web_Create_Or_Update( | 294 Canvas_Web_Create_Or_Update( |
| 237 p_entity->id, | 295 p_entity->id, |
| 238 p_entity->text, | 296 p_entity->text, |
| 239 bounds.x, | 297 bounds.x, |
| 240 bounds.y, | 298 bounds.y, |
| 241 bounds.width, | 299 bounds.width, |
| 242 bounds.height); | 300 bounds.height, |
| 301 opacity, | |
| 302 p_surface->dark_mode ? 1 : 0); | |
| 243 } | 303 } |
| 244 visible_views++; | 304 visible_views++; |
| 245 } | 305 } |
| 246 Canvas_Web_End_Frame(); | 306 Canvas_Web_End_Frame(); |
| 247 } | 307 } |
| 252 const Canvas_Scene *p_scene) | 312 const Canvas_Scene *p_scene) |
| 253 { | 313 { |
| 254 (void)p_surface; | 314 (void)p_surface; |
| 255 (void)p_camera; | 315 (void)p_camera; |
| 256 (void)p_scene; | 316 (void)p_scene; |
| 317 } | |
| 318 | |
| 319 void Canvas_Web_Surface_Draw_Entity( | |
| 320 const Canvas_Web_Surface *p_surface, | |
| 321 const Canvas_Camera *p_camera, | |
| 322 const Canvas_Scene *p_scene, | |
| 323 size_t entity_index) | |
| 324 { | |
| 325 (void)p_surface; | |
| 326 (void)p_camera; | |
| 327 (void)p_scene; | |
| 328 (void)entity_index; | |
| 329 } | |
| 330 | |
| 331 const char *Canvas_Web_Surface_URL( | |
| 332 const Canvas_Web_Surface *p_surface, | |
| 333 uint32 entity_id) | |
| 334 { | |
| 335 (void)p_surface; | |
| 336 (void)entity_id; | |
| 337 return NULL; | |
| 338 } | |
| 339 | |
| 340 boolean Canvas_Web_Surface_Can_Go_Back( | |
| 341 const Canvas_Web_Surface *p_surface, | |
| 342 uint32 entity_id) | |
| 343 { | |
| 344 (void)p_surface; | |
| 345 return Canvas_Web_Has_Surface(entity_id) ? TRUE : FALSE; | |
| 346 } | |
| 347 | |
| 348 boolean Canvas_Web_Surface_Can_Go_Forward( | |
| 349 const Canvas_Web_Surface *p_surface, | |
| 350 uint32 entity_id) | |
| 351 { | |
| 352 (void)p_surface; | |
| 353 return Canvas_Web_Has_Surface(entity_id) ? TRUE : FALSE; | |
| 354 } | |
| 355 | |
| 356 void Canvas_Web_Surface_Go_Back( | |
| 357 Canvas_Web_Surface *p_surface, | |
| 358 uint32 entity_id) | |
| 359 { | |
| 360 (void)p_surface; | |
| 361 Canvas_Web_History_Go(entity_id, -1); | |
| 362 } | |
| 363 | |
| 364 void Canvas_Web_Surface_Go_Forward( | |
| 365 Canvas_Web_Surface *p_surface, | |
| 366 uint32 entity_id) | |
| 367 { | |
| 368 (void)p_surface; | |
| 369 Canvas_Web_History_Go(entity_id, 1); | |
| 257 } | 370 } |
| 258 | 371 |
| 259 boolean Canvas_Web_Surface_Is_Focused(const Canvas_Web_Surface *p_surface) | 372 boolean Canvas_Web_Surface_Is_Focused(const Canvas_Web_Surface *p_surface) |
| 260 { | 373 { |
| 261 (void)p_surface; | 374 (void)p_surface; |