diff infinite_canvas/web_surface_native.cc @ 276:b55c22cff335

Add interactive infinite canvas prototype
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 16:57:56 -0700
parents
children 8d560f50ed4c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/infinite_canvas/web_surface_native.cc	Mon Aug 17 16:57:56 2026 -0700
@@ -0,0 +1,790 @@
+extern "C" {
+#include "infinite_canvas/web_surface.h"
+}
+
+#include "include/cef_app.h"
+#include "include/cef_browser.h"
+#include "include/cef_client.h"
+#include "include/cef_render_handler.h"
+#include "include/capi/cef_app_capi.h"
+
+#include <algorithm>
+#include <cmath>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <filesystem>
+#include <new>
+
+#if defined(OS_LINUX)
+#include <dlfcn.h>
+#include <limits.h>
+#endif
+
+namespace {
+
+constexpr int32 kResolutionStep = 64;
+constexpr int32 kMinTextureWidth = 160;
+constexpr int32 kMinTextureHeight = 120;
+constexpr int32 kMaxTextureWidth = 1280;
+constexpr int32 kMaxTextureHeight = 800;
+int g_argc = 0;
+char **g_argv = nullptr;
+char *g_cef_argv[128] = {};
+#if defined(OS_LINUX)
+char g_resources_path[PATH_MAX + 32] = {};
+char g_locales_path[PATH_MAX + 32] = {};
+char g_resources_switch[PATH_MAX + 64] = {};
+char g_locales_switch[PATH_MAX + 64] = {};
+#endif
+
+void FindResourcePaths();
+
+const Canvas_Entity *FindSurfaceEntity(
+    const Canvas_Scene *scene,
+    uint32 entity_id) {
+  for (size_t index = 0; index < Dowa_Array_Length(scene->p_entities); ++index) {
+    const Canvas_Entity *entity = &scene->p_entities[index];
+    if ((entity->type == CANVAS_ENTITY_WEB_CONTENT ||
+         entity->type == CANVAS_ENTITY_IMAGE) &&
+        entity->id == entity_id) {
+      return entity;
+    }
+  }
+  return nullptr;
+}
+
+Rectangle ContentBounds(
+    const Canvas_Camera *camera,
+    const Canvas_Entity *entity) {
+  float frame_inset = CANVAS_WEB_FRAME_INSET / 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});
+  return Rectangle{
+      position.x,
+      position.y,
+      (entity->size.x - frame_inset - resize_handle) * camera->zoom,
+      (entity->size.y - frame_inset - resize_handle) * camera->zoom,
+  };
+}
+
+boolean IsVisible(const Canvas_Camera *camera, Rectangle bounds) {
+  return camera->zoom >= 0.30f &&
+      bounds.x + bounds.width > 0.0f &&
+      bounds.y + bounds.height > 0.0f &&
+      bounds.x < static_cast<float>(camera->viewport_width) &&
+      bounds.y < static_cast<float>(camera->viewport_height);
+}
+
+int32 FrameRateForView(
+    const Canvas_Web_Surface *surface,
+    const Canvas_Web_View *view,
+    Rectangle bounds) {
+  if (surface->focused_entity_id == view->entity_id) return 30;
+  float area = std::max(bounds.width, 0.0f) * std::max(bounds.height, 0.0f);
+  return area <= 220000.0f ? 12 : 20;
+}
+
+int32 BucketDimension(float value, int32 minimum, int32 maximum) {
+  int32 rounded = static_cast<int32>(
+      std::ceil(std::max(value, 1.0f) / kResolutionStep)) * kResolutionStep;
+  return std::clamp(rounded, minimum, maximum);
+}
+
+class CanvasCefApp : public CefApp,
+                     public CefBrowserProcessHandler {
+ public:
+  CanvasCefApp() = default;
+
+  CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override {
+    return this;
+  }
+
+  void OnBeforeCommandLineProcessing(
+      const CefString &process_type,
+      CefRefPtr<CefCommandLine> command_line) override {
+    (void)process_type;
+    command_line->AppendSwitch("no-sandbox");
+    command_line->AppendSwitch("disable-gpu");
+    command_line->AppendSwitch("disable-gpu-compositing");
+    command_line->AppendSwitch("allow-file-access-from-files");
+    command_line->AppendSwitch("allow-universal-access-from-files");
+    command_line->AppendSwitchWithValue("log-severity", "disable");
+#if defined(OS_LINUX)
+    if (g_resources_path[0]) {
+      command_line->AppendSwitchWithValue(
+          "resources-dir-path",
+          g_resources_path);
+      command_line->AppendSwitchWithValue(
+          "locales-dir-path",
+          g_locales_path);
+    }
+#endif
+  }
+
+ private:
+  IMPLEMENT_REFCOUNTING(CanvasCefApp);
+  DISALLOW_COPY_AND_ASSIGN(CanvasCefApp);
+};
+
+class CanvasCefClient : public CefClient,
+                        public CefRenderHandler,
+                        public CefLifeSpanHandler {
+ public:
+  explicit CanvasCefClient(Canvas_Web_View *view) : view_(view) {}
+
+  CefRefPtr<CefRenderHandler> GetRenderHandler() override { return this; }
+  CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
+
+  void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) override {
+    (void)browser;
+    rect = CefRect(0, 0, view_->texture_width, view_->texture_height);
+  }
+
+  void OnPaint(
+      CefRefPtr<CefBrowser> browser,
+      PaintElementType type,
+      const RectList &dirty_rects,
+      const void *buffer,
+      int width,
+      int height) override {
+    (void)browser;
+    (void)dirty_rects;
+    if (type != PET_VIEW || !view_->p_pixels ||
+        width != view_->texture_width ||
+        height != view_->texture_height) {
+      return;
+    }
+
+    const uint8 *source = static_cast<const uint8 *>(buffer);
+    uint8 *target = view_->p_pixels;
+    const size_t pixel_count = static_cast<size_t>(width) * height;
+    for (size_t index = 0; index < pixel_count; ++index) {
+      target[index * 4 + 0] = source[index * 4 + 2];
+      target[index * 4 + 1] = source[index * 4 + 1];
+      target[index * 4 + 2] = source[index * 4 + 0];
+      target[index * 4 + 3] = source[index * 4 + 3];
+    }
+    view_->pixels_dirty = TRUE;
+  }
+
+  void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
+    browser_ = browser;
+    browser_->GetHost()->WasHidden(!view_->active);
+  }
+
+  void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
+    (void)browser;
+    browser_ = nullptr;
+  }
+
+  CefRefPtr<CefBrowser> browser() const { return browser_; }
+
+ private:
+  Canvas_Web_View *view_;
+  CefRefPtr<CefBrowser> browser_;
+
+  IMPLEMENT_REFCOUNTING(CanvasCefClient);
+  DISALLOW_COPY_AND_ASSIGN(CanvasCefClient);
+};
+
+struct NativeState {
+  CefRefPtr<CanvasCefClient> clients[CANVAS_MAX_WEB_VIEWS];
+  boolean cef_initialized;
+};
+
+CefMainArgs MainArgs(int argc, char **argv) {
+#if defined(OS_WIN)
+  (void)argc;
+  (void)argv;
+  return CefMainArgs(GetModuleHandle(nullptr));
+#else
+  return CefMainArgs(argc, argv);
+#endif
+}
+
+#if defined(OS_LINUX)
+void FindResourcePaths() {
+  if (g_resources_path[0]) return;
+  Dl_info info = {};
+  if (!dladdr(reinterpret_cast<void *>(&cef_initialize), &info) ||
+      !info.dli_fname) {
+    return;
+  }
+
+  char root[PATH_MAX] = {};
+  if (!realpath(info.dli_fname, root)) return;
+  char *release = std::strstr(root, "/Release/libcef.so");
+  if (!release) return;
+  *release = '\0';
+
+  std::snprintf(
+      g_resources_path,
+      sizeof(g_resources_path),
+      "%s/Resources",
+      root);
+  std::snprintf(
+      g_locales_path,
+      sizeof(g_locales_path),
+      "%s/Resources/locales",
+      root);
+}
+
+void ConfigureLinuxResourcePaths(CefSettings &settings) {
+  FindResourcePaths();
+  CefString(&settings.resources_dir_path) = g_resources_path;
+  CefString(&settings.locales_dir_path) = g_locales_path;
+}
+#else
+void FindResourcePaths() {}
+#endif
+
+int32 ViewIndex(
+    const Canvas_Web_Surface *surface,
+    const Canvas_Web_View *view) {
+  return static_cast<int32>(view - surface->views);
+}
+
+Canvas_Web_View *FindView(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];
+  }
+  return nullptr;
+}
+
+boolean SceneHasVisibleEntity(
+    const Canvas_Scene *scene,
+    const Canvas_Camera *camera,
+    uint32 entity_id) {
+  const Canvas_Entity *entity = FindSurfaceEntity(scene, entity_id);
+  return entity && IsVisible(camera, ContentBounds(camera, entity));
+}
+
+boolean EnsureViewResolution(
+    Canvas_Web_Surface *surface,
+    Canvas_Web_View *view,
+    Rectangle bounds) {
+  int32 width = BucketDimension(
+      bounds.width,
+      kMinTextureWidth,
+      kMaxTextureWidth);
+  int32 height = BucketDimension(
+      bounds.height,
+      kMinTextureHeight,
+      kMaxTextureHeight);
+  if (view->p_pixels &&
+      view->texture_width == width &&
+      view->texture_height == height) {
+    return TRUE;
+  }
+
+  size_t pixel_capacity =
+      static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
+  void *resized_pixels = std::realloc(view->p_pixels, pixel_capacity);
+  if (!resized_pixels) return FALSE;
+  view->p_pixels = static_cast<uint8 *>(resized_pixels);
+  view->pixel_capacity = pixel_capacity;
+  view->texture_width = width;
+  view->texture_height = height;
+  std::memset(view->p_pixels, 255, pixel_capacity);
+  view->pixels_dirty = FALSE;
+
+  if (view->texture_ready) {
+    UnloadTexture(view->texture);
+    view->texture = {};
+    view->texture_ready = FALSE;
+  }
+
+  NativeState *state = static_cast<NativeState *>(surface->p_native);
+  int32 index = ViewIndex(surface, view);
+  CefRefPtr<CanvasCefClient> client = state->clients[index];
+  if (client && client->browser()) {
+    client->browser()->GetHost()->WasResized();
+  }
+  return TRUE;
+}
+
+boolean CreateBrowser(
+    Canvas_Web_Surface *surface,
+    Canvas_Web_View *view,
+    const char *url,
+    Rectangle bounds) {
+  NativeState *state = static_cast<NativeState *>(surface->p_native);
+  int32 index = ViewIndex(surface, view);
+  if (!EnsureViewResolution(surface, view, bounds)) return FALSE;
+
+  state->clients[index] = new CanvasCefClient(view);
+  CefWindowInfo window_info;
+  window_info.SetAsWindowless(kNullWindowHandle);
+  CefBrowserSettings browser_settings;
+  browser_settings.windowless_frame_rate = 30;
+  browser_settings.background_color = CefColorSetARGB(0, 255, 255, 255);
+  const char *initial_url =
+      view->entity_type == CANVAS_ENTITY_IMAGE ? "about:blank" : url;
+  if (!CefBrowserHost::CreateBrowser(
+          window_info,
+          state->clients[index],
+          initial_url,
+          browser_settings,
+          nullptr,
+          nullptr)) {
+    state->clients[index] = nullptr;
+    return FALSE;
+  }
+  view->browser_created = TRUE;
+  return TRUE;
+}
+
+void PercentEncode(
+    const char *source,
+    char *target,
+    size_t target_size) {
+  static const char hex[] = "0123456789ABCDEF";
+  size_t output = 0;
+  for (size_t index = 0; source[index] && output + 4 < target_size; ++index) {
+    uint8 value = static_cast<uint8>(source[index]);
+    boolean unreserved =
+        (value >= 'a' && value <= 'z') ||
+        (value >= 'A' && value <= 'Z') ||
+        (value >= '0' && value <= '9') ||
+        value == '-' || value == '_' || value == '.' || value == '~';
+    if (unreserved) {
+      target[output++] = static_cast<char>(value);
+    } else {
+      target[output++] = '%';
+      target[output++] = hex[value >> 4];
+      target[output++] = hex[value & 15];
+    }
+  }
+  target[output] = '\0';
+}
+
+void LoadImageSource(
+    CefRefPtr<CefBrowser> browser,
+    const char *source) {
+  char resolved[512] = {};
+  boolean local_file =
+      !std::strstr(source, "://") && std::strncmp(source, "data:", 5) != 0;
+  if (!local_file) {
+    std::snprintf(resolved, sizeof(resolved), "%s", source);
+  } else {
+    std::error_code error;
+    std::filesystem::path absolute = std::filesystem::absolute(source, error);
+    if (error) {
+      std::snprintf(resolved, sizeof(resolved), "%s", source);
+    } else {
+      std::snprintf(
+          resolved,
+          sizeof(resolved),
+          "file://%s",
+          absolute.generic_string().c_str());
+    }
+  }
+  std::error_code viewer_error;
+  std::filesystem::path viewer_path = std::filesystem::absolute(
+      "infinite_canvas/assets/image-view.html",
+      viewer_error);
+  if (viewer_error) {
+    browser->GetMainFrame()->LoadURL(resolved);
+    return;
+  }
+  char encoded_source[2048] = {};
+  PercentEncode(resolved, encoded_source, sizeof(encoded_source));
+  char viewer_url[3072] = {};
+  std::snprintf(
+      viewer_url,
+      sizeof(viewer_url),
+      "file://%s?src=%s",
+      viewer_path.generic_string().c_str(),
+      encoded_source);
+  browser->GetMainFrame()->LoadURL(viewer_url);
+}
+
+void BindView(
+    Canvas_Web_Surface *surface,
+    Canvas_Web_View *view,
+    const Canvas_Entity *entity,
+    Rectangle bounds) {
+  NativeState *state = static_cast<NativeState *>(surface->p_native);
+  int32 index = ViewIndex(surface, view);
+  boolean type_changed = view->entity_type != entity->type;
+  view->active = TRUE;
+  view->entity_id = entity->id;
+  view->entity_type = entity->type;
+  if (!view->browser_created) {
+    if (!CreateBrowser(surface, view, entity->text, bounds)) {
+      view->active = FALSE;
+      view->entity_id = 0;
+      return;
+    }
+    if (entity->type == CANVAS_ENTITY_WEB_CONTENT) {
+      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) {
+    CefRefPtr<CanvasCefClient> client = state->clients[index];
+    if (client && client->browser()) {
+      if (entity->type == CANVAS_ENTITY_IMAGE) {
+        LoadImageSource(client->browser(), entity->text);
+      } else {
+        client->browser()->GetMainFrame()->LoadURL(entity->text);
+      }
+      std::snprintf(view->url, sizeof(view->url), "%s", entity->text);
+    }
+  }
+}
+
+void ReconcileViews(
+    Canvas_Web_Surface *surface,
+    const Canvas_Camera *camera,
+    const Canvas_Scene *scene) {
+  for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
+    Canvas_Web_View *view = &surface->views[index];
+    view->active = view->entity_id != 0 &&
+        SceneHasVisibleEntity(scene, camera, view->entity_id);
+  }
+
+  for (size_t index = 0; index < Dowa_Array_Length(scene->p_entities); ++index) {
+    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))) {
+      continue;
+    }
+    Rectangle bounds = ContentBounds(camera, entity);
+    Canvas_Web_View *view = FindView(surface, entity->id);
+    if (view) {
+      BindView(surface, view, entity, bounds);
+      continue;
+    }
+    view = FindInactiveView(surface);
+    if (!view) break;
+    BindView(surface, view, entity, bounds);
+  }
+
+  if (surface->focused_entity_id != 0) {
+    Canvas_Web_View *focused = FindView(surface, surface->focused_entity_id);
+    if (!focused || !focused->active) surface->focused_entity_id = 0;
+  }
+
+  NativeState *state = static_cast<NativeState *>(surface->p_native);
+  for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
+    CefRefPtr<CanvasCefClient> client = state->clients[index];
+    if (client && client->browser()) {
+      Canvas_Web_View *view = &surface->views[index];
+      CefRefPtr<CefBrowserHost> host = client->browser()->GetHost();
+      host->WasHidden(!view->active);
+      if (view->active) {
+        const Canvas_Entity *entity = FindSurfaceEntity(scene, view->entity_id);
+        if (entity) {
+          host->SetWindowlessFrameRate(
+              FrameRateForView(surface, view, ContentBounds(camera, entity)));
+        }
+      }
+    }
+  }
+}
+
+}  // namespace
+
+extern "C" int32 Canvas_Web_Surface_Execute_Subprocess(
+    int argc,
+    char **p_argv) {
+  g_argc = argc;
+  FindResourcePaths();
+  int32 cef_argc = 0;
+  for (; cef_argc < argc && cef_argc < 120; ++cef_argc) {
+    g_cef_argv[cef_argc] = p_argv[cef_argc];
+  }
+#if defined(OS_LINUX)
+  if (g_resources_path[0]) {
+    std::snprintf(
+        g_resources_switch,
+        sizeof(g_resources_switch),
+        "--resources-dir-path=%s",
+        g_resources_path);
+    std::snprintf(
+        g_locales_switch,
+        sizeof(g_locales_switch),
+        "--locales-dir-path=%s",
+        g_locales_path);
+    g_cef_argv[cef_argc++] = g_resources_switch;
+    g_cef_argv[cef_argc++] = g_locales_switch;
+  }
+#endif
+  g_cef_argv[cef_argc++] = const_cast<char *>("--no-sandbox");
+  g_cef_argv[cef_argc++] = const_cast<char *>("--disable-gpu");
+  g_cef_argv[cef_argc++] = const_cast<char *>("--disable-gpu-compositing");
+  g_argc = cef_argc;
+  g_argv = g_cef_argv;
+  CefMainArgs args = MainArgs(g_argc, g_argv);
+  CefRefPtr<CanvasCefApp> app = new CanvasCefApp();
+  return CefExecuteProcess(args, app, nullptr);
+}
+
+extern "C" boolean Canvas_Web_Surface_Init(
+    Canvas_Web_Surface *p_surface,
+    Dowa_Arena *p_arena) {
+  std::memset(p_surface, 0, sizeof(*p_surface));
+  p_surface->p_arena = p_arena;
+  NativeState *state = static_cast<NativeState *>(
+      Dowa_Arena_Allocate(p_arena, sizeof(NativeState)));
+  if (!state) return FALSE;
+
+  new (state) NativeState();
+  p_surface->p_native = state;
+
+  CefSettings settings;
+  settings.windowless_rendering_enabled = true;
+  settings.no_sandbox = true;
+  settings.multi_threaded_message_loop = false;
+  settings.external_message_pump = false;
+  settings.log_severity = LOGSEVERITY_DISABLE;
+  const char *cache_path = std::getenv("INFINITE_CANVAS_CEF_CACHE");
+  if (cache_path) {
+    CefString(&settings.root_cache_path) = cache_path;
+    CefString(&settings.cache_path) = cache_path;
+  }
+#if defined(OS_LINUX)
+  ConfigureLinuxResourcePaths(settings);
+#endif
+
+  CefMainArgs args = MainArgs(g_argc, g_argv);
+  CefRefPtr<CanvasCefApp> app = new CanvasCefApp();
+  if (!CefInitialize(args, settings, app, nullptr)) {
+    state->~NativeState();
+    p_surface->p_native = nullptr;
+    return FALSE;
+  }
+  state->cef_initialized = TRUE;
+  p_surface->initialized = TRUE;
+  return TRUE;
+}
+
+extern "C" boolean Canvas_Web_Surface_Update_Input(
+    Canvas_Web_Surface *p_surface,
+    const Canvas_Camera *p_camera,
+    const Canvas_Scene *p_scene) {
+  if (!p_surface->initialized || !p_surface->p_native) return FALSE;
+  NativeState *state = static_cast<NativeState *>(p_surface->p_native);
+  Vector2 mouse = GetMousePosition();
+  Canvas_Web_View *pointer_view = nullptr;
+  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;
+    }
+  }
+
+  if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+    p_surface->focused_entity_id = pointer_view ? pointer_view->entity_id : 0;
+    for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
+      CefRefPtr<CanvasCefClient> client = state->clients[index];
+      if (client && client->browser()) {
+        client->browser()->GetHost()->SetFocus(
+            pointer_view == &p_surface->views[index]);
+      }
+    }
+  }
+
+  Canvas_Web_View *focused_view =
+      FindView(p_surface, p_surface->focused_entity_id);
+  Canvas_Web_View *input_view = pointer_view ? pointer_view : focused_view;
+  if (!input_view || !input_view->active) return FALSE;
+  int32 view_index = ViewIndex(p_surface, input_view);
+  CefRefPtr<CanvasCefClient> client = state->clients[view_index];
+  CefRefPtr<CefBrowser> browser = client ? client->browser() : nullptr;
+  const Canvas_Entity *input_entity = pointer_entity ?
+      pointer_entity :
+      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);
+  boolean inside = pointer_view == input_view;
+  float scale_x = static_cast<float>(input_view->texture_width) /
+      std::max(bounds.width, 1.0f);
+  float scale_y = static_cast<float>(input_view->texture_height) /
+      std::max(bounds.height, 1.0f);
+  CefMouseEvent event;
+  event.x = static_cast<int>((mouse.x - bounds.x) * scale_x);
+  event.y = static_cast<int>((mouse.y - bounds.y) * scale_y);
+  event.modifiers = 0;
+  CefRefPtr<CefBrowserHost> host = browser->GetHost();
+  host->SendMouseMoveEvent(event, !inside);
+  if (inside && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+    host->SendMouseClickEvent(event, MBT_LEFT, false, 1);
+  }
+  if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) &&
+      p_surface->focused_entity_id == input_view->entity_id) {
+    host->SendMouseClickEvent(event, MBT_LEFT, true, 1);
+  }
+  if (inside) {
+    float wheel = GetMouseWheelMove();
+    if (wheel != 0.0f) {
+      host->SendMouseWheelEvent(event, 0, static_cast<int>(wheel * 80.0f));
+    }
+  }
+
+  if (p_surface->focused_entity_id == input_view->entity_id) {
+    int32 codepoint = GetCharPressed();
+    while (codepoint > 0) {
+      CefKeyEvent key;
+      key.type = KEYEVENT_CHAR;
+      key.windows_key_code = codepoint;
+      key.native_key_code = codepoint;
+      key.character = static_cast<char16_t>(codepoint);
+      key.unmodified_character = static_cast<char16_t>(codepoint);
+      host->SendKeyEvent(key);
+      codepoint = GetCharPressed();
+    }
+
+    struct {
+      int32 raylib_key;
+      int32 browser_key;
+    } keys[] = {
+        {KEY_BACKSPACE, 0x08},
+        {KEY_ENTER, 0x0D},
+        {KEY_LEFT, 0x25},
+        {KEY_UP, 0x26},
+        {KEY_RIGHT, 0x27},
+        {KEY_DOWN, 0x28},
+        {KEY_DELETE, 0x2E},
+    };
+    for (const auto &key_mapping : keys) {
+      if (!IsKeyPressed(key_mapping.raylib_key)) continue;
+      CefKeyEvent key;
+      key.windows_key_code = key_mapping.browser_key;
+      key.native_key_code = key_mapping.browser_key;
+      key.type = KEYEVENT_RAWKEYDOWN;
+      host->SendKeyEvent(key);
+      key.type = KEYEVENT_KEYUP;
+      host->SendKeyEvent(key);
+    }
+  }
+  return inside ||
+      p_surface->focused_entity_id == input_view->entity_id;
+}
+
+extern "C" 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;
+  CefDoMessageLoopWork();
+  ReconcileViews(p_surface, p_camera, p_scene);
+
+  for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
+    Canvas_Web_View *view = &p_surface->views[index];
+    if (!view->browser_created) continue;
+    if (!view->texture_ready && IsWindowReady()) {
+      Image image = {
+          .data = view->p_pixels,
+          .width = view->texture_width,
+          .height = view->texture_height,
+          .mipmaps = 1,
+          .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
+      };
+      view->texture = LoadTextureFromImage(image);
+      view->texture_ready = view->texture.id != 0 ? TRUE : FALSE;
+      if (view->texture_ready) {
+        SetTextureFilter(view->texture, TEXTURE_FILTER_BILINEAR);
+      }
+    }
+    if (view->texture_ready && view->pixels_dirty) {
+      UpdateTexture(view->texture, view->p_pixels);
+      view->pixels_dirty = FALSE;
+    }
+  }
+}
+
+extern "C" void Canvas_Web_Surface_Draw(
+    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;
+
+    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();
+  }
+}
+
+extern "C" boolean Canvas_Web_Surface_Is_Focused(
+    const Canvas_Web_Surface *p_surface) {
+  return p_surface->focused_entity_id != 0;
+}
+
+extern "C" void Canvas_Web_Surface_Shutdown(Canvas_Web_Surface *p_surface) {
+  if (!p_surface->p_native) return;
+  NativeState *state = static_cast<NativeState *>(p_surface->p_native);
+  for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
+    if (state->clients[index] && state->clients[index]->browser()) {
+      state->clients[index]->browser()->GetHost()->CloseBrowser(true);
+    }
+    state->clients[index] = nullptr;
+    if (p_surface->views[index].texture_ready) {
+      UnloadTexture(p_surface->views[index].texture);
+    }
+    Dowa_Free(p_surface->views[index].p_pixels);
+  }
+  CefDoMessageLoopWork();
+  if (state->cef_initialized) CefShutdown();
+  state->~NativeState();
+  p_surface->p_native = nullptr;
+  p_surface->initialized = FALSE;
+}