diff infinite_canvas/web_surface_native.cc @ 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 diff
--- a/infinite_canvas/web_surface_native.cc	Mon Aug 17 17:01:40 2026 -0700
+++ b/infinite_canvas/web_surface_native.cc	Mon Aug 17 22:16:14 2026 -0700
@@ -5,6 +5,7 @@
 #include "include/cef_app.h"
 #include "include/cef_browser.h"
 #include "include/cef_client.h"
+#include "include/cef_display_handler.h"
 #include "include/cef_render_handler.h"
 #include "include/capi/cef_app_capi.h"
 
@@ -40,6 +41,27 @@
 
 void FindResourcePaths();
 
+void ApplyColorScheme(CefRefPtr<CefBrowser> browser, boolean dark_mode) {
+  if (!browser) return;
+  CefRefPtr<CefDictionaryValue> params = CefDictionaryValue::Create();
+  CefRefPtr<CefListValue> features = CefListValue::Create();
+  CefRefPtr<CefDictionaryValue> feature = CefDictionaryValue::Create();
+  feature->SetString("name", "prefers-color-scheme");
+  feature->SetString("value", dark_mode ? "dark" : "light");
+  features->SetDictionary(0, feature);
+  params->SetList("features", features);
+  browser->GetHost()->ExecuteDevToolsMethod(
+      0,
+      "Emulation.setEmulatedMedia",
+      params);
+
+  CefRefPtr<CefFrame> frame = browser->GetMainFrame();
+  const char *script = dark_mode
+      ? "document.documentElement.style.colorScheme='dark'"
+      : "document.documentElement.style.colorScheme='light'";
+  frame->ExecuteJavaScript(script, frame->GetURL(), 0);
+}
+
 const Canvas_Entity *FindSurfaceEntity(
     const Canvas_Scene *scene,
     uint32 entity_id) {
@@ -56,19 +78,25 @@
 
 Rectangle ContentBounds(
     const Canvas_Camera *camera,
+    const Canvas_Scene *scene,
     const Canvas_Entity *entity) {
+  (void)scene;
   float frame_inset = CANVAS_WEB_FRAME_INSET / camera->zoom;
+  float amount = std::clamp(entity->web_chrome_amount, 0.0f, 1.0f);
+  float eased = amount * amount * (3.0f - 2.0f * amount);
+  float toolbar_height = CANVAS_WEB_TOOLBAR_HEIGHT * eased / camera->zoom;
   float resize_handle = CANVAS_WEB_RESIZE_HANDLE / camera->zoom;
   Vector2 position = Canvas_Camera_World_To_Screen(
       camera,
       Vector2{
           entity->position.x + frame_inset,
-          entity->position.y + frame_inset});
+          entity->position.y + frame_inset + toolbar_height});
   return Rectangle{
       position.x,
       position.y,
       (entity->size.x - frame_inset - resize_handle) * camera->zoom,
-      (entity->size.y - frame_inset - resize_handle) * camera->zoom,
+      (entity->size.y - frame_inset - resize_handle - toolbar_height) *
+          camera->zoom,
   };
 }
 
@@ -80,6 +108,11 @@
       bounds.y < static_cast<float>(camera->viewport_height);
 }
 
