comparison infinite_canvas/web_surface_native.cc @ 280:49e9e591c9bb

Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 19:14:53 -0700
parents 8d560f50ed4c
children
comparison
equal deleted inserted replaced
279:b3b547563ec7 280:49e9e591c9bb
4 4
5 #include "include/cef_app.h" 5 #include "include/cef_app.h"
6 #include "include/cef_browser.h" 6 #include "include/cef_browser.h"
7 #include "include/cef_client.h" 7 #include "include/cef_client.h"
8 #include "include/cef_display_handler.h" 8 #include "include/cef_display_handler.h"
9 #include "include/cef_permission_handler.h"
9 #include "include/cef_render_handler.h" 10 #include "include/cef_render_handler.h"
11 #include "include/cef_request_context.h"
10 #include "include/capi/cef_app_capi.h" 12 #include "include/capi/cef_app_capi.h"
11 13
12 #include <algorithm> 14 #include <algorithm>
15 #include <cctype>
13 #include <cmath> 16 #include <cmath>
14 #include <cstdio> 17 #include <cstdio>
15 #include <cstdlib> 18 #include <cstdlib>
16 #include <cstring> 19 #include <cstring>
17 #include <filesystem> 20 #include <filesystem>
18 #include <new> 21 #include <new>
22 #include <string>
19 23
20 #if defined(OS_LINUX) 24 #if defined(OS_LINUX)
21 #include <dlfcn.h> 25 #include <dlfcn.h>
22 #include <limits.h> 26 #include <limits.h>
23 #endif 27 #endif
27 constexpr int32 kResolutionStep = 64; 31 constexpr int32 kResolutionStep = 64;
28 constexpr int32 kMinTextureWidth = 160; 32 constexpr int32 kMinTextureWidth = 160;
29 constexpr int32 kMinTextureHeight = 120; 33 constexpr int32 kMinTextureHeight = 120;
30 constexpr int32 kMaxTextureWidth = 1280; 34 constexpr int32 kMaxTextureWidth = 1280;
31 constexpr int32 kMaxTextureHeight = 800; 35 constexpr int32 kMaxTextureHeight = 800;
36 constexpr uint32 kDictationViewId = 0xffffffffu;
37 constexpr const char *kDictationUrl = "http://127.0.0.1:8090/?canvas=1";
32 int g_argc = 0; 38 int g_argc = 0;
33 char **g_argv = nullptr; 39 char **g_argv = nullptr;
34 char *g_cef_argv[128] = {}; 40 char *g_cef_argv[128] = {};
35 #if defined(OS_LINUX) 41 #if defined(OS_LINUX)
36 char g_resources_path[PATH_MAX + 32] = {}; 42 char g_resources_path[PATH_MAX + 32] = {};
38 char g_resources_switch[PATH_MAX + 64] = {}; 44 char g_resources_switch[PATH_MAX + 64] = {};
39 char g_locales_switch[PATH_MAX + 64] = {}; 45 char g_locales_switch[PATH_MAX + 64] = {};
40 #endif 46 #endif
41 47
42 void FindResourcePaths(); 48 void FindResourcePaths();
49
50 void PercentDecode(const char *source, char *target, size_t target_size) {
51 size_t output = 0;
52 for (size_t index = 0;
53 source[index] && output + 1 < target_size;
54 ++index) {
55 if (source[index] == '%' &&
56 std::isxdigit(static_cast<unsigned char>(source[index + 1])) &&
57 std::isxdigit(static_cast<unsigned char>(source[index + 2]))) {
58 char hex[3] = {source[index + 1], source[index + 2], '\0'};
59 target[output++] = static_cast<char>(std::strtol(hex, nullptr, 16));
60 index += 2;
61 } else if (source[index] == '+') {
62 target[output++] = ' ';
63 } else {
64 target[output++] = source[index];
65 }
66 }
67 target[output] = '\0';
68 }
43 69
44 void ApplyColorScheme(CefRefPtr<CefBrowser> browser, boolean dark_mode) { 70 void ApplyColorScheme(CefRefPtr<CefBrowser> browser, boolean dark_mode) {
45 if (!browser) return; 71 if (!browser) return;
46 CefRefPtr<CefDictionaryValue> params = CefDictionaryValue::Create(); 72 CefRefPtr<CefDictionaryValue> params = CefDictionaryValue::Create();
47 CefRefPtr<CefListValue> features = CefListValue::Create(); 73 CefRefPtr<CefListValue> features = CefListValue::Create();
58 CefRefPtr<CefFrame> frame = browser->GetMainFrame(); 84 CefRefPtr<CefFrame> frame = browser->GetMainFrame();
59 const char *script = dark_mode 85 const char *script = dark_mode
60 ? "document.documentElement.style.colorScheme='dark'" 86 ? "document.documentElement.style.colorScheme='dark'"
61 : "document.documentElement.style.colorScheme='light'"; 87 : "document.documentElement.style.colorScheme='light'";
62 frame->ExecuteJavaScript(script, frame->GetURL(), 0); 88 frame->ExecuteJavaScript(script, frame->GetURL(), 0);
89 }
90
91 boolean IsLocalDictationOrigin(const CefString &requesting_origin) {
92 std::string origin = requesting_origin.ToString();
93 return origin.rfind("http://127.0.0.1:8090", 0) == 0 ||
94 origin.rfind("http://localhost:8090", 0) == 0;
63 } 95 }
64 96
65 const Canvas_Entity *FindSurfaceEntity( 97 const Canvas_Entity *FindSurfaceEntity(
66 const Canvas_Scene *scene, 98 const Canvas_Scene *scene,
67 uint32 entity_id) { 99 uint32 entity_id) {
165 }; 197 };
166 198
167 class CanvasCefClient : public CefClient, 199 class CanvasCefClient : public CefClient,
168 public CefRenderHandler, 200 public CefRenderHandler,
169 public CefDisplayHandler, 201 public CefDisplayHandler,
202 public CefPermissionHandler,
170 public CefLifeSpanHandler { 203 public CefLifeSpanHandler {
171 public: 204 public:
172 explicit CanvasCefClient(Canvas_Web_View *view) : view_(view) {} 205 explicit CanvasCefClient(Canvas_Web_View *view) : view_(view) {}
173 206
174 CefRefPtr<CefRenderHandler> GetRenderHandler() override { return this; } 207 CefRefPtr<CefRenderHandler> GetRenderHandler() override { return this; }
175 CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; } 208 CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
209 CefRefPtr<CefPermissionHandler> GetPermissionHandler() override {
210 return this;
211 }
176 CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; } 212 CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
177 213
178 void OnAddressChange( 214 void OnAddressChange(
179 CefRefPtr<CefBrowser> browser, 215 CefRefPtr<CefBrowser> browser,
180 CefRefPtr<CefFrame> frame, 216 CefRefPtr<CefFrame> frame,
185 sizeof(view_->url), 221 sizeof(view_->url),
186 "%s", 222 "%s",
187 url.ToString().c_str()); 223 url.ToString().c_str());
188 view_->can_go_back = browser->CanGoBack() ? TRUE : FALSE; 224 view_->can_go_back = browser->CanGoBack() ? TRUE : FALSE;
189 view_->can_go_forward = browser->CanGoForward() ? TRUE : FALSE; 225 view_->can_go_forward = browser->CanGoForward() ? TRUE : FALSE;
226 }
227
228 void OnTitleChange(
229 CefRefPtr<CefBrowser> browser,
230 const CefString &title) override {
231 (void)browser;
232 std::string value = title.ToString();
233 constexpr const char *prefix = "Zenbu Dictation|";
234 if (value.rfind(prefix, 0) != 0) return;
235 const char *kind = std::strchr(
236 value.c_str() + std::strlen(prefix),
237 '|');
238 if (!kind) return;
239 kind++;
240 const char *encoded = std::strchr(kind, '|');
241 if (!encoded) return;
242 std::string event_kind(kind, static_cast<size_t>(encoded - kind));
243 if (event_kind == "status") {
244 view_->dictation_event = CANVAS_DICTATION_EVENT_STATUS;
245 } else if (event_kind == "partial") {
246 view_->dictation_event = CANVAS_DICTATION_EVENT_PARTIAL;
247 } else if (event_kind == "final") {
248 view_->dictation_event = CANVAS_DICTATION_EVENT_FINAL;
249 } else {
250 return;
251 }
252 PercentDecode(
253 encoded + 1,
254 view_->dictation_text,
255 sizeof(view_->dictation_text));
256 view_->dictation_sequence++;
257 }
258
259 bool OnRequestMediaAccessPermission(
260 CefRefPtr<CefBrowser> browser,
261 CefRefPtr<CefFrame> frame,
262 const CefString &requesting_origin,
263 uint32_t requested_permissions,
264 CefRefPtr<CefMediaAccessCallback> callback) override {
265 (void)browser;
266 (void)frame;
267 boolean local_dictation =
268 view_->is_dictation_transport &&
269 IsLocalDictationOrigin(requesting_origin);
270 uint32_t audio = CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE;
271 boolean requests_audio = (requested_permissions & audio) != 0;
272 boolean requests_other_media = (requested_permissions & ~audio) != 0;
273 if (local_dictation && requests_audio && !requests_other_media) {
274 callback->Continue(audio);
275 } else {
276 callback->Cancel();
277 }
278 return true;
279 }
280
281 bool OnShowPermissionPrompt(
282 CefRefPtr<CefBrowser> browser,
283 uint64_t prompt_id,
284 const CefString &requesting_origin,
285 uint32_t requested_permissions,
286 CefRefPtr<CefPermissionPromptCallback> callback) override {
287 (void)browser;
288 (void)prompt_id;
289 if (!view_->is_dictation_transport ||
290 !IsLocalDictationOrigin(requesting_origin)) {
291 return false;
292 }
293 uint32_t microphone = CEF_PERMISSION_TYPE_MIC_STREAM;
294 boolean requests_microphone =
295 (requested_permissions & microphone) != 0;
296 boolean requests_other_permissions =
297 (requested_permissions & ~microphone) != 0;
298 callback->Continue(
299 requests_microphone && !requests_other_permissions
300 ? CEF_PERMISSION_RESULT_ACCEPT
301 : CEF_PERMISSION_RESULT_DENY);
302 return true;
190 } 303 }
191 304
192 void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) override { 305 void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) override {
193 (void)browser; 306 (void)browser;
194 rect = CefRect(0, 0, view_->texture_width, view_->texture_height); 307 rect = CefRect(0, 0, view_->texture_width, view_->texture_height);
241 DISALLOW_COPY_AND_ASSIGN(CanvasCefClient); 354 DISALLOW_COPY_AND_ASSIGN(CanvasCefClient);
242 }; 355 };
243 356
244 struct NativeState { 357 struct NativeState {
245 CefRefPtr<CanvasCefClient> clients[CANVAS_MAX_WEB_VIEWS]; 358 CefRefPtr<CanvasCefClient> clients[CANVAS_MAX_WEB_VIEWS];
359 CefRefPtr<CefRequestContext> dictation_context;
246 boolean cef_initialized; 360 boolean cef_initialized;
247 }; 361 };
362
363 void EnsureDictationRequestContext(NativeState *state) {
364 if (state->dictation_context) return;
365 CefRequestContextSettings settings;
366 state->dictation_context =
367 CefRequestContext::CreateContext(settings, nullptr);
368 }
248 369
249 CefMainArgs MainArgs(int argc, char **argv) { 370 CefMainArgs MainArgs(int argc, char **argv) {
250 #if defined(OS_WIN) 371 #if defined(OS_WIN)
251 (void)argc; 372 (void)argc;
252 (void)argv; 373 (void)argv;
394 browser_settings.background_color = surface->dark_mode 515 browser_settings.background_color = surface->dark_mode
395 ? CefColorSetARGB(255, 15, 15, 17) 516 ? CefColorSetARGB(255, 15, 15, 17)
396 : CefColorSetARGB(255, 255, 255, 255); 517 : CefColorSetARGB(255, 255, 255, 255);
397 const char *initial_url = 518 const char *initial_url =
398 view->entity_type == CANVAS_ENTITY_IMAGE ? "about:blank" : url; 519 view->entity_type == CANVAS_ENTITY_IMAGE ? "about:blank" : url;
520 CefRefPtr<CefRequestContext> request_context;
521 if (view->is_dictation_transport) {
522 EnsureDictationRequestContext(state);
523 request_context = state->dictation_context;
524 }
399 if (!CefBrowserHost::CreateBrowser( 525 if (!CefBrowserHost::CreateBrowser(
400 window_info, 526 window_info,
401 state->clients[index], 527 state->clients[index],
402 initial_url, 528 initial_url,
403 browser_settings, 529 browser_settings,
404 nullptr, 530 nullptr,
405 nullptr)) { 531 request_context)) {
406 state->clients[index] = nullptr; 532 state->clients[index] = nullptr;
407 return FALSE; 533 return FALSE;
408 } 534 }
409 view->browser_created = TRUE; 535 view->browser_created = TRUE;
410 return TRUE; 536 return TRUE;
537 }
538
539 Canvas_Web_View *EnsureDictationView(Canvas_Web_Surface *surface) {
540 Canvas_Web_View *view = FindView(surface, kDictationViewId);
541 if (view) {
542 view->active = TRUE;
543 return view;
544 }
545 view = FindInactiveView(surface);
546 if (!view) return nullptr;
547 boolean browser_created = view->browser_created;
548 view->active = TRUE;
549 view->entity_id = kDictationViewId;
550 view->entity_type = CANVAS_ENTITY_WEB_CONTENT;
551 view->is_dictation_transport = TRUE;
552 std::snprintf(
553 view->source_url,
554 sizeof(view->source_url),
555 "%s",
556 kDictationUrl);
557 std::snprintf(view->url, sizeof(view->url), "%s", kDictationUrl);
558 Rectangle bounds = {0.0f, 0.0f, 320.0f, 240.0f};
559 if (!browser_created &&
560 !CreateBrowser(surface, view, kDictationUrl, bounds)) {
561 view->active = FALSE;
562 view->entity_id = 0;
563 view->is_dictation_transport = FALSE;
564 view->source_url[0] = '\0';
565 view->url[0] = '\0';
566 return nullptr;
567 }
568 if (browser_created) {
569 EnsureViewResolution(surface, view, bounds);
570 NativeState *state = static_cast<NativeState *>(surface->p_native);
571 CefRefPtr<CanvasCefClient> client =
572 state->clients[ViewIndex(surface, view)];
573 if (client && client->browser()) {
574 client->browser()->GetMainFrame()->LoadURL(kDictationUrl);
575 }
576 }
577 return view;
411 } 578 }
412 579
413 void PercentEncode( 580 void PercentEncode(
414 const char *source, 581 const char *source,
415 char *target, 582 char *target,
526 Canvas_Web_Surface *surface, 693 Canvas_Web_Surface *surface,
527 const Canvas_Camera *camera, 694 const Canvas_Camera *camera,
528 const Canvas_Scene *scene) { 695 const Canvas_Scene *scene) {
529 for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) { 696 for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
530 Canvas_Web_View *view = &surface->views[index]; 697 Canvas_Web_View *view = &surface->views[index];
531 view->active = view->entity_id != 0 && 698 view->active = view->is_dictation_transport ||
532 SceneHasVisibleEntity(scene, camera, view->entity_id); 699 (view->entity_id != 0 &&
700 SceneHasVisibleEntity(scene, camera, view->entity_id));
533 } 701 }
534 702
535 for (size_t index = 0; index < Dowa_Array_Length(scene->p_entities); ++index) { 703 for (size_t index = 0; index < Dowa_Array_Length(scene->p_entities); ++index) {
536 const Canvas_Entity *entity = &scene->p_entities[index]; 704 const Canvas_Entity *entity = &scene->p_entities[index];
537 if ((entity->type != CANVAS_ENTITY_WEB_CONTENT && 705 if ((entity->type != CANVAS_ENTITY_WEB_CONTENT &&
560 CefRefPtr<CanvasCefClient> client = state->clients[index]; 728 CefRefPtr<CanvasCefClient> client = state->clients[index];
561 if (client && client->browser()) { 729 if (client && client->browser()) {
562 Canvas_Web_View *view = &surface->views[index]; 730 Canvas_Web_View *view = &surface->views[index];
563 CefRefPtr<CefBrowserHost> host = client->browser()->GetHost(); 731 CefRefPtr<CefBrowserHost> host = client->browser()->GetHost();
564 host->WasHidden(!view->active); 732 host->WasHidden(!view->active);
565 if (view->active) { 733 if (view->active && !view->is_dictation_transport) {
566 const Canvas_Entity *entity = FindSurfaceEntity(scene, view->entity_id); 734 const Canvas_Entity *entity = FindSurfaceEntity(scene, view->entity_id);
567 if (entity) { 735 if (entity) {
568 host->SetWindowlessFrameRate( 736 host->SetWindowlessFrameRate(
569 FrameRateForView( 737 FrameRateForView(
570 surface, 738 surface,
803 Canvas_Web_Surface *p_surface, 971 Canvas_Web_Surface *p_surface,
804 const Canvas_Camera *p_camera, 972 const Canvas_Camera *p_camera,
805 const Canvas_Scene *p_scene) { 973 const Canvas_Scene *p_scene) {
806 if (!p_surface->initialized) return; 974 if (!p_surface->initialized) return;
807 CefDoMessageLoopWork(); 975 CefDoMessageLoopWork();
976 EnsureDictationView(p_surface);
808 ReconcileViews(p_surface, p_camera, p_scene); 977 ReconcileViews(p_surface, p_camera, p_scene);
978 if (p_surface->pending_dictation_command != 0) {
979 Canvas_Web_View *view = EnsureDictationView(p_surface);
980 if (view) {
981 view->dictation_command = p_surface->pending_dictation_command;
982 p_surface->pending_dictation_command = 0;
983 }
984 }
809 985
810 for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) { 986 for (int32 index = 0; index < CANVAS_MAX_WEB_VIEWS; ++index) {
811 Canvas_Web_View *view = &p_surface->views[index]; 987 Canvas_Web_View *view = &p_surface->views[index];
812 if (!view->browser_created) continue; 988 if (!view->browser_created) continue;
813 if (!view->texture_ready && IsWindowReady()) { 989 if (!view->is_dictation_transport &&
990 !view->texture_ready &&
991 IsWindowReady()) {
814 Image image = { 992 Image image = {
815 .data = view->p_pixels, 993 .data = view->p_pixels,
816 .width = view->texture_width, 994 .width = view->texture_width,
817 .height = view->texture_height, 995 .height = view->texture_height,
818 .mipmaps = 1, 996 .mipmaps = 1,
822 view->texture_ready = view->texture.id != 0 ? TRUE : FALSE; 1000 view->texture_ready = view->texture.id != 0 ? TRUE : FALSE;
823 if (view->texture_ready) { 1001 if (view->texture_ready) {
824 SetTextureFilter(view->texture, TEXTURE_FILTER_BILINEAR); 1002 SetTextureFilter(view->texture, TEXTURE_FILTER_BILINEAR);
825 } 1003 }
826 } 1004 }
827 if (view->texture_ready && view->pixels_dirty) { 1005 if (!view->is_dictation_transport &&
1006 view->texture_ready &&
1007 view->pixels_dirty) {
828 UpdateTexture(view->texture, view->p_pixels); 1008 UpdateTexture(view->texture, view->p_pixels);
829 view->pixels_dirty = FALSE; 1009 view->pixels_dirty = FALSE;
830 } 1010 }
831 NativeState *state = static_cast<NativeState *>(p_surface->p_native); 1011 NativeState *state = static_cast<NativeState *>(p_surface->p_native);
832 CefRefPtr<CanvasCefClient> client = state->clients[index]; 1012 CefRefPtr<CanvasCefClient> client = state->clients[index];
838 view->color_scheme_applied = TRUE; 1018 view->color_scheme_applied = TRUE;
839 view->applied_dark_mode = p_surface->dark_mode; 1019 view->applied_dark_mode = p_surface->dark_mode;
840 } 1020 }
841 view->can_go_back = client->browser()->CanGoBack() ? TRUE : FALSE; 1021 view->can_go_back = client->browser()->CanGoBack() ? TRUE : FALSE;
842 view->can_go_forward = client->browser()->CanGoForward() ? TRUE : FALSE; 1022 view->can_go_forward = client->browser()->CanGoForward() ? TRUE : FALSE;
1023 if (view->dictation_command != 0) {
1024 CefRefPtr<CefFrame> frame = client->browser()->GetMainFrame();
1025 const char *function_name =
1026 view->dictation_command == 2
1027 ? "ZenbuDictationCommit"
1028 : (view->dictation_command > 0
1029 ? "ZenbuDictationStart"
1030 : "ZenbuDictationStop");
1031 char script[512];
1032 std::snprintf(
1033 script,
1034 sizeof(script),
1035 "(()=>{let attempts=0;const invoke=()=>{"
1036 "if(window.%s){window.%s();return;}"
1037 "if(attempts++<40)setTimeout(invoke,250);};invoke();})()",
1038 function_name,
1039 function_name);
1040 frame->ExecuteJavaScript(
1041 script,
1042 frame->GetURL(),
1043 0);
1044 view->dictation_command = 0;
1045 }
843 } 1046 }
844 } 1047 }
845 } 1048 }
846 1049
847 extern "C" void Canvas_Web_Surface_Draw( 1050 extern "C" void Canvas_Web_Surface_Draw(
953 if (client && client->browser() && client->browser()->CanGoForward()) { 1156 if (client && client->browser() && client->browser()->CanGoForward()) {
954 client->browser()->GoForward(); 1157 client->browser()->GoForward();
955 } 1158 }
956 } 1159 }
957 1160
1161 extern "C" void Canvas_Web_Surface_Set_Dictation_Active(
1162 Canvas_Web_Surface *p_surface,
1163 boolean active) {
1164 int32 command = active ? 1 : -1;
1165 Canvas_Web_View *view = FindView(p_surface, kDictationViewId);
1166 if (view) {
1167 view->dictation_command = command;
1168 } else {
1169 p_surface->pending_dictation_command = command;
1170 }
1171 }
1172
1173 extern "C" void Canvas_Web_Surface_Commit_Dictation(
1174 Canvas_Web_Surface *p_surface) {
1175 Canvas_Web_View *view = FindView(p_surface, kDictationViewId);
1176 if (view) {
1177 view->dictation_command = 2;
1178 } else {
1179 p_surface->pending_dictation_command = 2;
1180 }
1181 }
1182
1183 extern "C" Canvas_Dictation_Event Canvas_Web_Surface_Consume_Dictation(
1184 Canvas_Web_Surface *p_surface,
1185 char *p_text,
1186 size_t text_capacity) {
1187 Canvas_Web_View *view = FindView(p_surface, kDictationViewId);
1188 if (!view ||
1189 view->dictation_sequence == view->consumed_dictation_sequence) {
1190 return CANVAS_DICTATION_EVENT_NONE;
1191 }
1192 snprintf(p_text, text_capacity, "%s", view->dictation_text);
1193 view->consumed_dictation_sequence = view->dictation_sequence;
1194 return view->dictation_event;
1195 }
1196
958 extern "C" boolean Canvas_Web_Surface_Is_Focused( 1197 extern "C" boolean Canvas_Web_Surface_Is_Focused(
959 const Canvas_Web_Surface *p_surface) { 1198 const Canvas_Web_Surface *p_surface) {
960 return p_surface->focused_entity_id != 0; 1199 return p_surface->focused_entity_id != 0;
961 } 1200 }
962 1201