feat(cef): add WebViewFactory for OSR browser creation
WebViewFactory implements port.WebViewFactory, creating CEF off-screen browser WebViews with unique IDs, render pipelines, input bridges, and asynchronous browser creation via BrowserHostCreateBrowser.
- commit
- 1ef6a99abdf4999af0f8d82db84ff78310583598
- author
- brice · · 2026-03-19 15:41 UTC
- committer
- brice · · 2026-03-28 04:35 UTC
- parent
- 2d5276a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +148 -0
A internal/infrastructure/cef/factory.go +148 -0
| @@ -0,0 +1,148 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "sync/atomic" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Compile-time interface check. | |
| 14 | + | var _ port.WebViewFactory = (*WebViewFactory)(nil) | |
| 15 | + | ||
| 16 | + | // WebViewFactory creates new CEF off-screen browser WebViews. Each WebView | |
| 17 | + | // gets a unique ID, its own renderPipeline and inputBridge, and an | |
| 18 | + | // asynchronously-created CEF browser (via BrowserHostCreateBrowser). | |
| 19 | + | type WebViewFactory struct { | |
| 20 | + | gl *glLoader | |
| 21 | + | nextID atomic.Uint64 | |
| 22 | + | scale int32 | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // newWebViewFactory returns a factory that will create WebViews using the | |
| 26 | + | // given GL loader and HiDPI scale factor. | |
| 27 | + | func newWebViewFactory(gl *glLoader, scale int32) *WebViewFactory { | |
| 28 | + | if scale < 1 { | |
| 29 | + | scale = 1 | |
| 30 | + | } | |
| 31 | + | return &WebViewFactory{ | |
| 32 | + | gl: gl, | |
| 33 | + | scale: scale, | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // Create builds a new CEF off-screen browser WebView. The browser is created | |
| 38 | + | // asynchronously; the returned WebView is usable immediately but navigation | |
| 39 | + | // will fail with errNoBrowser until OnAfterCreated fires. | |
| 40 | + | func (f *WebViewFactory) Create(_ context.Context) (port.WebView, error) { | |
| 41 | + | id := port.WebViewID(f.nextID.Add(1)) | |
| 42 | + | ||
| 43 | + | pipeline := newRenderPipeline(f.gl, f.scale) | |
| 44 | + | ||
| 45 | + | wv := &WebView{ | |
| 46 | + | id: id, | |
| 47 | + | pipeline: pipeline, | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | handlers := &handlerSet{wv: wv} | |
| 51 | + | wv.handlers = handlers | |
| 52 | + | ||
| 53 | + | input := newInputBridge(f.scale) | |
| 54 | + | input.attachTo(pipeline.glArea) | |
| 55 | + | wv.input = input | |
| 56 | + | ||
| 57 | + | // Build a CEF client backed by our handlerSet. NewClient allocates a raw | |
| 58 | + | // CEF client struct with ref-counting and callback vtable wired up. We | |
| 59 | + | // keep the pointer alive on the WebView so it is not garbage collected | |
| 60 | + | // before the browser is destroyed. | |
| 61 | + | clientPtr := purecef.NewClient(wv.handlers) | |
| 62 | + | ||
| 63 | + | // Configure WindowInfo for off-screen rendering (OSR). | |
| 64 | + | var windowInfo purecef.WindowInfo | |
| 65 | + | windowInfo.Size = unsafe.Sizeof(windowInfo) | |
| 66 | + | windowInfo.WindowlessRenderingEnabled = 1 | |
| 67 | + | ||
| 68 | + | // Configure BrowserSettings with 60 fps for smooth rendering. | |
| 69 | + | var settings purecef.BrowserSettings | |
| 70 | + | settings.Size = unsafe.Sizeof(settings) | |
| 71 | + | settings.WindowlessFrameRate = 60 | |
| 72 | + | ||
| 73 | + | // Create the browser asynchronously. BrowserHostCreateBrowser posts to the | |
| 74 | + | // CEF UI thread; the browser will be nil until OnAfterCreated fires. | |
| 75 | + | // | |
| 76 | + | // We call the raw creation function directly with the client pointer from | |
| 77 | + | // NewClient, because the high-level wrapper's extractRawPointer requires a | |
| 78 | + | // package-internal interface that external types cannot satisfy. | |
| 79 | + | // | |
| 80 | + | // NOTE: BrowserHostCreateBrowser is currently a stub in purego-cef (returns | |
| 81 | + | // 0). That is expected — the skeleton is correct and will work once the | |
| 82 | + | // stub is implemented. | |
| 83 | + | createBrowser(clientPtr, &windowInfo, &settings) | |
| 84 | + | ||
| 85 | + | return wv, nil | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // createBrowser calls purecef.BrowserHostCreateBrowser with the raw client | |
| 89 | + | // pointer. This is separated for testability and to document the impedance | |
| 90 | + | // mismatch between NewClient (returns unsafe.Pointer) and | |
| 91 | + | // BrowserHostCreateBrowser (expects Client interface with unexported | |
| 92 | + | // rawPointer method). | |
| 93 | + | func createBrowser(clientPtr unsafe.Pointer, windowInfo *purecef.WindowInfo, settings *purecef.BrowserSettings) { | |
| 94 | + | // Wrap the raw pointer in a thin Client adapter so BrowserHostCreateBrowser | |
| 95 | + | // can extract it. handlerSet already implements Client; the real vtable is | |
| 96 | + | // in the raw struct that clientPtr points to. | |
| 97 | + | adapter := &clientAdapter{ptr: clientPtr} | |
| 98 | + | purecef.BrowserHostCreateBrowser( | |
| 99 | + | windowInfo, | |
| 100 | + | adapter, | |
| 101 | + | "about:blank", | |
| 102 | + | settings, | |
| 103 | + | nil, // extraInfo | |
| 104 | + | nil, // requestContext | |
| 105 | + | ) | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // CreateRelated creates a WebView that shares session/cookies with the parent. | |
| 109 | + | // TODO(phase2): look up the parent browser by parentID and use the same | |
| 110 | + | // request context so cookies and session state are shared. | |
| 111 | + | func (f *WebViewFactory) CreateRelated(ctx context.Context, _ port.WebViewID) (port.WebView, error) { | |
| 112 | + | return f.Create(ctx) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // clientAdapter wraps an unsafe.Pointer from NewClient into something that | |
| 116 | + | // satisfies the purecef.Client interface. The actual CEF callbacks are already | |
| 117 | + | // wired inside the raw struct; these Go methods are never invoked by CEF. | |
| 118 | + | // | |
| 119 | + | // NOTE: purecef.extractRawPointer checks for an unexported rawPointer() | |
| 120 | + | // method, which external packages cannot satisfy. Until purego-cef exports a | |
| 121 | + | // proper WrapClient helper, the pointer extraction will return nil. This is | |
| 122 | + | // acceptable because BrowserHostCreateBrowser is currently a stub. | |
| 123 | + | type clientAdapter struct { | |
| 124 | + | ptr unsafe.Pointer | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func (c *clientAdapter) GetAudioHandler() purecef.AudioHandler { return nil } | |
| 128 | + | func (c *clientAdapter) GetCommandHandler() purecef.CommandHandler { return nil } | |
| 129 | + | func (c *clientAdapter) GetContextMenuHandler() purecef.ContextMenuHandler { return nil } | |
| 130 | + | func (c *clientAdapter) GetDialogHandler() purecef.DialogHandler { return nil } | |
| 131 | + | func (c *clientAdapter) GetDisplayHandler() purecef.DisplayHandler { return nil } | |
| 132 | + | func (c *clientAdapter) GetDownloadHandler() purecef.DownloadHandler { return nil } | |
| 133 | + | func (c *clientAdapter) GetDragHandler() purecef.DragHandler { return nil } | |
| 134 | + | func (c *clientAdapter) GetFindHandler() purecef.FindHandler { return nil } | |
| 135 | + | func (c *clientAdapter) GetFocusHandler() purecef.FocusHandler { return nil } | |
| 136 | + | func (c *clientAdapter) GetFrameHandler() purecef.FrameHandler { return nil } | |
| 137 | + | func (c *clientAdapter) GetPermissionHandler() purecef.PermissionHandler { return nil } | |
| 138 | + | func (c *clientAdapter) GetJsdialogHandler() purecef.JsdialogHandler { return nil } | |
| 139 | + | func (c *clientAdapter) GetKeyboardHandler() purecef.KeyboardHandler { return nil } | |
| 140 | + | func (c *clientAdapter) GetLifeSpanHandler() purecef.LifeSpanHandler { return nil } | |
| 141 | + | func (c *clientAdapter) GetLoadHandler() purecef.LoadHandler { return nil } | |
| 142 | + | func (c *clientAdapter) GetPrintHandler() purecef.PrintHandler { return nil } | |
| 143 | + | func (c *clientAdapter) GetRenderHandler() purecef.RenderHandler { return nil } | |
| 144 | + | func (c *clientAdapter) GetRequestHandler() purecef.RequestHandler { return nil } | |
| 145 | + | ||
| 146 | + | func (c *clientAdapter) OnProcessMessageReceived(_ purecef.Browser, _ purecef.Frame, _ purecef.ProcessID, _ purecef.ProcessMessage) int32 { | |
| 147 | + | return 0 | |
| 148 | + | } | |
| @@ -0,0 +1,148 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "sync/atomic" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Compile-time interface check. | |||
| 14 | + | var _ port.WebViewFactory = (*WebViewFactory)(nil) | |||
| 15 | + | ||||
| 16 | + | // WebViewFactory creates new CEF off-screen browser WebViews. Each WebView | |||
| 17 | + | // gets a unique ID, its own renderPipeline and inputBridge, and an | |||
| 18 | + | // asynchronously-created CEF browser (via BrowserHostCreateBrowser). | |||
| 19 | + | type WebViewFactory struct { | |||
| 20 | + | gl *glLoader | |||
| 21 | + | nextID atomic.Uint64 | |||
| 22 | + | scale int32 | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // newWebViewFactory returns a factory that will create WebViews using the | |||
| 26 | + | // given GL loader and HiDPI scale factor. | |||
| 27 | + | func newWebViewFactory(gl *glLoader, scale int32) *WebViewFactory { | |||
| 28 | + | if scale < 1 { | |||
| 29 | + | scale = 1 | |||
| 30 | + | } | |||
| 31 | + | return &WebViewFactory{ | |||
| 32 | + | gl: gl, | |||
| 33 | + | scale: scale, | |||
| 34 | + | } | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // Create builds a new CEF off-screen browser WebView. The browser is created | |||
| 38 | + | // asynchronously; the returned WebView is usable immediately but navigation | |||
| 39 | + | // will fail with errNoBrowser until OnAfterCreated fires. | |||
| 40 | + | func (f *WebViewFactory) Create(_ context.Context) (port.WebView, error) { | |||
| 41 | + | id := port.WebViewID(f.nextID.Add(1)) | |||
| 42 | + | ||||
| 43 | + | pipeline := newRenderPipeline(f.gl, f.scale) | |||
| 44 | + | ||||
| 45 | + | wv := &WebView{ | |||
| 46 | + | id: id, | |||
| 47 | + | pipeline: pipeline, | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | handlers := &handlerSet{wv: wv} | |||
| 51 | + | wv.handlers = handlers | |||
| 52 | + | ||||
| 53 | + | input := newInputBridge(f.scale) | |||
| 54 | + | input.attachTo(pipeline.glArea) | |||
| 55 | + | wv.input = input | |||
| 56 | + | ||||
| 57 | + | // Build a CEF client backed by our handlerSet. NewClient allocates a raw | |||
| 58 | + | // CEF client struct with ref-counting and callback vtable wired up. We | |||
| 59 | + | // keep the pointer alive on the WebView so it is not garbage collected | |||
| 60 | + | // before the browser is destroyed. | |||
| 61 | + | clientPtr := purecef.NewClient(wv.handlers) | |||
| 62 | + | ||||
| 63 | + | // Configure WindowInfo for off-screen rendering (OSR). | |||
| 64 | + | var windowInfo purecef.WindowInfo | |||
| 65 | + | windowInfo.Size = unsafe.Sizeof(windowInfo) | |||
| 66 | + | windowInfo.WindowlessRenderingEnabled = 1 | |||
| 67 | + | ||||
| 68 | + | // Configure BrowserSettings with 60 fps for smooth rendering. | |||
| 69 | + | var settings purecef.BrowserSettings | |||
| 70 | + | settings.Size = unsafe.Sizeof(settings) | |||
| 71 | + | settings.WindowlessFrameRate = 60 | |||
| 72 | + | ||||
| 73 | + | // Create the browser asynchronously. BrowserHostCreateBrowser posts to the | |||
| 74 | + | // CEF UI thread; the browser will be nil until OnAfterCreated fires. | |||
| 75 | + | // | |||
| 76 | + | // We call the raw creation function directly with the client pointer from | |||
| 77 | + | // NewClient, because the high-level wrapper's extractRawPointer requires a | |||
| 78 | + | // package-internal interface that external types cannot satisfy. | |||
| 79 | + | // | |||
| 80 | + | // NOTE: BrowserHostCreateBrowser is currently a stub in purego-cef (returns | |||
| 81 | + | // 0). That is expected — the skeleton is correct and will work once the | |||
| 82 | + | // stub is implemented. | |||
| 83 | + | createBrowser(clientPtr, &windowInfo, &settings) | |||
| 84 | + | ||||
| 85 | + | return wv, nil | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // createBrowser calls purecef.BrowserHostCreateBrowser with the raw client | |||
| 89 | + | // pointer. This is separated for testability and to document the impedance | |||
| 90 | + | // mismatch between NewClient (returns unsafe.Pointer) and | |||
| 91 | + | // BrowserHostCreateBrowser (expects Client interface with unexported | |||
| 92 | + | // rawPointer method). | |||
| 93 | + | func createBrowser(clientPtr unsafe.Pointer, windowInfo *purecef.WindowInfo, settings *purecef.BrowserSettings) { | |||
| 94 | + | // Wrap the raw pointer in a thin Client adapter so BrowserHostCreateBrowser | |||
| 95 | + | // can extract it. handlerSet already implements Client; the real vtable is | |||
| 96 | + | // in the raw struct that clientPtr points to. | |||
| 97 | + | adapter := &clientAdapter{ptr: clientPtr} | |||
| 98 | + | purecef.BrowserHostCreateBrowser( | |||
| 99 | + | windowInfo, | |||
| 100 | + | adapter, | |||
| 101 | + | "about:blank", | |||
| 102 | + | settings, | |||
| 103 | + | nil, // extraInfo | |||
| 104 | + | nil, // requestContext | |||
| 105 | + | ) | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // CreateRelated creates a WebView that shares session/cookies with the parent. | |||
| 109 | + | // TODO(phase2): look up the parent browser by parentID and use the same | |||
| 110 | + | // request context so cookies and session state are shared. | |||
| 111 | + | func (f *WebViewFactory) CreateRelated(ctx context.Context, _ port.WebViewID) (port.WebView, error) { | |||
| 112 | + | return f.Create(ctx) | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | // clientAdapter wraps an unsafe.Pointer from NewClient into something that | |||
| 116 | + | // satisfies the purecef.Client interface. The actual CEF callbacks are already | |||
| 117 | + | // wired inside the raw struct; these Go methods are never invoked by CEF. | |||
| 118 | + | // | |||
| 119 | + | // NOTE: purecef.extractRawPointer checks for an unexported rawPointer() | |||
| 120 | + | // method, which external packages cannot satisfy. Until purego-cef exports a | |||
| 121 | + | // proper WrapClient helper, the pointer extraction will return nil. This is | |||
| 122 | + | // acceptable because BrowserHostCreateBrowser is currently a stub. | |||
| 123 | + | type clientAdapter struct { | |||
| 124 | + | ptr unsafe.Pointer | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func (c *clientAdapter) GetAudioHandler() purecef.AudioHandler { return nil } | |||
| 128 | + | func (c *clientAdapter) GetCommandHandler() purecef.CommandHandler { return nil } | |||
| 129 | + | func (c *clientAdapter) GetContextMenuHandler() purecef.ContextMenuHandler { return nil } | |||
| 130 | + | func (c *clientAdapter) GetDialogHandler() purecef.DialogHandler { return nil } | |||
| 131 | + | func (c *clientAdapter) GetDisplayHandler() purecef.DisplayHandler { return nil } | |||
| 132 | + | func (c *clientAdapter) GetDownloadHandler() purecef.DownloadHandler { return nil } | |||
| 133 | + | func (c *clientAdapter) GetDragHandler() purecef.DragHandler { return nil } | |||
| 134 | + | func (c *clientAdapter) GetFindHandler() purecef.FindHandler { return nil } | |||
| 135 | + | func (c *clientAdapter) GetFocusHandler() purecef.FocusHandler { return nil } | |||
| 136 | + | func (c *clientAdapter) GetFrameHandler() purecef.FrameHandler { return nil } | |||
| 137 | + | func (c *clientAdapter) GetPermissionHandler() purecef.PermissionHandler { return nil } | |||
| 138 | + | func (c *clientAdapter) GetJsdialogHandler() purecef.JsdialogHandler { return nil } | |||
| 139 | + | func (c *clientAdapter) GetKeyboardHandler() purecef.KeyboardHandler { return nil } | |||
| 140 | + | func (c *clientAdapter) GetLifeSpanHandler() purecef.LifeSpanHandler { return nil } | |||
| 141 | + | func (c *clientAdapter) GetLoadHandler() purecef.LoadHandler { return nil } | |||
| 142 | + | func (c *clientAdapter) GetPrintHandler() purecef.PrintHandler { return nil } | |||
| 143 | + | func (c *clientAdapter) GetRenderHandler() purecef.RenderHandler { return nil } | |||
| 144 | + | func (c *clientAdapter) GetRequestHandler() purecef.RequestHandler { return nil } | |||
| 145 | + | ||||
| 146 | + | func (c *clientAdapter) OnProcessMessageReceived(_ purecef.Browser, _ purecef.Frame, _ purecef.ProcessID, _ purecef.ProcessMessage) int32 { | |||
| 147 | + | return 0 | |||
| 148 | + | } | |||