+float EntityOpacity(const Canvas_Entity *entity) {
+  float amount = std::clamp(entity->visibility_amount, 0.0f, 1.0f);
+  return amount * amount * (3.0f - 2.0f * amount);
+}
+
 int32 FrameRateForView(
     const Canvas_Web_Surface *surface,
     const Canvas_Web_View *view,
@@ -133,13 +166,29 @@
 
 class CanvasCefClient : public CefClient,
                         public CefRenderHandler,
+                        public CefDisplayHandler,
                         public CefLifeSpanHandler {
  public:
   explicit CanvasCefClient(Canvas_Web_View *view) : view_(view) {}
 
   CefRefPtr<CefRenderHandler> GetRenderHandler() override { return this; }
+  CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
   CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
 
+  void OnAddressChange(
+      CefRefPtr<CefBrowser> browser,
+      CefRefPtr<CefFrame> frame,
+      const CefString &url) override {
+    if (!frame->IsMain()) return;
+    std::snprintf(
+        view_->url,
+        sizeof(view_->url),
+        "%s",
+        url.ToString().c_str());
+    view_->can_go_back = browser->CanGoBack() ? TRUE : FALSE;
+    view_->can_go_forward = browser->CanGoForward() ? TRUE : FALSE;
+  }
+
   void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) override {
     (void)browser;
     rect = CefRect(0, 0, view_->texture_width, view_->texture_height);
@@ -258,6 +307,17 @@
   return nullptr;
 }
 
+const Canvas_Web_View *FindView(
+    const Canvas_Web_Surface *surface,
+    uint32 entity_id) {
+  for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
+    if (surface->views[index].entity_id == entity_id) {
+      return &surface->views[index];
+    }
+  }
+  return nullptr;
+}
+
 Canvas_Web_View *FindInactiveView(Canvas_Web_Surface *surface) {
   for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
     if (!surface->views[index].active) return &surface->views[index];
@@ -270,7 +330,7 @@
     const Canvas_Camera *camera,
     uint32 entity_id) {
   const Canvas_Entity *entity = FindSurfaceEntity(scene, entity_id);
-  return entity && IsVisible(camera, ContentBounds(camera, entity));
+  return entity && IsVisible(camera, ContentBounds(camera, scene, entity));
 }
 
 boolean EnsureViewResolution(
@@ -331,7 +391,9 @@
   window_info.SetAsWindowless(kNullWindowHandle);
   CefBrowserSettings browser_settings;
   browser_settings.windowless_frame_rate = 30;
-  browser_settings.background_color = CefColorSetARGB(0, 255, 255, 255);
+  browser_settings.background_color = surface->dark_mode
+      ? CefColorSetARGB(255, 15, 15, 17)
+      : CefColorSetARGB(255, 255, 255, 255);
   const char *initial_url =
       view->entity_type == CANVAS_ENTITY_IMAGE ? "about:blank" : url;
   if (!CefBrowserHost::CreateBrowser(
@@ -431,13 +493,18 @@
       return;
     }
     if (entity->type == CANVAS_ENTITY_WEB_CONTENT) {
+      std::snprintf(
+          view->source_url,
+          sizeof(view->source_url),
+          "%s",
+          entity->text);
       std::snprintf(view->url, sizeof(view->url), "%s", entity->text);
     }
     return;
   }
   EnsureViewResolution(surface, view, bounds);
 
-  if (type_changed || std::strcmp(view->url, entity->text) != 0) {
+  if (type_changed || std::strcmp(view->source_url, entity->text) != 0) {
     CefRefPtr<CanvasCefClient> client = state->clients[index];
     if (client && client->browser()) {
       if (entity->type == CANVAS_ENTITY_IMAGE) {
@@ -445,6 +512,11 @@
       } else {
         client->browser()->GetMainFrame()->LoadURL(entity->text);
       }
+      std::snprintf(
+          view->source_url,
+          sizeof(view->source_url),
+          "%s",
+          entity->text);
       std::snprintf(view->url, sizeof(view->url), "%s", entity->text);
     }
   }
@@ -464,10 +536,10 @@
     const Canvas_Entity *entity = &scene->p_entities[index];
     if ((entity->type != CANVAS_ENTITY_WEB_CONTENT &&
          entity->type != CANVAS_ENTITY_IMAGE) ||
-        !IsVisible(camera, ContentBounds(camera, entity))) {
+        !IsVisible(camera, ContentBounds(camera, scene, entity))) {
       continue;
     }
-    Rectangle bounds = ContentBounds(camera, entity);
+    Rectangle bounds = ContentBounds(camera, scene, entity);
     Canvas_Web_View *view = FindView(surface, entity->id);
     if (view) {
       BindView(surface, view, entity, bounds);
@@ -494,7 +566,10 @@
         const Canvas_Entity *entity = FindSurfaceEntity(scene, view->entity_id);
         if (entity) {
           host->SetWindowlessFrameRate(
-              FrameRateForView(surface, view, ContentBounds(camera, entity)));
+              FrameRateForView(
+                  surface,
+                  view,
+                  ContentBounds(camera, scene, entity)));
         }
       }
     }
