Mercurial
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; +}