detached 1ef6a99

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close