@@ -540,9 +615,11 @@
 
 extern "C" boolean Canvas_Web_Surface_Init(
     Canvas_Web_Surface *p_surface,
-    Dowa_Arena *p_arena) {
+    Dowa_Arena *p_arena,
+    boolean dark_mode) {
   std::memset(p_surface, 0, sizeof(*p_surface));
   p_surface->p_arena = p_arena;
+  p_surface->dark_mode = dark_mode;
   NativeState *state = static_cast<NativeState *>(
       Dowa_Arena_Allocate(p_arena, sizeof(NativeState)));
   if (!state) return FALSE;
@@ -577,6 +654,13 @@
   return TRUE;
 }
 
+extern "C" void Canvas_Web_Surface_Set_Dark_Mode(
+    Canvas_Web_Surface *p_surface,
+    boolean dark_mode) {
+  if (!p_surface->initialized || p_surface->dark_mode == dark_mode) return;
+  p_surface->dark_mode = dark_mode;
+}
+
 extern "C" boolean Canvas_Web_Surface_Update_Input(
     Canvas_Web_Surface *p_surface,
     const Canvas_Camera *p_camera,
@@ -588,17 +672,25 @@
   const Canvas_Entity *pointer_entity = nullptr;
   Rectangle pointer_bounds = {};
 
-  for (int32 index = CANVAS_MAX_WEB_VIEWS - 1; index >= 0; --index) {
-    Canvas_Web_View *view = &p_surface->views[index];
-    if (!view->active) continue;
-    const Canvas_Entity *entity = FindSurfaceEntity(p_scene, view->entity_id);
-    if (!entity || entity->type != CANVAS_ENTITY_WEB_CONTENT) continue;
-    Rectangle bounds = ContentBounds(p_camera, entity);
-    if (CheckCollisionPointRec(mouse, bounds)) {
-      pointer_view = view;
-      pointer_entity = entity;
-      pointer_bounds = bounds;
-      break;
+  Vector2 world_mouse = Canvas_Camera_Screen_To_World(p_camera, mouse);
+  int32 top_index = Canvas_Scene_Topmost_Visual(
+      p_scene,
+      world_mouse,
+      p_camera->zoom);
+  if (top_index >= 0) {
+    const Canvas_Entity *entity = &p_scene->p_entities[top_index];
+    if (entity->type == CANVAS_ENTITY_WEB_CONTENT &&
+        !entity->removing &&
+        EntityOpacity(entity) >= 0.99f) {
+      Canvas_Web_View *view = FindView(p_surface, entity->id);
+      Rectangle bounds = ContentBounds(p_camera, p_scene, entity);
+      if (view &&
+          view->active &&
+          CheckCollisionPointRec(mouse, bounds)) {
+        pointer_view = view;
+        pointer_entity = entity;
+        pointer_bounds = bounds;
+      }
     }
   }
 
@@ -625,7 +717,9 @@
       FindSurfaceEntity(p_scene, input_view->entity_id);
   if (!browser || !input_entity) return pointer_view != nullptr;
 
-  Rectangle bounds = pointer_view ? pointer_bounds : ContentBounds(p_camera, input_entity);
+  Rectangle bounds = pointer_view
+      ? pointer_bounds
+      : ContentBounds(p_camera, p_scene, input_entity);
   boolean inside = pointer_view == input_view;
   float scale_x = static_cast<float>(input_view->texture_width) /
       std::max(bounds.width, 1.0f);
@@ -647,7 +741,14 @@
   if (inside) {
     float wheel = GetMouseWheelMove();
     if (wheel != 0.0f) {
-      host->SendMouseWheelEvent(event, 0, static_cast<int>(wheel * 80.0f));
+      boolean control_zoom =
+          IsKeyDown(KEY_LEFT_CONTROL) ||
+          IsKeyDown(KEY_RIGHT_CONTROL) ||
+          IsKeyDown(KEY_LEFT_SUPER) ||
+          IsKeyDown(KEY_RIGHT_SUPER);
+      if (!control_zoom) {
+        host->SendMouseWheelEvent(event, 0, static_cast<int>(wheel * 80.0f));
+      }
     }
   }
 
@@ -687,8 +788,15 @@
       host->SendKeyEvent(key);
     }
   }
-  return inside ||
-      p_surface->focused_entity_id == input_view->entity_id;
+  if (inside &&
+      (IsKeyDown(KEY_LEFT_CONTROL) ||
+       IsKeyDown(KEY_RIGHT_CONTROL) ||
+       IsKeyDown(KEY_LEFT_SUPER) ||
+       IsKeyDown(KEY_RIGHT_SUPER)) &&
+      GetMouseWheelMove() != 0.0f) {
+    return FALSE;
+  }
+  return inside;
 }
 
 extern "C" void Canvas_Web_Surface_Sync(
@@ -720,6 +828,19 @@
       UpdateTexture(view->texture, view->p_pixels);
       view->pixels_dirty = FALSE;
     }
