v0.32.0 0617278

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, 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}

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