1package cef
2
3import (
4 "errors"
5 "sync"
6 "sync/atomic"
7
8 purecef "github.com/bnema/purego-cef/cef"
9 cef2gtk "github.com/bnema/purego-cef2gtk"
10 "github.com/bnema/puregotk/v4/gtk"
11)
12
13// ErrAdapterDestroyed is returned when an operation is attempted on a destroyed
14// Cef2gtkAdapter.
15var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
16
17// Cef2gtkAdapter is a thin infrastructure adapter around *cef2gtk.View. It
18// keeps purego-cef2gtk imports confined to internal/infrastructure/cef.
19//
20// GTK-thread affinity is a hard constraint: most methods touch GTK/CEF view
21// lifecycle or input and MUST be called from Dumber's GTK scheduling path
22// (e.g. runOnGTK or runOnGTKSync) unless the caller is already executing on
23// the GTK main context. Methods are annotated below.
24//
25// The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input
26// controller implementation — those belong to purego-cef2gtk.
27type Cef2gtkAdapter struct {
28 viewMu sync.RWMutex
29 view *cef2gtk.View
30 inputTargetWidget *gtk.Widget
31 destroyed atomic.Bool
32 destroyCnt atomic.Uint64
33}
34
35// NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin
36// Dumber adapter.
37func NewCef2gtkAdapter(renderStackPlan cef2gtk.RenderStackPlan, applicationScale float64) *Cef2gtkAdapter {
38 v := cef2gtk.NewViewWithOptions(cef2gtk.ViewOptions{RenderStackPlan: renderStackPlan, ScaleMultiplier: applicationScale})
39 if v == nil {
40 return nil
41 }
42 return &Cef2gtkAdapter{view: v}
43}
44
45// Widget returns the native GTK widget for embedding into containers.
46// The returned pointer must be used from the GTK main thread for packing.
47func (a *Cef2gtkAdapter) Widget() *gtk.Widget {
48 if a == nil || a.destroyed.Load() {
49 return nil
50 }
51 a.viewMu.RLock()
52 defer a.viewMu.RUnlock()
53 if a.view == nil {
54 return nil
55 }
56 return a.view.Widget()
57}
58
59// GLArea returns the underlying GtkGLArea when the GLArea backend is active.
60// It returns nil for the GDK DMABUF backend.
61func (a *Cef2gtkAdapter) GLArea() *gtk.GLArea {
62 if a == nil || a.destroyed.Load() {
63 return nil
64 }
65 a.viewMu.RLock()
66 defer a.viewMu.RUnlock()
67 if a.view == nil {
68 return nil
69 }
70 return a.view.GLArea()
71}
72
73// NativeWidget returns the uintptr of the active GTK widget for embedding via
74// port.NativeWidgetProvider. Call on the GTK main thread.
75func (a *Cef2gtkAdapter) NativeWidget() uintptr {
76 widget := a.Widget()
77 if widget == nil {
78 return 0
79 }
80 return widget.GoPointer()
81}
82
83// Size returns the bridge's last observed positive widget size, or 1x1 before
84// the GTK widget has been allocated. It is safe to call off the GTK thread.
85func (a *Cef2gtkAdapter) Size() (int32, int32) {
86 if a == nil || a.destroyed.Load() {
87 return 1, 1
88 }
89 a.viewMu.RLock()
90 defer a.viewMu.RUnlock()
91 if a.view == nil {
92 return 1, 1
93 }
94 return a.view.Size()
95}
96
97// DeviceScaleFactor returns the bridge's last observed GTK scale factor, or 1
98// before the widget has reported a scale. It is safe to call off the GTK thread.
99func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 {
100 if a == nil || a.destroyed.Load() {
101 return 1
102 }
103 a.viewMu.RLock()
104 defer a.viewMu.RUnlock()
105 if a.view == nil {
106 return 1
107 }
108 return a.view.DeviceScaleFactor()
109}
110
111// OSRBackingScaleFactor returns the device backing scale used for CEF OSR
112// coordinates. It returns 1 when purego-cef2gtk is using CEF's normal logical
113// OSR contract.
114func (a *Cef2gtkAdapter) OSRBackingScaleFactor() float64 {
115 if a == nil || a.destroyed.Load() {
116 return 1
117 }
118 a.viewMu.RLock()
119 defer a.viewMu.RUnlock()
120 if a.view == nil {
121 return 1
122 }
123 return cef2gtk.OSRBackingScaleFactorForScale(float64(a.view.DeviceScaleFactor()))
124}
125
126// PageZoomCompensation returns the factor Dumber must multiply user page zoom by
127// before applying it through CEF, so the page's CSS viewport keeps matching the
128// GTK logical view size under the bridge's active OSR geometry contract. It
129// returns 1 for a nil or destroyed adapter and whenever CEF's normal logical OSR
130// contract is in effect.
131func (a *Cef2gtkAdapter) PageZoomCompensation() float64 {
132 if a == nil || a.destroyed.Load() {
133 return 1
134 }
135 a.viewMu.RLock()
136 defer a.viewMu.RUnlock()
137 if a.view == nil {
138 return 1
139 }
140 return a.view.PageZoomCompensation()
141}
142
143// AddSizeObserver registers a positive-size change callback. Register and
144// unregister from the GTK main thread; callbacks are invoked by the bridge from
145// GTK size notifications.
146func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
147 if a == nil || a.destroyed.Load() {
148 return func() {}
149 }
150 a.viewMu.RLock()
151 defer a.viewMu.RUnlock()
152 if a.view == nil {
153 return func() {}
154 }
155 return a.view.AddSizeObserver(fn)
156}
157
158// HasFocus reports whether the effective GTK input target currently has focus.
159// Must be called on the GTK main thread.
160func (a *Cef2gtkAdapter) HasFocus() bool {
161 if a == nil || a.destroyed.Load() {
162 return false
163 }
164 a.viewMu.RLock()
165 defer a.viewMu.RUnlock()
166 if a.view == nil {
167 return false
168 }
169 if a.inputTargetWidget != nil {
170 return a.inputTargetWidget.HasFocus()
171 }
172 return a.view.HasFocus()
173}
174
175// SetCursorFromName applies a named cursor to the bridge widget. Must be called
176// on the GTK main thread.
177func (a *Cef2gtkAdapter) SetCursorFromName(name string) {
178 if a == nil || a.destroyed.Load() {
179 return
180 }
181 a.viewMu.RLock()
182 defer a.viewMu.RUnlock()
183 if a.view == nil {
184 return
185 }
186 a.view.SetCursorFromName(name)
187}
188
189// RefreshObservedSizeOnGTKThread synchronously refreshes the bridge's cached
190// observed size from the current GTK allocation. Must be called on the GTK
191// main thread.
192func (a *Cef2gtkAdapter) RefreshObservedSizeOnGTKThread() (int32, int32) {
193 if a == nil || a.destroyed.Load() {
194 return 1, 1
195 }
196 a.viewMu.RLock()
197 defer a.viewMu.RUnlock()
198 if a.view == nil {
199 return 1, 1
200 }
201 return a.view.RefreshObservedSizeOnGTKThread()
202}
203
204// PrepareOnGTKThread initializes renderer resources owned by purego-cef2gtk.
205// Must be called on the GTK main thread.
206func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {
207 if a == nil || a.destroyed.Load() {
208 return ErrAdapterDestroyed
209 }
210 a.viewMu.RLock()
211 defer a.viewMu.RUnlock()
212 if a.view == nil {
213 return ErrAdapterDestroyed
214 }
215 return a.view.PrepareOnGTKThread()
216}
217
218// RenderHandler returns a CEF render handler that delegates accelerated paint
219// handling to purego-cef2gtk. The returned handler satisfies cef.RenderHandler
220// and uses the bridge's selected accelerated DMABUF import path.
221//
222// hooks may be a zero-value cef2gtk.Hooks if no custom callbacks are needed.
223func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler {
224 if a == nil || a.destroyed.Load() {
225 return nil
226 }
227 a.viewMu.RLock()
228 defer a.viewMu.RUnlock()
229 if a.view == nil {
230 return nil
231 }
232 return a.view.RenderHandler(hooks)
233}
234
235// ConfigureProfiling enables or disables purego-cef2gtk render profiling for this view.
236func (a *Cef2gtkAdapter) ConfigureProfiling(opts cef2gtk.ProfileOptions) error {
237 if a == nil || a.destroyed.Load() {
238 return ErrAdapterDestroyed
239 }
240 a.viewMu.RLock()
241 defer a.viewMu.RUnlock()
242 if a.view == nil {
243 return ErrAdapterDestroyed
244 }
245 return a.view.ConfigureProfiling(opts)
246}
247
248// RecordExternalBeginFrameSent records one externally-driven CEF BeginFrame in
249// the bridge profiler when profiling is enabled. It is a no-op otherwise.
250func (a *Cef2gtkAdapter) RecordExternalBeginFrameSent() {
251 if a == nil || a.destroyed.Load() {
252 return
253 }
254 a.viewMu.RLock()
255 defer a.viewMu.RUnlock()
256 if a.view == nil {
257 return
258 }
259 a.view.RecordExternalBeginFrameSent()
260}
261
262// AttachInput attaches GTK event controllers to the view and forwards input
263// to the given CEF browser host. Must be called on the GTK main thread.
264//
265// opts.Scale configures HiDPI scale for pointer coordinate translation.
266func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {
267 return a.AttachInputToWidget(host, nil, opts)
268}
269
270// AttachInputToWidget attaches GTK event controllers to the specified target
271// widget and forwards input to the given CEF browser host. When targetWidget is
272// nil, it falls back to the bridge render widget. Must be called on the GTK
273// main thread.
274func (a *Cef2gtkAdapter) AttachInputToWidget(host purecef.BrowserHost, targetWidget *gtk.Widget, opts cef2gtk.InputOptions) error {
275 if a == nil || a.destroyed.Load() {
276 return ErrAdapterDestroyed
277 }
278 a.viewMu.Lock()
279 defer a.viewMu.Unlock()
280 if a.view == nil {
281 return ErrAdapterDestroyed
282 }
283 effectiveTarget := targetWidget
284 if effectiveTarget == nil {
285 effectiveTarget = a.view.Widget()
286 }
287 type inputWidgetAttacher interface {
288 AttachInputToWidget(purecef.BrowserHost, *gtk.Widget, cef2gtk.InputOptions) error
289 }
290 var err error
291 if attacher, ok := any(a.view).(inputWidgetAttacher); ok {
292 err = attacher.AttachInputToWidget(host, effectiveTarget, opts)
293 } else {
294 err = a.view.AttachInput(host, opts)
295 }
296 if err != nil {
297 a.inputTargetWidget = nil
298 return err
299 }
300 a.inputTargetWidget = effectiveTarget
301 return nil
302}
303
304// SetInputHost updates the CEF browser host used by the attached input bridge.
305// Must be called on the GTK main thread. Callers must call AttachInput first or
306// this returns an error from the underlying bridge.
307func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {
308 if a == nil || a.destroyed.Load() {
309 return ErrAdapterDestroyed
310 }
311 a.viewMu.RLock()
312 defer a.viewMu.RUnlock()
313 if a.view == nil {
314 return ErrAdapterDestroyed
315 }
316 return a.view.SetInputHost(host)
317}
318
319// InjectScroll feeds a synthetic scroll impulse (keyboard scrolling) into
320// the animated wheel engine from any thread. It reports whether the
321// impulse was accepted. Destroyed or absent views report false.
322func (a *Cef2gtkAdapter) InjectScroll(dx, dy float64) bool {
323 if a == nil || a.destroyed.Load() {
324 return false
325 }
326 // Snapshot under lock, invoke outside it: View.InjectScroll hops to
327 // the GTK thread when called off-thread, and GTK teardown waits on
328 // this same lock — holding it across the call would freeze Destroy.
329 a.viewMu.RLock()
330 view := a.view
331 a.viewMu.RUnlock()
332 if view == nil {
333 return false
334 }
335 return view.InjectScroll(dx, dy)
336}
337
338// InvalidateScroll immediately retires animated scroll motion from any
339// thread, returning the retired epoch. Pass that epoch to
340// CancelScrollEpoch for GTK cleanup. Destroyed or absent views report zero.
341func (a *Cef2gtkAdapter) InvalidateScroll() uint64 {
342 if a == nil || a.destroyed.Load() {
343 return 0
344 }
345 a.viewMu.RLock()
346 defer a.viewMu.RUnlock()
347 if a.view == nil {
348 return 0
349 }
350 return a.view.InvalidateScroll()
351}
352
353// CancelScroll synchronously invalidates and cleans up scroll motion.
354// Must be called on the GTK main thread.
355func (a *Cef2gtkAdapter) CancelScroll() {
356 if a == nil || a.destroyed.Load() {
357 return
358 }
359 a.viewMu.RLock()
360 defer a.viewMu.RUnlock()
361 if a.view == nil {
362 return
363 }
364 a.view.CancelScroll()
365}
366
367// CancelScrollEpoch performs GTK-only cleanup for a retired epoch. It never
368// clears a newer session and reports whether cleanup ran. Must be called on
369// the GTK main thread.
370func (a *Cef2gtkAdapter) CancelScrollEpoch(epoch uint64) bool {
371 if a == nil || a.destroyed.Load() {
372 return false
373 }
374 a.viewMu.RLock()
375 defer a.viewMu.RUnlock()
376 if a.view == nil {
377 return false
378 }
379 return a.view.CancelScrollEpoch(epoch)
380}
381
382// DetachInput removes GTK input controllers attached by AttachInput.
383// Must be called on the GTK main thread.
384func (a *Cef2gtkAdapter) DetachInput() error {
385 if a == nil || a.destroyed.Load() {
386 return ErrAdapterDestroyed
387 }
388 a.viewMu.Lock()
389 defer a.viewMu.Unlock()
390 if a.view == nil {
391 return ErrAdapterDestroyed
392 }
393 err := a.view.DetachInput()
394 a.inputTargetWidget = nil
395 return err
396}
397
398// Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics
399// including accelerated paint counts, import/render failures, and events.
400func (a *Cef2gtkAdapter) Diagnostics() cef2gtk.Diagnostics {
401 if a == nil || a.destroyed.Load() {
402 return cef2gtk.Diagnostics{}
403 }
404 a.viewMu.RLock()
405 defer a.viewMu.RUnlock()
406 if a.view == nil {
407 return cef2gtk.Diagnostics{}
408 }
409 return a.view.Diagnostics()
410}
411
412// Destroy releases renderer resources owned by purego-cef2gtk and disconnects GTK
413// signal handlers. Must be called on the GTK main thread. After Destroy,
414// operations return ErrAdapterDestroyed or a zero-value result.
415func (a *Cef2gtkAdapter) Destroy() error {
416 if a == nil {
417 return ErrAdapterDestroyed
418 }
419 a.viewMu.Lock()
420 defer a.viewMu.Unlock()
421 if !a.destroyed.CompareAndSwap(false, true) || a.view == nil {
422 return ErrAdapterDestroyed
423 }
424 err := a.view.Destroy()
425 a.view = nil
426 a.inputTargetWidget = nil
427 a.destroyCnt.Add(1)
428 return err
429}
430
431// IsDestroyed returns true if the adapter has been destroyed.
432func (a *Cef2gtkAdapter) IsDestroyed() bool {
433 return a == nil || a.destroyed.Load()
434}