+    NativeState *state = static_cast<NativeState *>(p_surface->p_native);
+    CefRefPtr<CanvasCefClient> client = state->clients[index];
+    if (client && client->browser() &&
+        view->entity_type == CANVAS_ENTITY_WEB_CONTENT) {
+      if (!view->color_scheme_applied ||
+          view->applied_dark_mode != p_surface->dark_mode) {
+        ApplyColorScheme(client->browser(), p_surface->dark_mode);
+        view->color_scheme_applied = TRUE;
+        view->applied_dark_mode = p_surface->dark_mode;
+      }
+      view->can_go_back = client->browser()->CanGoBack() ? TRUE : FALSE;
+      view->can_go_forward = client->browser()->CanGoForward() ? TRUE : FALSE;
+    }
   }
 }
 
@@ -727,40 +848,110 @@
     const Canvas_Web_Surface *p_surface,
     const Canvas_Camera *p_camera,
     const Canvas_Scene *p_scene) {
-  for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
-    const Canvas_Web_View *view = &p_surface->views[index];
-    if (!view->active || !view->texture_ready) continue;
-    const Canvas_Entity *entity = FindSurfaceEntity(p_scene, view->entity_id);
-    if (!entity) continue;
-    Rectangle bounds = ContentBounds(p_camera, entity);
-    float left = std::max(bounds.x, 0.0f);
-    float top = std::max(bounds.y, 0.0f);
-    float right = std::min(
-        bounds.x + bounds.width,
-        static_cast<float>(p_camera->viewport_width));
-    float bottom = std::min(
-        bounds.y + bounds.height,
-        static_cast<float>(p_camera->viewport_height));
-    if (right <= left || bottom <= top) continue;
+  for (size_t index = 0;
+      index < Dowa_Array_Length(p_scene->p_entities);
+      ++index) {
+    Canvas_Web_Surface_Draw_Entity(
+        p_surface,
+        p_camera,
+        p_scene,
+        index);
+  }
+}
+
+extern "C" 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) {
+  if (entity_index >= Dowa_Array_Length(p_scene->p_entities)) return;
+  const Canvas_Entity *entity = &p_scene->p_entities[entity_index];
+  if (entity->type != CANVAS_ENTITY_WEB_CONTENT &&
+      entity->type != CANVAS_ENTITY_IMAGE) {
+    return;
+  }
+  const Canvas_Web_View *view = FindView(p_surface, entity->id);
+  if (!view || !view->active || !view->texture_ready) return;
+
+  Rectangle bounds = ContentBounds(p_camera, p_scene, entity);
+  float left = std::max(bounds.x, 0.0f);
+  float top = std::max(bounds.y, 0.0f);
+  float right = std::min(
+      bounds.x + bounds.width,
+      static_cast<float>(p_camera->viewport_width));
+  float bottom = std::min(
+      bounds.y + bounds.height,
+      static_cast<float>(p_camera->viewport_height));
+  if (right <= left || bottom <= top) return;
 
-    BeginScissorMode(
-        static_cast<int>(left),
-        static_cast<int>(top),
-        static_cast<int>(right - left),
-        static_cast<int>(bottom - top));
-    DrawTexturePro(
-        view->texture,
-        Rectangle{
-            0.0f,
-            0.0f,
-            static_cast<float>(view->texture_width),
-            static_cast<float>(view->texture_height),
-        },
-        bounds,
-        Vector2{0.0f, 0.0f},
-        0.0f,
-        WHITE);
-    EndScissorMode();
+  BeginScissorMode(
+      static_cast<int>(left),
+      static_cast<int>(top),
+      static_cast<int>(right - left),
+      static_cast<int>(bottom - top));
+  DrawTexturePro(
+      view->texture,
+      Rectangle{
+          0.0f,
+          0.0f,
+          static_cast<float>(view->texture_width),
+          static_cast<float>(view->texture_height),
+      },
+      bounds,
+      Vector2{0.0f, 0.0f},
+      0.0f,
+      Color{
+          255,
+          255,
+          255,
+          static_cast<unsigned char>(EntityOpacity(entity) * 255.0f)});
+  EndScissorMode();
+}
+
+extern "C" const char *Canvas_Web_Surface_URL(
+    const Canvas_Web_Surface *p_surface,
+    uint32 entity_id) {
+  const Canvas_Web_View *view = FindView(p_surface, entity_id);
+  return view && view->url[0] ? view->url : nullptr;
+}
+
+extern "C" boolean Canvas_Web_Surface_Can_Go_Back(
+    const Canvas_Web_Surface *p_surface,
+    uint32 entity_id) {
+  const Canvas_Web_View *view = FindView(p_surface, entity_id);
+  return view ? view->can_go_back : FALSE;
+}
+
+extern "C" boolean Canvas_Web_Surface_Can_Go_Forward(
+    const Canvas_Web_Surface *p_surface,
+    uint32 entity_id) {
+  const Canvas_Web_View *view = FindView(p_surface, entity_id);
+  return view ? view->can_go_forward : FALSE;
+}
+
+extern "C" void Canvas_Web_Surface_Go_Back(
+    Canvas_Web_Surface *p_surface,
+    uint32 entity_id) {
+  if (!p_surface->p_native) return;
+  Canvas_Web_View *view = FindView(p_surface, entity_id);
+  if (!view) return;
+  NativeState *state = static_cast<NativeState *>(p_surface->p_native);
+  CefRefPtr<CanvasCefClient> client = state->clients[ViewIndex(p_surface, view)];
+  if (client && client->browser() && client->browser()->CanGoBack()) {
+    client->browser()->GoBack();
+  }
+}
+
+extern "C" void Canvas_Web_Surface_Go_Forward(
+    Canvas_Web_Surface *p_surface,
+    uint32 entity_id) {
+  if (!p_surface->p_native) return;
+  Canvas_Web_View *view = FindView(p_surface, entity_id);
+  if (!view) return;
+  NativeState *state = static_cast<NativeState *>(p_surface->p_native);
+  CefRefPtr<CanvasCefClient> client = state->clients[ViewIndex(p_surface, view)];
+  if (client && client->browser() && client->browser()->CanGoForward()) {
+    client->browser()->GoForward();
   }
 }