Mercurial
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(); } }