fix(gui): restore omnibox styling and messaging in isolated world
Fixes omnibox and GUI components appearing without styles on YouTube and other sites with aggressive scripts. Root causes: - CSS variables from color-scheme.js only in main world - webkit.messageHandlers only accessible in main world - Component styles injected via UserStyleSheet (doesn't work with Shadow DOM) Changes: - Inject color-scheme.js in both main and isolated worlds for CSS variables - Implement CustomEvent bridge (dumber:isolated-message) from isolated to main world - Move main-world.ts injection from isolated to main world - Change component styles to shadow root injection instead of UserStyleSheet - Update messaging.ts to dispatch CustomEvents instead of direct webkit calls - Register message handler in main world (not isolated) Architecture: GUI (isolated) → CustomEvent → main-world.ts (main) → webkit.messageHandlers → Go backend Resolves omnibox rendering with full styles and working suggestions.
- commit
- 956f1eb1feebbb372598299bb59bf4834faf31a8
- author
- bnema · · 2025-11-16 20:16 UTC
- committer
- bnema · · 2025-11-16 19:42 UTC
- parent
- e075001
- signature
- unsigned
8 files · +197 -75
M CHANGELOG.md +1 -0
| @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. | |||
| 14 | 14 | - **Code cleanup**: Removed dead code files and emojis from GUI modules | |
| 15 | 15 | ||
| 16 | 16 | ### Fixed | |
| 17 | + | - **Isolated world GUI rendering**: Fixed omnibox and GUI components appearing without styles on YouTube and other aggressive sites. Injected `color-scheme.js` in both main and isolated worlds to provide CSS variables. Implemented CustomEvent bridge (`dumber:isolated-message`) from isolated world to main world for webkit.messageHandlers communication. Moved main-world script injection from isolated to main world. Changed component styles injection from UserStyleSheet to shadow root injection for proper Shadow DOM encapsulation. | |
| 17 | 18 | - **Favicon processing on refresh**: Skip redundant favicon processing when WebKit's cache already has the URI. Made domain propagation async to prevent blocking page loads. | |
| 18 | 19 | - **Popup handler registration timing**: Fixed `g_object_ref` assertion errors when WebKit takes ownership of popup WebViews by connecting signals immediately instead of deferring via `glib.IdleAdd`. This resolves crashes with `target="_blank"` links and `window.open()` calls. | |
| 19 | 20 | ||
| @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. | |||||
| 14 | - **Code cleanup**: Removed dead code files and emojis from GUI modules | 14 | - **Code cleanup**: Removed dead code files and emojis from GUI modules | ||
| 15 | 15 | ||||
| 16 | ### Fixed | 16 | ### Fixed | ||
| 17 | + | - **Isolated world GUI rendering**: Fixed omnibox and GUI components appearing without styles on YouTube and other aggressive sites. Injected `color-scheme.js` in both main and isolated worlds to provide CSS variables. Implemented CustomEvent bridge (`dumber:isolated-message`) from isolated world to main world for webkit.messageHandlers communication. Moved main-world script injection from isolated to main world. Changed component styles injection from UserStyleSheet to shadow root injection for proper Shadow DOM encapsulation. | |||
| 17 | - **Favicon processing on refresh**: Skip redundant favicon processing when WebKit's cache already has the URI. Made domain propagation async to prevent blocking page loads. | 18 | - **Favicon processing on refresh**: Skip redundant favicon processing when WebKit's cache already has the URI. Made domain propagation async to prevent blocking page loads. | ||
| 18 | - **Popup handler registration timing**: Fixed `g_object_ref` assertion errors when WebKit takes ownership of popup WebViews by connecting signals immediately instead of deferring via `glib.IdleAdd`. This resolves crashes with `target="_blank"` links and `window.open()` calls. | 19 | - **Popup handler registration timing**: Fixed `g_object_ref` assertion errors when WebKit takes ownership of popup WebViews by connecting signals immediately instead of deferring via `glib.IdleAdd`. This resolves crashes with `target="_blank"` links and `window.open()` calls. | ||
| 19 | 20 | ||||
M assets/gui_embed.go +6 -0
| @@ -12,3 +12,9 @@ var GUIScript string | |||
| 12 | 12 | ||
| 13 | 13 | //go:embed gui/color-scheme.js | |
| 14 | 14 | var ColorSchemeScript string | |
| 15 | + | ||
| 16 | + | // GUI component styles extracted from Svelte components | |
| 17 | + | // These are injected into the isolated world using UserStyleSheet | |
| 18 | + | // | |
| 19 | + | //go:embed gui/gui.min.css | |
| 20 | + | var ComponentStyles string | |
| @@ -12,3 +12,9 @@ var GUIScript string | |||||
| 12 | 12 | ||||
| 13 | //go:embed gui/color-scheme.js | 13 | //go:embed gui/color-scheme.js | ||
| 14 | var ColorSchemeScript string | 14 | var ColorSchemeScript string | ||
| 15 | + | ||||
| 16 | + | // GUI component styles extracted from Svelte components | |||
| 17 | + | // These are injected into the isolated world using UserStyleSheet | |||
| 18 | + | // | |||
| 19 | + | //go:embed gui/gui.min.css | |||
| 20 | + | var ComponentStyles string | |||
M gui/src/components/omnibox/messaging.ts +18 -25
| @@ -8,32 +8,25 @@ import type { OmniboxMessage, OmniboxMessageBridge, Suggestion, SearchShortcut } | |||
| 8 | 8 | import { omniboxStore } from "./stores.svelte.ts"; | |
| 9 | 9 | ||
| 10 | 10 | export class OmniboxBridge implements OmniboxMessageBridge { | |
| 11 | - | // Forward messages to the native WebKit message handler when available | |
| 12 | - | // Fallback: for navigate, perform direct location change | |
| 11 | + | // Forward messages via CustomEvent to main world bridge | |
| 12 | + | // The main world will forward to webkit.messageHandlers.dumber | |
| 13 | 13 | postMessage(msg: OmniboxMessage): void { | |
| 14 | - | console.log("📤 [DEBUG] Posting message to backend:", msg); | |
| 15 | - | // NOTE: Do NOT detach postMessage from its receiver. In WKWebView, | |
| 16 | - | // postMessage must be called on the UserMessageHandler instance. | |
| 17 | - | const bridge = window.webkit?.messageHandlers?.dumber; | |
| 18 | - | if (bridge && typeof bridge.postMessage === "function") { | |
| 19 | - | try { | |
| 20 | - | console.log("📱 [DEBUG] Using webkit message handler"); | |
| 21 | - | bridge.postMessage(JSON.stringify(msg)); | |
| 22 | - | return; | |
| 23 | - | } catch (e) { | |
| 24 | - | console.warn( | |
| 25 | - | "postMessage to native handler failed, using fallback:", | |
| 26 | - | e, | |
| 27 | - | ); | |
| 28 | - | } | |
| 29 | - | } | |
| 30 | - | console.log("⚠️ [DEBUG] No webkit bridge available"); | |
| 31 | - | // Fallback navigation if no native bridge is available | |
| 32 | - | if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) { | |
| 33 | - | try { | |
| 34 | - | window.location.href = msg.url; | |
| 35 | - | } catch (e) { | |
| 36 | - | console.error("Fallback navigation failed:", e); | |
| 14 | + | console.log("📤 [DEBUG] Posting message to backend via bridge:", msg); | |
| 15 | + | try { | |
| 16 | + | // Dispatch CustomEvent that main-world bridge will listen to | |
| 17 | + | document.dispatchEvent(new CustomEvent("dumber:isolated-message", { | |
| 18 | + | detail: { payload: msg } | |
| 19 | + | })); | |
| 20 | + | console.log("✅ [DEBUG] Dispatched isolated message event"); | |
| 21 | + | } catch (e) { | |
| 22 | + | console.error("❌ [DEBUG] Failed to dispatch message:", e); | |
| 23 | + | // Fallback navigation if dispatch fails | |
| 24 | + | if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) { | |
| 25 | + | try { | |
| 26 | + | window.location.href = msg.url; | |
| 27 | + | } catch (navError) { | |
| 28 | + | console.error("Fallback navigation failed:", navError); | |
| 29 | + | } | |
| 37 | 30 | } | |
| 38 | 31 | } | |
| 39 | 32 | } | |
| @@ -8,32 +8,25 @@ import type { OmniboxMessage, OmniboxMessageBridge, Suggestion, SearchShortcut } | |||||
| 8 | import { omniboxStore } from "./stores.svelte.ts"; | 8 | import { omniboxStore } from "./stores.svelte.ts"; | ||
| 9 | 9 | ||||
| 10 | export class OmniboxBridge implements OmniboxMessageBridge { | 10 | export class OmniboxBridge implements OmniboxMessageBridge { | ||
| 11 | - | // Forward messages to the native WebKit message handler when available | 11 | + | // Forward messages via CustomEvent to main world bridge |
| 12 | - | // Fallback: for navigate, perform direct location change | 12 | + | // The main world will forward to webkit.messageHandlers.dumber |
| 13 | postMessage(msg: OmniboxMessage): void { | 13 | postMessage(msg: OmniboxMessage): void { | ||
| 14 | - | console.log("📤 [DEBUG] Posting message to backend:", msg); | 14 | + | console.log("📤 [DEBUG] Posting message to backend via bridge:", msg); |
| 15 | - | // NOTE: Do NOT detach postMessage from its receiver. In WKWebView, | 15 | + | try { |
| 16 | - | // postMessage must be called on the UserMessageHandler instance. | 16 | + | // Dispatch CustomEvent that main-world bridge will listen to |
| 17 | - | const bridge = window.webkit?.messageHandlers?.dumber; | 17 | + | document.dispatchEvent(new CustomEvent("dumber:isolated-message", { |
| 18 | - | if (bridge && typeof bridge.postMessage === "function") { | 18 | + | detail: { payload: msg } |
| 19 | - | try { | 19 | + | })); |
| 20 | - | console.log("📱 [DEBUG] Using webkit message handler"); | 20 | + | console.log("✅ [DEBUG] Dispatched isolated message event"); |
| 21 | - | bridge.postMessage(JSON.stringify(msg)); | 21 | + | } catch (e) { |
| 22 | - | return; | 22 | + | console.error("❌ [DEBUG] Failed to dispatch message:", e); |
| 23 | - | } catch (e) { | 23 | + | // Fallback navigation if dispatch fails |
| 24 | - | console.warn( | 24 | + | if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) { |
| 25 | - | "postMessage to native handler failed, using fallback:", | 25 | + | try { |
| 26 | - | e, | 26 | + | window.location.href = msg.url; |
| 27 | - | ); | 27 | + | } catch (navError) { |
| 28 | - | } | 28 | + | console.error("Fallback navigation failed:", navError); |
| 29 | - | } | 29 | + | } |
| 30 | - | console.log("⚠️ [DEBUG] No webkit bridge available"); | |||
| 31 | - | // Fallback navigation if no native bridge is available | |||
| 32 | - | if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) { | |||
| 33 | - | try { | |||
| 34 | - | window.location.href = msg.url; | |||
| 35 | - | } catch (e) { | |||
| 36 | - | console.error("Fallback navigation failed:", e); | |||
| 37 | } | 30 | } | ||
| 38 | } | 31 | } | ||
| 39 | } | 32 | } | ||
M gui/src/injected/main-world.ts +23 -0
| @@ -909,6 +909,29 @@ interface DumberAPI { | |||
| 909 | 909 | }); | |
| 910 | 910 | }; | |
| 911 | 911 | ||
| 912 | + | // Bridge for isolated world communication | |
| 913 | + | // Listen for messages from isolated world and forward to backend via webkit.messageHandlers | |
| 914 | + | const setupIsolatedWorldBridge = () => { | |
| 915 | + | document.addEventListener("dumber:isolated-message", ((event: CustomEvent) => { | |
| 916 | + | try { | |
| 917 | + | const { payload } = event.detail; | |
| 918 | + | if (window.webkit?.messageHandlers?.dumber) { | |
| 919 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify(payload)); | |
| 920 | + | console.log("[dumber-bridge] Forwarded isolated world message:", payload.type); | |
| 921 | + | } else { | |
| 922 | + | console.warn("[dumber-bridge] webkit message handler not available"); | |
| 923 | + | } | |
| 924 | + | } catch (error) { | |
| 925 | + | console.error("[dumber-bridge] Failed to forward message:", error); | |
| 926 | + | } | |
| 927 | + | }) as EventListener); | |
| 928 | + | ||
| 929 | + | console.log("[dumber-bridge] Isolated world message bridge initialized"); | |
| 930 | + | }; | |
| 931 | + | ||
| 932 | + | // Initialize isolated world bridge | |
| 933 | + | setupIsolatedWorldBridge(); | |
| 934 | + | ||
| 912 | 935 | // Initialize console capture | |
| 913 | 936 | setupConsoleCapture(); | |
| 914 | 937 | } catch (err) { | |
| @@ -909,6 +909,29 @@ interface DumberAPI { | |||||
| 909 | }); | 909 | }); | ||
| 910 | }; | 910 | }; | ||
| 911 | 911 | ||||
| 912 | + | // Bridge for isolated world communication | |||
| 913 | + | // Listen for messages from isolated world and forward to backend via webkit.messageHandlers | |||
| 914 | + | const setupIsolatedWorldBridge = () => { | |||
| 915 | + | document.addEventListener("dumber:isolated-message", ((event: CustomEvent) => { | |||
| 916 | + | try { | |||
| 917 | + | const { payload } = event.detail; | |||
| 918 | + | if (window.webkit?.messageHandlers?.dumber) { | |||
| 919 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify(payload)); | |||
| 920 | + | console.log("[dumber-bridge] Forwarded isolated world message:", payload.type); | |||
| 921 | + | } else { | |||
| 922 | + | console.warn("[dumber-bridge] webkit message handler not available"); | |||
| 923 | + | } | |||
| 924 | + | } catch (error) { | |||
| 925 | + | console.error("[dumber-bridge] Failed to forward message:", error); | |||
| 926 | + | } | |||
| 927 | + | }) as EventListener); | |||
| 928 | + | ||||
| 929 | + | console.log("[dumber-bridge] Isolated world message bridge initialized"); | |||
| 930 | + | }; | |||
| 931 | + | ||||
| 932 | + | // Initialize isolated world bridge | |||
| 933 | + | setupIsolatedWorldBridge(); | |||
| 934 | + | ||||
| 912 | // Initialize console capture | 935 | // Initialize console capture | ||
| 913 | setupConsoleCapture(); | 936 | setupConsoleCapture(); | ||
| 914 | } catch (err) { | 937 | } catch (err) { | ||
M gui/src/injected/modules/shadowHost.ts +39 -23
| @@ -182,33 +182,49 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||
| 182 | 182 | `; | |
| 183 | 183 | shadowRoot.appendChild(tokensStyle); | |
| 184 | 184 | ||
| 185 | - | // Inject the global GUI stylesheet so components rendered inside the shadow | |
| 186 | - | // root (e.g., toasts, omnibox) receive their styles | |
| 185 | + | // Inject the global GUI stylesheet (Tailwind) into shadow root | |
| 187 | 186 | try { | |
| 188 | - | // Prefer constructable stylesheets when available | |
| 189 | - | const hasConstructable = typeof CSSStyleSheet !== "undefined"; | |
| 190 | - | const supportsAdopted = "adoptedStyleSheets" in shadowRoot; | |
| 191 | - | if (supportsAdopted && hasConstructable) { | |
| 192 | - | const sheet = new CSSStyleSheet(); | |
| 193 | - | sheet.replaceSync(globalStyles); | |
| 194 | - | const rootWithSheets = shadowRoot as ShadowRoot & { | |
| 195 | - | adoptedStyleSheets: CSSStyleSheet[]; | |
| 196 | - | }; | |
| 197 | - | rootWithSheets.adoptedStyleSheets = [ | |
| 198 | - | ...(rootWithSheets.adoptedStyleSheets ?? []), | |
| 199 | - | sheet, | |
| 200 | - | ]; | |
| 201 | - | } else { | |
| 202 | - | // Fallback: append a <style> element with the global CSS | |
| 203 | - | const styleTag = document.createElement("style"); | |
| 204 | - | styleTag.textContent = globalStyles; | |
| 205 | - | shadowRoot.appendChild(styleTag); | |
| 206 | - | } | |
| 207 | - | } catch { | |
| 208 | - | // Final fallback if anything above fails | |
| 187 | + | // Use <style> tag injection for Tailwind global styles | |
| 209 | 188 | const styleTag = document.createElement("style"); | |
| 189 | + | styleTag.setAttribute("data-source", "dumber-tailwind-styles"); | |
| 210 | 190 | styleTag.textContent = globalStyles; | |
| 211 | 191 | shadowRoot.appendChild(styleTag); | |
| 192 | + | ||
| 193 | + | // Inject component styles from Go-provided string | |
| 194 | + | // These are Svelte component-scoped styles that must be inside the shadow root | |
| 195 | + | // due to Shadow DOM style encapsulation | |
| 196 | + | const componentStyles = (window as any).__dumber_component_styles; | |
| 197 | + | if (componentStyles) { | |
| 198 | + | const componentStyleTag = document.createElement("style"); | |
| 199 | + | componentStyleTag.setAttribute("data-source", "dumber-component-styles"); | |
| 200 | + | componentStyleTag.textContent = componentStyles; | |
| 201 | + | shadowRoot.appendChild(componentStyleTag); | |
| 202 | + | console.log( | |
| 203 | + | "[shadowHost] Component styles injected:", | |
| 204 | + | componentStyles.length, | |
| 205 | + | "chars", | |
| 206 | + | ); | |
| 207 | + | } else { | |
| 208 | + | console.warn("[shadowHost] Component styles not available on window"); | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | console.log( | |
| 212 | + | "[shadowHost] All styles injected, shadow root children:", | |
| 213 | + | shadowRoot.childNodes.length, | |
| 214 | + | ); | |
| 215 | + | } catch (error) { | |
| 216 | + | console.error("[shadowHost] Failed to inject styles:", error); | |
| 217 | + | // Fallback attempt | |
| 218 | + | try { | |
| 219 | + | const fallbackStyle = document.createElement("style"); | |
| 220 | + | fallbackStyle.textContent = globalStyles || ""; | |
| 221 | + | shadowRoot.appendChild(fallbackStyle); | |
| 222 | + | } catch (fallbackError) { | |
| 223 | + | console.error( | |
| 224 | + | "[shadowHost] Fallback style injection also failed:", | |
| 225 | + | fallbackError, | |
| 226 | + | ); | |
| 227 | + | } | |
| 212 | 228 | } | |
| 213 | 229 | shadowResetApplied.add(shadowRoot); | |
| 214 | 230 | } | |
| @@ -182,33 +182,49 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||||
| 182 | `; | 182 | `; | ||
| 183 | shadowRoot.appendChild(tokensStyle); | 183 | shadowRoot.appendChild(tokensStyle); | ||
| 184 | 184 | ||||
| 185 | - | // Inject the global GUI stylesheet so components rendered inside the shadow | 185 | + | // Inject the global GUI stylesheet (Tailwind) into shadow root |
| 186 | - | // root (e.g., toasts, omnibox) receive their styles | |||
| 187 | try { | 186 | try { | ||
| 188 | - | // Prefer constructable stylesheets when available | 187 | + | // Use <style> tag injection for Tailwind global styles |
| 189 | - | const hasConstructable = typeof CSSStyleSheet !== "undefined"; | |||
| 190 | - | const supportsAdopted = "adoptedStyleSheets" in shadowRoot; | |||
| 191 | - | if (supportsAdopted && hasConstructable) { | |||
| 192 | - | const sheet = new CSSStyleSheet(); | |||
| 193 | - | sheet.replaceSync(globalStyles); | |||
| 194 | - | const rootWithSheets = shadowRoot as ShadowRoot & { | |||
| 195 | - | adoptedStyleSheets: CSSStyleSheet[]; | |||
| 196 | - | }; | |||
| 197 | - | rootWithSheets.adoptedStyleSheets = [ | |||
| 198 | - | ...(rootWithSheets.adoptedStyleSheets ?? []), | |||
| 199 | - | sheet, | |||
| 200 | - | ]; | |||
| 201 | - | } else { | |||
| 202 | - | // Fallback: append a <style> element with the global CSS | |||
| 203 | - | const styleTag = document.createElement("style"); | |||
| 204 | - | styleTag.textContent = globalStyles; | |||
| 205 | - | shadowRoot.appendChild(styleTag); | |||
| 206 | - | } | |||
| 207 | - | } catch { | |||
| 208 | - | // Final fallback if anything above fails | |||
| 209 | const styleTag = document.createElement("style"); | 188 | const styleTag = document.createElement("style"); | ||
| 189 | + | styleTag.setAttribute("data-source", "dumber-tailwind-styles"); | |||
| 210 | styleTag.textContent = globalStyles; | 190 | styleTag.textContent = globalStyles; | ||
| 211 | shadowRoot.appendChild(styleTag); | 191 | shadowRoot.appendChild(styleTag); | ||
| 192 | + | ||||
| 193 | + | // Inject component styles from Go-provided string | |||
| 194 | + | // These are Svelte component-scoped styles that must be inside the shadow root | |||
| 195 | + | // due to Shadow DOM style encapsulation | |||
| 196 | + | const componentStyles = (window as any).__dumber_component_styles; | |||
| 197 | + | if (componentStyles) { | |||
| 198 | + | const componentStyleTag = document.createElement("style"); | |||
| 199 | + | componentStyleTag.setAttribute("data-source", "dumber-component-styles"); | |||
| 200 | + | componentStyleTag.textContent = componentStyles; | |||
| 201 | + | shadowRoot.appendChild(componentStyleTag); | |||
| 202 | + | console.log( | |||
| 203 | + | "[shadowHost] Component styles injected:", | |||
| 204 | + | componentStyles.length, | |||
| 205 | + | "chars", | |||
| 206 | + | ); | |||
| 207 | + | } else { | |||
| 208 | + | console.warn("[shadowHost] Component styles not available on window"); | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | console.log( | |||
| 212 | + | "[shadowHost] All styles injected, shadow root children:", | |||
| 213 | + | shadowRoot.childNodes.length, | |||
| 214 | + | ); | |||
| 215 | + | } catch (error) { | |||
| 216 | + | console.error("[shadowHost] Failed to inject styles:", error); | |||
| 217 | + | // Fallback attempt | |||
| 218 | + | try { | |||
| 219 | + | const fallbackStyle = document.createElement("style"); | |||
| 220 | + | fallbackStyle.textContent = globalStyles || ""; | |||
| 221 | + | shadowRoot.appendChild(fallbackStyle); | |||
| 222 | + | } catch (fallbackError) { | |||
| 223 | + | console.error( | |||
| 224 | + | "[shadowHost] Fallback style injection also failed:", | |||
| 225 | + | fallbackError, | |||
| 226 | + | ); | |||
| 227 | + | } | |||
| 212 | } | 228 | } | ||
| 213 | shadowResetApplied.add(shadowRoot); | 229 | shadowResetApplied.add(shadowRoot); | ||
| 214 | } | 230 | } | ||
M gui/vite.config.gui.ts +2 -2
| @@ -5,7 +5,7 @@ import { resolve } from "path"; | |||
| 5 | 5 | export default defineConfig({ | |
| 6 | 6 | plugins: [ | |
| 7 | 7 | svelte({ | |
| 8 | - | emitCss: false, // Inline component CSS into JS bundle | |
| 8 | + | emitCss: true, // Extract component CSS to separate file for isolated world injection | |
| 9 | 9 | }), | |
| 10 | 10 | ], | |
| 11 | 11 | build: { | |
| @@ -18,7 +18,7 @@ export default defineConfig({ | |||
| 18 | 18 | name: "__dumberGUI", | |
| 19 | 19 | inlineDynamicImports: true, | |
| 20 | 20 | manualChunks: undefined, // Prevent chunk splitting | |
| 21 | - | assetFileNames: "[name].[ext]", // Don't hash asset names since they're inlined | |
| 21 | + | assetFileNames: "gui.min.[ext]", // CSS will be named gui.min.css | |
| 22 | 22 | }, | |
| 23 | 23 | }, | |
| 24 | 24 | emptyOutDir: false, | |
| @@ -5,7 +5,7 @@ import { resolve } from "path"; | |||||
| 5 | export default defineConfig({ | 5 | export default defineConfig({ | ||
| 6 | plugins: [ | 6 | plugins: [ | ||
| 7 | svelte({ | 7 | svelte({ | ||
| 8 | - | emitCss: false, // Inline component CSS into JS bundle | 8 | + | emitCss: true, // Extract component CSS to separate file for isolated world injection |
| 9 | }), | 9 | }), | ||
| 10 | ], | 10 | ], | ||
| 11 | build: { | 11 | build: { | ||
| @@ -18,7 +18,7 @@ export default defineConfig({ | |||||
| 18 | name: "__dumberGUI", | 18 | name: "__dumberGUI", | ||
| 19 | inlineDynamicImports: true, | 19 | inlineDynamicImports: true, | ||
| 20 | manualChunks: undefined, // Prevent chunk splitting | 20 | manualChunks: undefined, // Prevent chunk splitting | ||
| 21 | - | assetFileNames: "[name].[ext]", // Don't hash asset names since they're inlined | 21 | + | assetFileNames: "gui.min.[ext]", // CSS will be named gui.min.css |
| 22 | }, | 22 | }, | ||
| 23 | }, | 23 | }, | ||
| 24 | emptyOutDir: false, | 24 | emptyOutDir: false, | ||
M pkg/webkit/user_content.go +105 -25
| @@ -9,6 +9,13 @@ import ( | |||
| 9 | 9 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | + | const ( | |
| 13 | + | // DumberIsolatedWorld is the name of the isolated JavaScript world | |
| 14 | + | // where Dumber's GUI scripts run. This prevents page scripts from | |
| 15 | + | // interfering with the browser's UI components. | |
| 16 | + | DumberIsolatedWorld = "dumber-isolated" | |
| 17 | + | ) | |
| 18 | + | ||
| 12 | 19 | // SetupUserContentManager configures UserContentManager for the WebView | |
| 13 | 20 | // This injects GUI scripts at document-start and registers message handlers | |
| 14 | 21 | func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error { | |
| @@ -23,22 +30,35 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, | |||
| 23 | 30 | return nil | |
| 24 | 31 | } | |
| 25 | 32 | ||
| 26 | - | // Inject webview ID FIRST, so GUI scripts can access it immediately | |
| 33 | + | // Inject webview ID in BOTH main world and isolated world | |
| 34 | + | // The isolated world (GUI scripts) needs this, and pages might use it for communication | |
| 27 | 35 | // Note: webviewID is uint64, formatted as number (not string) to avoid any injection concerns | |
| 28 | 36 | webviewIDScript := fmt.Sprintf(` | |
| 29 | 37 | window.__dumber_webview_id = %d; | |
| 30 | 38 | console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id); | |
| 31 | 39 | `, webviewID) | |
| 40 | + | ||
| 41 | + | // Main world (for page scripts) | |
| 32 | 42 | ucm.AddScript(webkit.NewUserScript( | |
| 33 | 43 | webviewIDScript, | |
| 34 | - | webkit.UserContentInjectTopFrame, // Only top frame needs the webview ID | |
| 44 | + | webkit.UserContentInjectTopFrame, | |
| 45 | + | webkit.UserScriptInjectAtDocumentStart, | |
| 46 | + | nil, | |
| 47 | + | nil, | |
| 48 | + | )) | |
| 49 | + | ||
| 50 | + | // Isolated world (for GUI scripts) | |
| 51 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |
| 52 | + | webviewIDScript, | |
| 53 | + | webkit.UserContentInjectTopFrame, | |
| 35 | 54 | webkit.UserScriptInjectAtDocumentStart, | |
| 55 | + | DumberIsolatedWorld, | |
| 36 | 56 | nil, | |
| 37 | 57 | nil, | |
| 38 | 58 | )) | |
| 39 | - | log.Printf("[webkit] Injected webview ID script for ID: %d", webviewID) | |
| 59 | + | log.Printf("[webkit] Injected webview ID script for ID: %d (main + isolated world)", webviewID) | |
| 40 | 60 | ||
| 41 | - | // Inject GTK theme detection SECOND, before color-scheme script | |
| 61 | + | // Inject GTK theme detection in BOTH worlds | |
| 42 | 62 | // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set | |
| 43 | 63 | // Respect the ColorScheme config setting | |
| 44 | 64 | cfg := config.Get() | |
| @@ -57,71 +77,131 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, | |||
| 57 | 77 | } | |
| 58 | 78 | ||
| 59 | 79 | gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark) | |
| 80 | + | ||
| 81 | + | // Main world | |
| 60 | 82 | ucm.AddScript(webkit.NewUserScript( | |
| 61 | 83 | gtkThemeScript, | |
| 62 | - | webkit.UserContentInjectTopFrame, // Only top frame needs theme preference | |
| 84 | + | webkit.UserContentInjectTopFrame, | |
| 63 | 85 | webkit.UserScriptInjectAtDocumentStart, | |
| 64 | 86 | nil, | |
| 65 | 87 | nil, | |
| 66 | 88 | )) | |
| 67 | - | log.Printf("[webkit] Injected theme preference: prefersDark=%t", prefersDark) | |
| 68 | 89 | ||
| 69 | - | // Inject palette config SECOND, before GUI scripts | |
| 90 | + | // Isolated world | |
| 91 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |
| 92 | + | gtkThemeScript, | |
| 93 | + | webkit.UserContentInjectTopFrame, | |
| 94 | + | webkit.UserScriptInjectAtDocumentStart, | |
| 95 | + | DumberIsolatedWorld, | |
| 96 | + | nil, | |
| 97 | + | nil, | |
| 98 | + | )) | |
| 99 | + | log.Printf("[webkit] Injected theme preference: prefersDark=%t (main + isolated world)", prefersDark) | |
| 100 | + | ||
| 101 | + | // Inject palette config in BOTH worlds | |
| 70 | 102 | // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} } | |
| 71 | 103 | if appearanceConfigJSON != "" { | |
| 72 | 104 | paletteScript := fmt.Sprintf(`window.__dumber_palette = %s;`, appearanceConfigJSON) | |
| 105 | + | ||
| 106 | + | // Main world | |
| 73 | 107 | ucm.AddScript(webkit.NewUserScript( | |
| 74 | 108 | paletteScript, | |
| 75 | - | webkit.UserContentInjectTopFrame, // Only top frame needs palette config | |
| 109 | + | webkit.UserContentInjectTopFrame, | |
| 76 | 110 | webkit.UserScriptInjectAtDocumentStart, | |
| 77 | 111 | nil, | |
| 78 | 112 | nil, | |
| 79 | 113 | )) | |
| 80 | - | log.Printf("[webkit] Injected palette config at document-start (%d bytes)", len(paletteScript)) | |
| 114 | + | ||
| 115 | + | // Isolated world | |
| 116 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |
| 117 | + | paletteScript, | |
| 118 | + | webkit.UserContentInjectTopFrame, | |
| 119 | + | webkit.UserScriptInjectAtDocumentStart, | |
| 120 | + | DumberIsolatedWorld, | |
| 121 | + | nil, | |
| 122 | + | nil, | |
| 123 | + | )) | |
| 124 | + | log.Printf("[webkit] Injected palette config (%d bytes, main + isolated world)", len(paletteScript)) | |
| 81 | 125 | } | |
| 82 | 126 | ||
| 83 | - | // Inject color-scheme script at document-start | |
| 127 | + | // Inject color-scheme script in BOTH worlds | |
| 128 | + | // Main world: manipulates document.documentElement.classList for page scripts | |
| 129 | + | // Isolated world: sets CSS variables that GUI scripts can read via getComputedStyle | |
| 84 | 130 | if assets.ColorSchemeScript != "" { | |
| 131 | + | // Main world | |
| 85 | 132 | ucm.AddScript(webkit.NewUserScript( | |
| 86 | 133 | assets.ColorSchemeScript, | |
| 87 | - | webkit.UserContentInjectTopFrame, // Only top frame needs color scheme detection | |
| 134 | + | webkit.UserContentInjectTopFrame, | |
| 88 | 135 | webkit.UserScriptInjectAtDocumentStart, | |
| 89 | - | nil, // whitelist (nil = all) | |
| 90 | - | nil, // blacklist (nil = none) | |
| 136 | + | nil, | |
| 137 | + | nil, | |
| 91 | 138 | )) | |
| 92 | - | log.Printf("[webkit] Injected color-scheme script (%d bytes)", len(assets.ColorSchemeScript)) | |
| 139 | + | ||
| 140 | + | // Isolated world - needed so GUI scripts can access CSS variables | |
| 141 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |
| 142 | + | assets.ColorSchemeScript, | |
| 143 | + | webkit.UserContentInjectTopFrame, | |
| 144 | + | webkit.UserScriptInjectAtDocumentStart, | |
| 145 | + | DumberIsolatedWorld, | |
| 146 | + | nil, | |
| 147 | + | nil, | |
| 148 | + | )) | |
| 149 | + | log.Printf("[webkit] Injected color-scheme script (%d bytes, main + isolated world)", len(assets.ColorSchemeScript)) | |
| 93 | 150 | } | |
| 94 | 151 | ||
| 95 | - | // Inject main-world script at document-start | |
| 152 | + | // Inject main-world script in MAIN world | |
| 153 | + | // This script needs webkit.messageHandlers access to forward isolated world messages | |
| 154 | + | // It also handles theme, zoom, and other main-world only APIs | |
| 96 | 155 | if assets.MainWorldScript != "" { | |
| 97 | 156 | ucm.AddScript(webkit.NewUserScript( | |
| 98 | 157 | assets.MainWorldScript, | |
| 99 | - | webkit.UserContentInjectTopFrame, // Only top frame needs bridge/UI functionality | |
| 158 | + | webkit.UserContentInjectTopFrame, | |
| 100 | 159 | webkit.UserScriptInjectAtDocumentStart, | |
| 101 | 160 | nil, | |
| 102 | 161 | nil, | |
| 103 | 162 | )) | |
| 104 | - | log.Printf("[webkit] Injected main-world script (%d bytes)", len(assets.MainWorldScript)) | |
| 163 | + | log.Printf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript)) | |
| 105 | 164 | } | |
| 106 | 165 | ||
| 107 | - | // Inject GUI controls script at document-start | |
| 166 | + | // Inject GUI controls script in ISOLATED world only | |
| 167 | + | // This contains Svelte components and must be protected from page interference | |
| 108 | 168 | if assets.GUIScript != "" { | |
| 109 | - | ucm.AddScript(webkit.NewUserScript( | |
| 169 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |
| 110 | 170 | assets.GUIScript, | |
| 111 | - | webkit.UserContentInjectTopFrame, // Only top frame needs GUI controls | |
| 171 | + | webkit.UserContentInjectTopFrame, | |
| 172 | + | webkit.UserScriptInjectAtDocumentStart, | |
| 173 | + | DumberIsolatedWorld, | |
| 174 | + | nil, | |
| 175 | + | nil, | |
| 176 | + | )) | |
| 177 | + | log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world)", len(assets.GUIScript)) | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // Inject component CSS styles as a JavaScript variable in ISOLATED world | |
| 181 | + | // These styles need to be injected into the shadow root by shadowHost.ts | |
| 182 | + | // because Shadow DOM has style encapsulation - external stylesheets don't penetrate it | |
| 183 | + | if assets.ComponentStyles != "" { | |
| 184 | + | // Use fmt.Sprintf with %q to properly escape the CSS string for JavaScript | |
| 185 | + | componentStylesScript := fmt.Sprintf(`window.__dumber_component_styles = %q;`, assets.ComponentStyles) | |
| 186 | + | ||
| 187 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |
| 188 | + | componentStylesScript, | |
| 189 | + | webkit.UserContentInjectTopFrame, | |
| 112 | 190 | webkit.UserScriptInjectAtDocumentStart, | |
| 191 | + | DumberIsolatedWorld, | |
| 113 | 192 | nil, | |
| 114 | 193 | nil, | |
| 115 | 194 | )) | |
| 116 | - | log.Printf("[webkit] Injected GUI controls script (%d bytes)", len(assets.GUIScript)) | |
| 195 | + | log.Printf("[webkit] Injected component styles string (%d bytes, isolated world)", len(assets.ComponentStyles)) | |
| 117 | 196 | } | |
| 118 | 197 | ||
| 119 | - | // Register script message handler "dumber" for communication from JS | |
| 120 | - | // Pass empty string for worldName to use the default world | |
| 198 | + | // Register script message handler "dumber" in the MAIN world | |
| 199 | + | // Isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler | |
| 200 | + | // This architecture is required because webkit.messageHandlers is only available in main world | |
| 121 | 201 | if !ucm.RegisterScriptMessageHandler("dumber", "") { | |
| 122 | - | log.Printf("[webkit] Warning: failed to register 'dumber' script message handler") | |
| 202 | + | log.Printf("[webkit] Warning: failed to register 'dumber' script message handler in main world") | |
| 123 | 203 | } else { | |
| 124 | - | log.Printf("[webkit] Registered 'dumber' script message handler") | |
| 204 | + | log.Printf("[webkit] Registered 'dumber' script message handler in main world (default)") | |
| 125 | 205 | } | |
| 126 | 206 | ||
| 127 | 207 | return nil | |
| @@ -9,6 +9,13 @@ import ( | |||||
| 9 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | 9 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | ||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | + | const ( | |||
| 13 | + | // DumberIsolatedWorld is the name of the isolated JavaScript world | |||
| 14 | + | // where Dumber's GUI scripts run. This prevents page scripts from | |||
| 15 | + | // interfering with the browser's UI components. | |||
| 16 | + | DumberIsolatedWorld = "dumber-isolated" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 12 | // SetupUserContentManager configures UserContentManager for the WebView | 19 | // SetupUserContentManager configures UserContentManager for the WebView | ||
| 13 | // This injects GUI scripts at document-start and registers message handlers | 20 | // This injects GUI scripts at document-start and registers message handlers | ||
| 14 | func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error { | 21 | func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error { | ||
| @@ -23,22 +30,35 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, | |||||
| 23 | return nil | 30 | return nil | ||
| 24 | } | 31 | } | ||
| 25 | 32 | ||||
| 26 | - | // Inject webview ID FIRST, so GUI scripts can access it immediately | 33 | + | // Inject webview ID in BOTH main world and isolated world |
| 34 | + | // The isolated world (GUI scripts) needs this, and pages might use it for communication | |||
| 27 | // Note: webviewID is uint64, formatted as number (not string) to avoid any injection concerns | 35 | // Note: webviewID is uint64, formatted as number (not string) to avoid any injection concerns | ||
| 28 | webviewIDScript := fmt.Sprintf(` | 36 | webviewIDScript := fmt.Sprintf(` | ||
| 29 | window.__dumber_webview_id = %d; | 37 | window.__dumber_webview_id = %d; | ||
| 30 | console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id); | 38 | console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id); | ||
| 31 | `, webviewID) | 39 | `, webviewID) | ||
| 40 | + | ||||
| 41 | + | // Main world (for page scripts) | |||
| 32 | ucm.AddScript(webkit.NewUserScript( | 42 | ucm.AddScript(webkit.NewUserScript( | ||
| 33 | webviewIDScript, | 43 | webviewIDScript, | ||
| 34 | - | webkit.UserContentInjectTopFrame, // Only top frame needs the webview ID | 44 | + | webkit.UserContentInjectTopFrame, |
| 45 | + | webkit.UserScriptInjectAtDocumentStart, | |||
| 46 | + | nil, | |||
| 47 | + | nil, | |||
| 48 | + | )) | |||
| 49 | + | ||||
| 50 | + | // Isolated world (for GUI scripts) | |||
| 51 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |||
| 52 | + | webviewIDScript, | |||
| 53 | + | webkit.UserContentInjectTopFrame, | |||
| 35 | webkit.UserScriptInjectAtDocumentStart, | 54 | webkit.UserScriptInjectAtDocumentStart, | ||
| 55 | + | DumberIsolatedWorld, | |||
| 36 | nil, | 56 | nil, | ||
| 37 | nil, | 57 | nil, | ||
| 38 | )) | 58 | )) | ||
| 39 | - | log.Printf("[webkit] Injected webview ID script for ID: %d", webviewID) | 59 | + | log.Printf("[webkit] Injected webview ID script for ID: %d (main + isolated world)", webviewID) |
| 40 | 60 | ||||
| 41 | - | // Inject GTK theme detection SECOND, before color-scheme script | 61 | + | // Inject GTK theme detection in BOTH worlds |
| 42 | // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set | 62 | // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set | ||
| 43 | // Respect the ColorScheme config setting | 63 | // Respect the ColorScheme config setting | ||
| 44 | cfg := config.Get() | 64 | cfg := config.Get() | ||
| @@ -57,71 +77,131 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, | |||||
| 57 | } | 77 | } | ||
| 58 | 78 | ||||
| 59 | gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark) | 79 | gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark) | ||
| 80 | + | ||||
| 81 | + | // Main world | |||
| 60 | ucm.AddScript(webkit.NewUserScript( | 82 | ucm.AddScript(webkit.NewUserScript( | ||
| 61 | gtkThemeScript, | 83 | gtkThemeScript, | ||
| 62 | - | webkit.UserContentInjectTopFrame, // Only top frame needs theme preference | 84 | + | webkit.UserContentInjectTopFrame, |
| 63 | webkit.UserScriptInjectAtDocumentStart, | 85 | webkit.UserScriptInjectAtDocumentStart, | ||
| 64 | nil, | 86 | nil, | ||
| 65 | nil, | 87 | nil, | ||
| 66 | )) | 88 | )) | ||
| 67 | - | log.Printf("[webkit] Injected theme preference: prefersDark=%t", prefersDark) | |||
| 68 | 89 | ||||
| 69 | - | // Inject palette config SECOND, before GUI scripts | 90 | + | // Isolated world |
| 91 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |||
| 92 | + | gtkThemeScript, | |||
| 93 | + | webkit.UserContentInjectTopFrame, | |||
| 94 | + | webkit.UserScriptInjectAtDocumentStart, | |||
| 95 | + | DumberIsolatedWorld, | |||
| 96 | + | nil, | |||
| 97 | + | nil, | |||
| 98 | + | )) | |||
| 99 | + | log.Printf("[webkit] Injected theme preference: prefersDark=%t (main + isolated world)", prefersDark) | |||
| 100 | + | ||||
| 101 | + | // Inject palette config in BOTH worlds | |||
| 70 | // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} } | 102 | // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} } | ||
| 71 | if appearanceConfigJSON != "" { | 103 | if appearanceConfigJSON != "" { | ||
| 72 | paletteScript := fmt.Sprintf(`window.__dumber_palette = %s;`, appearanceConfigJSON) | 104 | paletteScript := fmt.Sprintf(`window.__dumber_palette = %s;`, appearanceConfigJSON) | ||
| 105 | + | ||||
| 106 | + | // Main world | |||
| 73 | ucm.AddScript(webkit.NewUserScript( | 107 | ucm.AddScript(webkit.NewUserScript( | ||
| 74 | paletteScript, | 108 | paletteScript, | ||
| 75 | - | webkit.UserContentInjectTopFrame, // Only top frame needs palette config | 109 | + | webkit.UserContentInjectTopFrame, |
| 76 | webkit.UserScriptInjectAtDocumentStart, | 110 | webkit.UserScriptInjectAtDocumentStart, | ||
| 77 | nil, | 111 | nil, | ||
| 78 | nil, | 112 | nil, | ||
| 79 | )) | 113 | )) | ||
| 80 | - | log.Printf("[webkit] Injected palette config at document-start (%d bytes)", len(paletteScript)) | 114 | + | |
| 115 | + | // Isolated world | |||
| 116 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |||
| 117 | + | paletteScript, | |||
| 118 | + | webkit.UserContentInjectTopFrame, | |||
| 119 | + | webkit.UserScriptInjectAtDocumentStart, | |||
| 120 | + | DumberIsolatedWorld, | |||
| 121 | + | nil, | |||
| 122 | + | nil, | |||
| 123 | + | )) | |||
| 124 | + | log.Printf("[webkit] Injected palette config (%d bytes, main + isolated world)", len(paletteScript)) | |||
| 81 | } | 125 | } | ||
| 82 | 126 | ||||
| 83 | - | // Inject color-scheme script at document-start | 127 | + | // Inject color-scheme script in BOTH worlds |
| 128 | + | // Main world: manipulates document.documentElement.classList for page scripts | |||
| 129 | + | // Isolated world: sets CSS variables that GUI scripts can read via getComputedStyle | |||
| 84 | if assets.ColorSchemeScript != "" { | 130 | if assets.ColorSchemeScript != "" { | ||
| 131 | + | // Main world | |||
| 85 | ucm.AddScript(webkit.NewUserScript( | 132 | ucm.AddScript(webkit.NewUserScript( | ||
| 86 | assets.ColorSchemeScript, | 133 | assets.ColorSchemeScript, | ||
| 87 | - | webkit.UserContentInjectTopFrame, // Only top frame needs color scheme detection | 134 | + | webkit.UserContentInjectTopFrame, |
| 88 | webkit.UserScriptInjectAtDocumentStart, | 135 | webkit.UserScriptInjectAtDocumentStart, | ||
| 89 | - | nil, // whitelist (nil = all) | 136 | + | nil, |
| 90 | - | nil, // blacklist (nil = none) | 137 | + | nil, |
| 91 | )) | 138 | )) | ||
| 92 | - | log.Printf("[webkit] Injected color-scheme script (%d bytes)", len(assets.ColorSchemeScript)) | 139 | + | |
| 140 | + | // Isolated world - needed so GUI scripts can access CSS variables | |||
| 141 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |||
| 142 | + | assets.ColorSchemeScript, | |||
| 143 | + | webkit.UserContentInjectTopFrame, | |||
| 144 | + | webkit.UserScriptInjectAtDocumentStart, | |||
| 145 | + | DumberIsolatedWorld, | |||
| 146 | + | nil, | |||
| 147 | + | nil, | |||
| 148 | + | )) | |||
| 149 | + | log.Printf("[webkit] Injected color-scheme script (%d bytes, main + isolated world)", len(assets.ColorSchemeScript)) | |||
| 93 | } | 150 | } | ||
| 94 | 151 | ||||
| 95 | - | // Inject main-world script at document-start | 152 | + | // Inject main-world script in MAIN world |
| 153 | + | // This script needs webkit.messageHandlers access to forward isolated world messages | |||
| 154 | + | // It also handles theme, zoom, and other main-world only APIs | |||
| 96 | if assets.MainWorldScript != "" { | 155 | if assets.MainWorldScript != "" { | ||
| 97 | ucm.AddScript(webkit.NewUserScript( | 156 | ucm.AddScript(webkit.NewUserScript( | ||
| 98 | assets.MainWorldScript, | 157 | assets.MainWorldScript, | ||
| 99 | - | webkit.UserContentInjectTopFrame, // Only top frame needs bridge/UI functionality | 158 | + | webkit.UserContentInjectTopFrame, |
| 100 | webkit.UserScriptInjectAtDocumentStart, | 159 | webkit.UserScriptInjectAtDocumentStart, | ||
| 101 | nil, | 160 | nil, | ||
| 102 | nil, | 161 | nil, | ||
| 103 | )) | 162 | )) | ||
| 104 | - | log.Printf("[webkit] Injected main-world script (%d bytes)", len(assets.MainWorldScript)) | 163 | + | log.Printf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript)) |
| 105 | } | 164 | } | ||
| 106 | 165 | ||||
| 107 | - | // Inject GUI controls script at document-start | 166 | + | // Inject GUI controls script in ISOLATED world only |
| 167 | + | // This contains Svelte components and must be protected from page interference | |||
| 108 | if assets.GUIScript != "" { | 168 | if assets.GUIScript != "" { | ||
| 109 | - | ucm.AddScript(webkit.NewUserScript( | 169 | + | ucm.AddScript(webkit.NewUserScriptForWorld( |
| 110 | assets.GUIScript, | 170 | assets.GUIScript, | ||
| 111 | - | webkit.UserContentInjectTopFrame, // Only top frame needs GUI controls | 171 | + | webkit.UserContentInjectTopFrame, |
| 172 | + | webkit.UserScriptInjectAtDocumentStart, | |||
| 173 | + | DumberIsolatedWorld, | |||
| 174 | + | nil, | |||
| 175 | + | nil, | |||
| 176 | + | )) | |||
| 177 | + | log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world)", len(assets.GUIScript)) | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // Inject component CSS styles as a JavaScript variable in ISOLATED world | |||
| 181 | + | // These styles need to be injected into the shadow root by shadowHost.ts | |||
| 182 | + | // because Shadow DOM has style encapsulation - external stylesheets don't penetrate it | |||
| 183 | + | if assets.ComponentStyles != "" { | |||
| 184 | + | // Use fmt.Sprintf with %q to properly escape the CSS string for JavaScript | |||
| 185 | + | componentStylesScript := fmt.Sprintf(`window.__dumber_component_styles = %q;`, assets.ComponentStyles) | |||
| 186 | + | ||||
| 187 | + | ucm.AddScript(webkit.NewUserScriptForWorld( | |||
| 188 | + | componentStylesScript, | |||
| 189 | + | webkit.UserContentInjectTopFrame, | |||
| 112 | webkit.UserScriptInjectAtDocumentStart, | 190 | webkit.UserScriptInjectAtDocumentStart, | ||
| 191 | + | DumberIsolatedWorld, | |||
| 113 | nil, | 192 | nil, | ||
| 114 | nil, | 193 | nil, | ||
| 115 | )) | 194 | )) | ||
| 116 | - | log.Printf("[webkit] Injected GUI controls script (%d bytes)", len(assets.GUIScript)) | 195 | + | log.Printf("[webkit] Injected component styles string (%d bytes, isolated world)", len(assets.ComponentStyles)) |
| 117 | } | 196 | } | ||
| 118 | 197 | ||||
| 119 | - | // Register script message handler "dumber" for communication from JS | 198 | + | // Register script message handler "dumber" in the MAIN world |
| 120 | - | // Pass empty string for worldName to use the default world | 199 | + | // Isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler |
| 200 | + | // This architecture is required because webkit.messageHandlers is only available in main world | |||
| 121 | if !ucm.RegisterScriptMessageHandler("dumber", "") { | 201 | if !ucm.RegisterScriptMessageHandler("dumber", "") { | ||
| 122 | - | log.Printf("[webkit] Warning: failed to register 'dumber' script message handler") | 202 | + | log.Printf("[webkit] Warning: failed to register 'dumber' script message handler in main world") |
| 123 | } else { | 203 | } else { | ||
| 124 | - | log.Printf("[webkit] Registered 'dumber' script message handler") | 204 | + | log.Printf("[webkit] Registered 'dumber' script message handler in main world (default)") |
| 125 | } | 205 | } | ||
| 126 | 206 | ||||
| 127 | return nil | 207 | return nil | ||
M pkg/webkit/webview.go +3 -0
| @@ -731,6 +731,9 @@ func (w *WebView) InjectScript(script string) error { | |||
| 731 | 731 | } | |
| 732 | 732 | ||
| 733 | 733 | // DispatchCustomEvent dispatches a custom event via JavaScript | |
| 734 | + | // This works across JavaScript world boundaries because it uses document.dispatchEvent. | |
| 735 | + | // GUI scripts running in the isolated world (see DumberIsolatedWorld in user_content.go) | |
| 736 | + | // can listen for these events because they share the same Document object with the main world. | |
| 734 | 737 | func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error { | |
| 735 | 738 | // Serialize data to JSON | |
| 736 | 739 | jsonData, err := json.Marshal(data) | |
| @@ -731,6 +731,9 @@ func (w *WebView) InjectScript(script string) error { | |||||
| 731 | } | 731 | } | ||
| 732 | 732 | ||||
| 733 | // DispatchCustomEvent dispatches a custom event via JavaScript | 733 | // DispatchCustomEvent dispatches a custom event via JavaScript | ||
| 734 | + | // This works across JavaScript world boundaries because it uses document.dispatchEvent. | |||
| 735 | + | // GUI scripts running in the isolated world (see DumberIsolatedWorld in user_content.go) | |||
| 736 | + | // can listen for these events because they share the same Document object with the main world. | |||
| 734 | func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error { | 737 | func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error { | ||
| 735 | // Serialize data to JSON | 738 | // Serialize data to JSON | ||
| 736 | jsonData, err := json.Marshal(data) | 739 | jsonData, err := json.Marshal(data) | ||