cef2gtk_adapter.go RawHistoryFolder
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) *Cef2gtkAdapter {
38 v := cef2gtk.NewViewWithOptions(cef2gtk.ViewOptions{RenderStackPlan: renderStackPlan})
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// AddSizeObserver registers a positive-size change callback. Register and
127// unregister from the GTK main thread; callbacks are invoked by the bridge from
128// GTK size notifications.
129func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
130 if a == nil || a.destroyed.Load() {
131 return func() {}
132 }
133 a.viewMu.RLock()
134 defer a.viewMu.RUnlock()
135 if a.view == nil {
136 return func() {}
137 }
138 return a.view.AddSizeObserver(fn)
139}
140
141// HasFocus reports whether the effective GTK input target currently has focus.
142// Must be called on the GTK main thread.
143func (a *Cef2gtkAdapter) HasFocus() bool {
144 if a == nil || a.destroyed.Load() {
145 return false
146 }
147 a.viewMu.RLock()
148 defer a.viewMu.RUnlock()
149 if a.view == nil {
150 return false
151 }
152 if a.inputTargetWidget != nil {
153 return a.inputTargetWidget.HasFocus()
154 }
155 return a.view.HasFocus()
156}
157
158// SetCursorFromName applies a named cursor to the bridge widget. Must be called
159// on the GTK main thread.
160func (a *Cef2gtkAdapter) SetCursorFromName(name string) {
161 if a == nil || a.destroyed.Load() {
162 return
163 }
164 a.viewMu.RLock()
165 defer a.viewMu.RUnlock()
166 if a.view == nil {
167 return
168 }
169 a.view.SetCursorFromName(name)
170}
171
172// RefreshObservedSizeOnGTKThread synchronously refreshes the bridge's cached
173// observed size from the current GTK allocation. Must be called on the GTK
174// main thread.
175func (a *Cef2gtkAdapter) RefreshObservedSizeOnGTKThread() (int32, int32) {
176 if a == nil || a.destroyed.Load() {
177 return 1, 1
178 }
179 a.viewMu.RLock()
180 defer a.viewMu.RUnlock()
181 if a.view == nil {
182 return 1, 1
183 }
184 return a.view.RefreshObservedSizeOnGTKThread()
185}
186
187// PrepareOnGTKThread initializes renderer resources owned by purego-cef2gtk.
188// Must be called on the GTK main thread.
189func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {
190 if a == nil || a.destroyed.Load() {
191 return ErrAdapterDestroyed
192 }
193 a.viewMu.RLock()
194 defer a.viewMu.RUnlock()
195 if a.view == nil {
196 return ErrAdapterDestroyed
197 }
198 return a.view.PrepareOnGTKThread()
199}
200
201// RenderHandler returns a CEF render handler that delegates accelerated paint
202// handling to purego-cef2gtk. The returned handler satisfies cef.RenderHandler
203// and uses the bridge's selected accelerated DMABUF import path.
204//
205// hooks may be a zero-value cef2gtk.Hooks if no custom callbacks are needed.
206func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler {
207 if a == nil || a.destroyed.Load() {
208 return nil
209 }
210 a.viewMu.RLock()
211 defer a.viewMu.RUnlock()
212 if a.view == nil {
213 return nil
214 }
215 return a.view.RenderHandler(hooks)
216}
217
218// ConfigureProfiling enables or disables purego-cef2gtk render profiling for this view.
219func (a *Cef2gtkAdapter) ConfigureProfiling(opts cef2gtk.ProfileOptions) error {
220 if a == nil || a.destroyed.Load() {
221 return ErrAdapterDestroyed
222 }
223 a.viewMu.RLock()
224 defer a.viewMu.RUnlock()
225 if a.view == nil {
226 return ErrAdapterDestroyed
227 }
228 return a.view.ConfigureProfiling(opts)
229}
230
231// RecordExternalBeginFrameSent records one externally-driven CEF BeginFrame in
232// the bridge profiler when profiling is enabled. It is a no-op otherwise.
233func (a *Cef2gtkAdapter) RecordExternalBeginFrameSent() {
234 if a == nil || a.destroyed.Load() {
235 return
236 }
237 a.viewMu.RLock()
238 defer a.viewMu.RUnlock()
239 if a.view == nil {
240 return
241 }
242 a.view.RecordExternalBeginFrameSent()
243}
244
245// AttachInput attaches GTK event controllers to the view and forwards input
246// to the given CEF browser host. Must be called on the GTK main thread.
247//
248// opts.Scale configures HiDPI scale for pointer coordinate translation.
249func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {
250 return a.AttachInputToWidget(host, nil, opts)
251}
252
253// AttachInputToWidget attaches GTK event controllers to the specified target
254// widget and forwards input to the given CEF browser host. When targetWidget is
255// nil, it falls back to the bridge render widget. Must be called on the GTK
256// main thread.
257func (a *Cef2gtkAdapter) AttachInputToWidget(host purecef.BrowserHost, targetWidget *gtk.Widget, opts cef2gtk.InputOptions) error {
258 if a == nil || a.destroyed.Load() {
259 return ErrAdapterDestroyed
260 }
261 a.viewMu.Lock()
262 defer a.viewMu.Unlock()
263 if a.view == nil {
264 return ErrAdapterDestroyed
265 }
266 effectiveTarget := targetWidget
267 if effectiveTarget == nil {
268 effectiveTarget = a.view.Widget()
269 }
270 type inputWidgetAttacher interface {
271 AttachInputToWidget(purecef.BrowserHost, *gtk.Widget, cef2gtk.InputOptions) error
272 }
273 var err error
274 if attacher, ok := any(a.view).(inputWidgetAttacher); ok {
275 err = attacher.AttachInputToWidget(host, effectiveTarget, opts)
276 } else {
277 err = a.view.AttachInput(host, opts)
278 }
279 if err != nil {
280 a.inputTargetWidget = nil
281 return err
282 }
283 a.inputTargetWidget = effectiveTarget
284 return nil
285}
286
287// SetInputHost updates the CEF browser host used by the attached input bridge.
288// Must be called on the GTK main thread. Callers must call AttachInput first or
289// this returns an error from the underlying bridge.
290func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {
291 if a == nil || a.destroyed.Load() {
292 return ErrAdapterDestroyed
293 }
294 a.viewMu.RLock()
295 defer a.viewMu.RUnlock()
296 if a.view == nil {
297 return ErrAdapterDestroyed
298 }
299 return a.view.SetInputHost(host)
300}
301
302// DetachInput removes GTK input controllers attached by AttachInput.
303// Must be called on the GTK main thread.
304func (a *Cef2gtkAdapter) DetachInput() error {
305 if a == nil || a.destroyed.Load() {
306 return ErrAdapterDestroyed
307 }
308 a.viewMu.Lock()
309 defer a.viewMu.Unlock()
310 if a.view == nil {
311 return ErrAdapterDestroyed
312 }
313 err := a.view.DetachInput()
314 a.inputTargetWidget = nil
315 return err
316}
317
318// Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics
319// including accelerated paint counts, import/render failures, and events.
320func (a *Cef2gtkAdapter) Diagnostics() cef2gtk.Diagnostics {
321 if a == nil || a.destroyed.Load() {
322 return cef2gtk.Diagnostics{}
323 }
324 a.viewMu.RLock()
325 defer a.viewMu.RUnlock()
326 if a.view == nil {
327 return cef2gtk.Diagnostics{}
328 }
329 return a.view.Diagnostics()
330}
331
332// Destroy releases renderer resources owned by purego-cef2gtk and disconnects GTK
333// signal handlers. Must be called on the GTK main thread. After Destroy,
334// operations return ErrAdapterDestroyed or a zero-value result.
335func (a *Cef2gtkAdapter) Destroy() error {
336 if a == nil {
337 return ErrAdapterDestroyed
338 }
339 a.viewMu.Lock()
340 defer a.viewMu.Unlock()
341 if !a.destroyed.CompareAndSwap(false, true) || a.view == nil {
342 return ErrAdapterDestroyed
343 }
344 err := a.view.Destroy()
345 a.view = nil
346 a.inputTargetWidget = nil
347 a.destroyCnt.Add(1)
348 return err
349}
350
351// IsDestroyed returns true if the adapter has been destroyed.
352func (a *Cef2gtkAdapter) IsDestroyed() bool {
353 return a == nil || a.destroyed.Load()
354}