detached 7297a70

Address migration review feedback

commit
7297a701a0071fb4a11fd59beb89d2ce7975b6b6
author
brice · · 2026-04-29 20:17 UTC
parent
e528b97
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +88 -37
M internal/infrastructure/cef/cef2gtk_adapter.go +18 -17
@@ -2,6 +2,7 @@ package cef
22
33 import (
44 "errors"
5+ "sync/atomic"
56
67 purecef "github.com/bnema/purego-cef/cef"
78 cef2gtk "github.com/bnema/purego-cef2gtk"
@@ -24,7 +25,7 @@ var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
2425 // controller implementation — those belong to purego-cef2gtk.
2526 type Cef2gtkAdapter struct {
2627 view *cef2gtk.View
27- destroyed bool
28+ destroyed atomic.Bool
2829 }
2930
3031 // NewCef2gtkAdapter creates a GtkGLArea-backed accelerated CEF view and wraps
@@ -40,7 +41,7 @@ func NewCef2gtkAdapter() *Cef2gtkAdapter {
4041 // Widget returns the native GTK widget for embedding into containers.
4142 // The returned pointer must be used from the GTK main thread for packing.
4243 func (a *Cef2gtkAdapter) Widget() *gtk.Widget {
43- if a == nil || a.view == nil {
44+ if a == nil || a.destroyed.Load() || a.view == nil {
4445 return nil
4546 }
4647 return a.view.Widget()
@@ -49,7 +50,7 @@ func (a *Cef2gtkAdapter) Widget() *gtk.Widget {
4950 // GLArea returns the underlying GtkGLArea for signal connections (resize,
5051 // tick, etc.). Signal connection/disconnection must happen on the GTK thread.
5152 func (a *Cef2gtkAdapter) GLArea() *gtk.GLArea {
52- if a == nil || a.view == nil {
53+ if a == nil || a.destroyed.Load() || a.view == nil {
5354 return nil
5455 }
5556 return a.view.GLArea()
@@ -68,7 +69,7 @@ func (a *Cef2gtkAdapter) NativeWidget() uintptr {
6869 // Size returns the bridge's last observed positive widget size, or 1x1 before
6970 // the GTK widget has been allocated. It is safe to call off the GTK thread.
7071 func (a *Cef2gtkAdapter) Size() (int32, int32) {
71- if a == nil || a.view == nil {
72+ if a == nil || a.destroyed.Load() || a.view == nil {
7273 return 1, 1
7374 }
7475 return a.view.Size()
@@ -77,7 +78,7 @@ func (a *Cef2gtkAdapter) Size() (int32, int32) {
7778 // DeviceScaleFactor returns the bridge's last observed GTK scale factor, or 1
7879 // before the widget has reported a scale. It is safe to call off the GTK thread.
7980 func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 {
80- if a == nil || a.view == nil {
81+ if a == nil || a.destroyed.Load() || a.view == nil {
8182 return 1
8283 }
8384 return a.view.DeviceScaleFactor()
@@ -87,7 +88,7 @@ func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 {
8788 // unregister from the GTK main thread; callbacks are invoked by the bridge from
8889 // GTK size notifications.
8990 func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
90- if a == nil || a.view == nil {
91+ if a == nil || a.destroyed.Load() || a.view == nil {
9192 return func() {}
9293 }
9394 return a.view.AddSizeObserver(fn)
@@ -96,7 +97,7 @@ func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
9697 // HasFocus reports whether the bridge widget currently has GTK focus. Must be
9798 // called on the GTK main thread.
9899 func (a *Cef2gtkAdapter) HasFocus() bool {
99- if a == nil || a.view == nil {
100+ if a == nil || a.destroyed.Load() || a.view == nil {
100101 return false
101102 }
102103 return a.view.HasFocus()
@@ -105,7 +106,7 @@ func (a *Cef2gtkAdapter) HasFocus() bool {
105106 // SetCursorFromName applies a named cursor to the bridge widget. Must be called
106107 // on the GTK main thread.
107108 func (a *Cef2gtkAdapter) SetCursorFromName(name string) {
108- if a == nil || a.view == nil {
109+ if a == nil || a.destroyed.Load() || a.view == nil {
109110 return
110111 }
111112 a.view.SetCursorFromName(name)
@@ -114,7 +115,7 @@ func (a *Cef2gtkAdapter) SetCursorFromName(name string) {
114115 // PrepareOnGTKThread initializes GL/EGL resources owned by purego-cef2gtk.
115116 // Must be called on the GTK main thread.
116117 func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {
117- if a == nil || a.view == nil {
118+ if a == nil || a.destroyed.Load() || a.view == nil {
118119 return ErrAdapterDestroyed
119120 }
120121 return a.view.PrepareOnGTKThread()
@@ -126,7 +127,7 @@ func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {
126127 //
127128 // hooks may be a zero-value cef2gtk.Hooks if no custom callbacks are needed.
128129 func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler {
129- if a == nil || a.view == nil {
130+ if a == nil || a.destroyed.Load() || a.view == nil {
130131 return nil
131132 }
132133 return a.view.RenderHandler(hooks)
@@ -137,7 +138,7 @@ func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandle
137138 //
138139 // opts.Scale configures HiDPI scale for pointer coordinate translation.
139140 func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {
140- if a == nil || a.view == nil {
141+ if a == nil || a.destroyed.Load() || a.view == nil {
141142 return ErrAdapterDestroyed
142143 }
143144 return a.view.AttachInput(host, opts)
@@ -147,7 +148,7 @@ func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.Inpu
147148 // Must be called on the GTK main thread. Callers must call AttachInput first or
148149 // this returns an error from the underlying bridge.
149150 func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {
150- if a == nil || a.view == nil {
151+ if a == nil || a.destroyed.Load() || a.view == nil {
151152 return ErrAdapterDestroyed
152153 }
153154 return a.view.SetInputHost(host)
@@ -156,7 +157,7 @@ func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {
156157 // DetachInput removes GTK input controllers attached by AttachInput.
157158 // Must be called on the GTK main thread.
158159 func (a *Cef2gtkAdapter) DetachInput() error {
159- if a == nil || a.view == nil {
160+ if a == nil || a.destroyed.Load() || a.view == nil {
160161 return ErrAdapterDestroyed
161162 }
162163 return a.view.DetachInput()
@@ -165,7 +166,7 @@ func (a *Cef2gtkAdapter) DetachInput() error {
165166 // Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics
166167 // including accelerated paint counts, import/render failures, and events.
167168 func (a *Cef2gtkAdapter) Diagnostics() cef2gtk.Diagnostics {
168- if a == nil || a.view == nil {
169+ if a == nil || a.destroyed.Load() || a.view == nil {
169170 return cef2gtk.Diagnostics{}
170171 }
171172 return a.view.Diagnostics()
@@ -181,16 +182,16 @@ func (a *Cef2gtkAdapter) Destroy() error {
181182 return ErrAdapterDestroyed
182183 }
183184 if a.view == nil {
184- a.destroyed = true
185+ a.destroyed.Store(true)
185186 return ErrAdapterDestroyed
186187 }
187188 err := a.view.Destroy()
188189 a.view = nil
189- a.destroyed = true
190+ a.destroyed.Store(true)
190191 return err
191192 }
192193
193194 // IsDestroyed returns true if the adapter has been destroyed.
194195 func (a *Cef2gtkAdapter) IsDestroyed() bool {
195- return a == nil || a.destroyed
196+ return a == nil || a.destroyed.Load()
196197 }
@@ -2,6 +2,7 @@ package cef
2 2
3 import (3 import (
4 "errors"4 "errors"
5+ "sync/atomic"
5 6
6 purecef "github.com/bnema/purego-cef/cef"7 purecef "github.com/bnema/purego-cef/cef"
7 cef2gtk "github.com/bnema/purego-cef2gtk"8 cef2gtk "github.com/bnema/purego-cef2gtk"
@@ -24,7 +25,7 @@ var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
24 // controller implementation — those belong to purego-cef2gtk.25 // controller implementation — those belong to purego-cef2gtk.
25 type Cef2gtkAdapter struct {26 type Cef2gtkAdapter struct {
26 view *cef2gtk.View27 view *cef2gtk.View
27- destroyed bool28+ destroyed atomic.Bool
28 }29 }
29 30
30 // NewCef2gtkAdapter creates a GtkGLArea-backed accelerated CEF view and wraps31 // NewCef2gtkAdapter creates a GtkGLArea-backed accelerated CEF view and wraps
@@ -40,7 +41,7 @@ func NewCef2gtkAdapter() *Cef2gtkAdapter {
40 // Widget returns the native GTK widget for embedding into containers.41 // Widget returns the native GTK widget for embedding into containers.
41 // The returned pointer must be used from the GTK main thread for packing.42 // The returned pointer must be used from the GTK main thread for packing.
42 func (a *Cef2gtkAdapter) Widget() *gtk.Widget {43 func (a *Cef2gtkAdapter) Widget() *gtk.Widget {
43- if a == nil || a.view == nil {44+ if a == nil || a.destroyed.Load() || a.view == nil {
44 return nil45 return nil
45 }46 }
46 return a.view.Widget()47 return a.view.Widget()
@@ -49,7 +50,7 @@ func (a *Cef2gtkAdapter) Widget() *gtk.Widget {
49 // GLArea returns the underlying GtkGLArea for signal connections (resize,50 // GLArea returns the underlying GtkGLArea for signal connections (resize,
50 // tick, etc.). Signal connection/disconnection must happen on the GTK thread.51 // tick, etc.). Signal connection/disconnection must happen on the GTK thread.
51 func (a *Cef2gtkAdapter) GLArea() *gtk.GLArea {52 func (a *Cef2gtkAdapter) GLArea() *gtk.GLArea {
52- if a == nil || a.view == nil {53+ if a == nil || a.destroyed.Load() || a.view == nil {
53 return nil54 return nil
54 }55 }
55 return a.view.GLArea()56 return a.view.GLArea()
@@ -68,7 +69,7 @@ func (a *Cef2gtkAdapter) NativeWidget() uintptr {
68 // Size returns the bridge's last observed positive widget size, or 1x1 before69 // Size returns the bridge's last observed positive widget size, or 1x1 before
69 // the GTK widget has been allocated. It is safe to call off the GTK thread.70 // the GTK widget has been allocated. It is safe to call off the GTK thread.
70 func (a *Cef2gtkAdapter) Size() (int32, int32) {71 func (a *Cef2gtkAdapter) Size() (int32, int32) {
71- if a == nil || a.view == nil {72+ if a == nil || a.destroyed.Load() || a.view == nil {
72 return 1, 173 return 1, 1
73 }74 }
74 return a.view.Size()75 return a.view.Size()
@@ -77,7 +78,7 @@ func (a *Cef2gtkAdapter) Size() (int32, int32) {
77 // DeviceScaleFactor returns the bridge's last observed GTK scale factor, or 178 // DeviceScaleFactor returns the bridge's last observed GTK scale factor, or 1
78 // before the widget has reported a scale. It is safe to call off the GTK thread.79 // before the widget has reported a scale. It is safe to call off the GTK thread.
79 func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 {80 func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 {
80- if a == nil || a.view == nil {81+ if a == nil || a.destroyed.Load() || a.view == nil {
81 return 182 return 1
82 }83 }
83 return a.view.DeviceScaleFactor()84 return a.view.DeviceScaleFactor()
@@ -87,7 +88,7 @@ func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 {
87 // unregister from the GTK main thread; callbacks are invoked by the bridge from88 // unregister from the GTK main thread; callbacks are invoked by the bridge from
88 // GTK size notifications.89 // GTK size notifications.
89 func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {90 func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
90- if a == nil || a.view == nil {91+ if a == nil || a.destroyed.Load() || a.view == nil {
91 return func() {}92 return func() {}
92 }93 }
93 return a.view.AddSizeObserver(fn)94 return a.view.AddSizeObserver(fn)
@@ -96,7 +97,7 @@ func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
96 // HasFocus reports whether the bridge widget currently has GTK focus. Must be97 // HasFocus reports whether the bridge widget currently has GTK focus. Must be
97 // called on the GTK main thread.98 // called on the GTK main thread.
98 func (a *Cef2gtkAdapter) HasFocus() bool {99 func (a *Cef2gtkAdapter) HasFocus() bool {
99- if a == nil || a.view == nil {100+ if a == nil || a.destroyed.Load() || a.view == nil {
100 return false101 return false
101 }102 }
102 return a.view.HasFocus()103 return a.view.HasFocus()
@@ -105,7 +106,7 @@ func (a *Cef2gtkAdapter) HasFocus() bool {
105 // SetCursorFromName applies a named cursor to the bridge widget. Must be called106 // SetCursorFromName applies a named cursor to the bridge widget. Must be called
106 // on the GTK main thread.107 // on the GTK main thread.
107 func (a *Cef2gtkAdapter) SetCursorFromName(name string) {108 func (a *Cef2gtkAdapter) SetCursorFromName(name string) {
108- if a == nil || a.view == nil {109+ if a == nil || a.destroyed.Load() || a.view == nil {
109 return110 return
110 }111 }
111 a.view.SetCursorFromName(name)112 a.view.SetCursorFromName(name)
@@ -114,7 +115,7 @@ func (a *Cef2gtkAdapter) SetCursorFromName(name string) {
114 // PrepareOnGTKThread initializes GL/EGL resources owned by purego-cef2gtk.115 // PrepareOnGTKThread initializes GL/EGL resources owned by purego-cef2gtk.
115 // Must be called on the GTK main thread.116 // Must be called on the GTK main thread.
116 func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {117 func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {
117- if a == nil || a.view == nil {118+ if a == nil || a.destroyed.Load() || a.view == nil {
118 return ErrAdapterDestroyed119 return ErrAdapterDestroyed
119 }120 }
120 return a.view.PrepareOnGTKThread()121 return a.view.PrepareOnGTKThread()
@@ -126,7 +127,7 @@ func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {
126 //127 //
127 // hooks may be a zero-value cef2gtk.Hooks if no custom callbacks are needed.128 // hooks may be a zero-value cef2gtk.Hooks if no custom callbacks are needed.
128 func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler {129 func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler {
129- if a == nil || a.view == nil {130+ if a == nil || a.destroyed.Load() || a.view == nil {
130 return nil131 return nil
131 }132 }
132 return a.view.RenderHandler(hooks)133 return a.view.RenderHandler(hooks)
@@ -137,7 +138,7 @@ func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandle
137 //138 //
138 // opts.Scale configures HiDPI scale for pointer coordinate translation.139 // opts.Scale configures HiDPI scale for pointer coordinate translation.
139 func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {140 func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {
140- if a == nil || a.view == nil {141+ if a == nil || a.destroyed.Load() || a.view == nil {
141 return ErrAdapterDestroyed142 return ErrAdapterDestroyed
142 }143 }
143 return a.view.AttachInput(host, opts)144 return a.view.AttachInput(host, opts)
@@ -147,7 +148,7 @@ func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.Inpu
147 // Must be called on the GTK main thread. Callers must call AttachInput first or148 // Must be called on the GTK main thread. Callers must call AttachInput first or
148 // this returns an error from the underlying bridge.149 // this returns an error from the underlying bridge.
149 func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {150 func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {
150- if a == nil || a.view == nil {151+ if a == nil || a.destroyed.Load() || a.view == nil {
151 return ErrAdapterDestroyed152 return ErrAdapterDestroyed
152 }153 }
153 return a.view.SetInputHost(host)154 return a.view.SetInputHost(host)
@@ -156,7 +157,7 @@ func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {
156 // DetachInput removes GTK input controllers attached by AttachInput.157 // DetachInput removes GTK input controllers attached by AttachInput.
157 // Must be called on the GTK main thread.158 // Must be called on the GTK main thread.
158 func (a *Cef2gtkAdapter) DetachInput() error {159 func (a *Cef2gtkAdapter) DetachInput() error {
159- if a == nil || a.view == nil {160+ if a == nil || a.destroyed.Load() || a.view == nil {
160 return ErrAdapterDestroyed161 return ErrAdapterDestroyed
161 }162 }
162 return a.view.DetachInput()163 return a.view.DetachInput()
@@ -165,7 +166,7 @@ func (a *Cef2gtkAdapter) DetachInput() error {
165 // Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics166 // Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics
166 // including accelerated paint counts, import/render failures, and events.167 // including accelerated paint counts, import/render failures, and events.
167 func (a *Cef2gtkAdapter) Diagnostics() cef2gtk.Diagnostics {168 func (a *Cef2gtkAdapter) Diagnostics() cef2gtk.Diagnostics {
168- if a == nil || a.view == nil {169+ if a == nil || a.destroyed.Load() || a.view == nil {
169 return cef2gtk.Diagnostics{}170 return cef2gtk.Diagnostics{}
170 }171 }
171 return a.view.Diagnostics()172 return a.view.Diagnostics()
@@ -181,16 +182,16 @@ func (a *Cef2gtkAdapter) Destroy() error {
181 return ErrAdapterDestroyed182 return ErrAdapterDestroyed
182 }183 }
183 if a.view == nil {184 if a.view == nil {
184- a.destroyed = true185+ a.destroyed.Store(true)
185 return ErrAdapterDestroyed186 return ErrAdapterDestroyed
186 }187 }
187 err := a.view.Destroy()188 err := a.view.Destroy()
188 a.view = nil189 a.view = nil
189- a.destroyed = true190+ a.destroyed.Store(true)
190 return err191 return err
191 }192 }
192 193
193 // IsDestroyed returns true if the adapter has been destroyed.194 // IsDestroyed returns true if the adapter has been destroyed.
194 func (a *Cef2gtkAdapter) IsDestroyed() bool {195 func (a *Cef2gtkAdapter) IsDestroyed() bool {
195- return a == nil || a.destroyed196+ return a == nil || a.destroyed.Load()
196 }197 }
M internal/infrastructure/cef/cef2gtk_boundary_test.go +39 -6
@@ -1,14 +1,18 @@
11 package cef
22
33 import (
4+ "go/parser"
5+ "go/token"
46 "os"
57 "path/filepath"
8+ "runtime"
9+ "strconv"
610 "strings"
711 "testing"
812 )
913
1014 func TestCEF2GTKImportStaysInInfrastructureCEF(t *testing.T) {
11- root := filepath.Clean("../../..")
15+ root := repoRoot(t)
1216 if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
1317 if err != nil {
1418 return err
@@ -23,11 +27,7 @@ func TestCEF2GTKImportStaysInInfrastructureCEF(t *testing.T) {
2327 if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
2428 return nil
2529 }
26- body, err := os.ReadFile(path)
27- if err != nil {
28- return err
29- }
30- if !strings.Contains(string(body), "github.com/bnema/purego-cef2gtk") {
30+ if !importsCEF2GTK(path) {
3131 return nil
3232 }
3333 rel, err := filepath.Rel(root, path)
@@ -43,3 +43,36 @@ func TestCEF2GTKImportStaysInInfrastructureCEF(t *testing.T) {
4343 t.Fatalf("scan imports: %v", err)
4444 }
4545 }
46+
47+func importsCEF2GTK(path string) bool {
48+ file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
49+ if err != nil {
50+ return false
51+ }
52+ for _, spec := range file.Imports {
53+ importPath, err := strconv.Unquote(spec.Path.Value)
54+ if err == nil && importPath == "github.com/bnema/purego-cef2gtk" {
55+ return true
56+ }
57+ }
58+ return false
59+}
60+
61+func repoRoot(t *testing.T) string {
62+ t.Helper()
63+ _, filename, _, ok := runtime.Caller(0)
64+ if !ok {
65+ t.Fatal("resolve caller path")
66+ }
67+ dir := filepath.Dir(filename)
68+ for {
69+ if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
70+ return dir
71+ }
72+ parent := filepath.Dir(dir)
73+ if parent == dir {
74+ t.Fatalf("go.mod not found from %s", filename)
75+ }
76+ dir = parent
77+ }
78+}
@@ -1,14 +1,18 @@
1 package cef1 package cef
2 2
3 import (3 import (
4+ "go/parser"
5+ "go/token"
4 "os"6 "os"
5 "path/filepath"7 "path/filepath"
8+ "runtime"
9+ "strconv"
6 "strings"10 "strings"
7 "testing"11 "testing"
8 )12 )
9 13
10 func TestCEF2GTKImportStaysInInfrastructureCEF(t *testing.T) {14 func TestCEF2GTKImportStaysInInfrastructureCEF(t *testing.T) {
11- root := filepath.Clean("../../..")15+ root := repoRoot(t)
12 if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {16 if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
13 if err != nil {17 if err != nil {
14 return err18 return err
@@ -23,11 +27,7 @@ func TestCEF2GTKImportStaysInInfrastructureCEF(t *testing.T) {
23 if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {27 if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
24 return nil28 return nil
25 }29 }
26- body, err := os.ReadFile(path)30+ if !importsCEF2GTK(path) {
27- if err != nil {
28- return err
29- }
30- if !strings.Contains(string(body), "github.com/bnema/purego-cef2gtk") {
31 return nil31 return nil
32 }32 }
33 rel, err := filepath.Rel(root, path)33 rel, err := filepath.Rel(root, path)
@@ -43,3 +43,36 @@ func TestCEF2GTKImportStaysInInfrastructureCEF(t *testing.T) {
43 t.Fatalf("scan imports: %v", err)43 t.Fatalf("scan imports: %v", err)
44 }44 }
45 }45 }
46+
47+func importsCEF2GTK(path string) bool {
48+ file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
49+ if err != nil {
50+ return false
51+ }
52+ for _, spec := range file.Imports {
53+ importPath, err := strconv.Unquote(spec.Path.Value)
54+ if err == nil && importPath == "github.com/bnema/purego-cef2gtk" {
55+ return true
56+ }
57+ }
58+ return false
59+}
60+
61+func repoRoot(t *testing.T) string {
62+ t.Helper()
63+ _, filename, _, ok := runtime.Caller(0)
64+ if !ok {
65+ t.Fatal("resolve caller path")
66+ }
67+ dir := filepath.Dir(filename)
68+ for {
69+ if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
70+ return dir
71+ }
72+ parent := filepath.Dir(dir)
73+ if parent == dir {
74+ t.Fatalf("go.mod not found from %s", filename)
75+ }
76+ dir = parent
77+ }
78+}
M internal/infrastructure/cef/factory.go +22 -10
@@ -74,7 +74,10 @@ func (f *WebViewFactory) setDefaultBackgroundColor(r, g, b, a float64) {
7474 // asynchronously; the returned WebView is usable immediately but navigation
7575 // will fail with errNoBrowser until OnAfterCreated fires.
7676 func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
77- wv := f.newWebView(ctx)
77+ wv, err := f.newWebView(ctx)
78+ if err != nil {
79+ return nil, err
80+ }
7881
7982 // Delegate windowless/shared-texture setup to the GTK bridge.
8083 windowInfo := purecef.NewWindowInfo()
@@ -99,9 +102,12 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
99102 return wv, nil
100103 }
101104
102-func (f *WebViewFactory) newWebView(ctx context.Context) *WebView {
105+func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
103106 id := port.WebViewID(f.nextID.Add(1))
104107 viewBridge := NewCef2gtkAdapter()
108+ if viewBridge == nil {
109+ return nil, fmt.Errorf("create cef2gtk view")
110+ }
105111
106112 wv := &WebView{
107113 id: id,
@@ -117,18 +123,21 @@ func (f *WebViewFactory) newWebView(ctx context.Context) *WebView {
117123 wv.handlers = handlers
118124 wv.findCtrl = newFindController()
119125
120- if viewBridge != nil {
121- wv.runOnGTKSync(func() {
122- if err := viewBridge.PrepareOnGTKThread(); err != nil && ctx != nil {
123- logging.FromContext(ctx).Warn().Err(err).Uint64("webview_id", uint64(id)).Msg("cef: failed to prepare cef2gtk view")
124- }
125- })
126+ var prepareErr error
127+ wv.runOnGTKSync(func() {
128+ prepareErr = viewBridge.PrepareOnGTKThread()
129+ })
130+ if prepareErr != nil {
131+ if ctx != nil {
132+ logging.FromContext(ctx).Error().Err(prepareErr).Uint64("webview_id", uint64(id)).Msg("cef: failed to prepare cef2gtk view")
133+ }
134+ return nil, fmt.Errorf("prepare cef2gtk view: %w", prepareErr)
126135 }
127136
128137 // Build a CEF client backed by our handlerSet.
129138 // Store on WebView to prevent GC collection before CEF AddRef's it.
130139 wv.client = purecef.NewClient(wv.handlers)
131- return wv
140+ return wv, nil
132141 }
133142
134143 func (f *WebViewFactory) configureInitialBrowserCreation(
@@ -292,7 +301,10 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie
292301 return nil, fmt.Errorf("parent webview %d is destroyed", parentID)
293302 }
294303
295- popupWV := f.newWebView(ctx)
304+ popupWV, err := f.newWebView(ctx)
305+ if err != nil {
306+ return nil, err
307+ }
296308 popupWV.markNativePopupCandidate(parent)
297309
298310 windowInfo := purecef.NewWindowInfo()
@@ -74,7 +74,10 @@ func (f *WebViewFactory) setDefaultBackgroundColor(r, g, b, a float64) {
74 // asynchronously; the returned WebView is usable immediately but navigation74 // asynchronously; the returned WebView is usable immediately but navigation
75 // will fail with errNoBrowser until OnAfterCreated fires.75 // will fail with errNoBrowser until OnAfterCreated fires.
76 func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {76 func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
77- wv := f.newWebView(ctx)77+ wv, err := f.newWebView(ctx)
78+ if err != nil {
79+ return nil, err
80+ }
78 81
79 // Delegate windowless/shared-texture setup to the GTK bridge.82 // Delegate windowless/shared-texture setup to the GTK bridge.
80 windowInfo := purecef.NewWindowInfo()83 windowInfo := purecef.NewWindowInfo()
@@ -99,9 +102,12 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
99 return wv, nil102 return wv, nil
100 }103 }
101 104
102-func (f *WebViewFactory) newWebView(ctx context.Context) *WebView {105+func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
103 id := port.WebViewID(f.nextID.Add(1))106 id := port.WebViewID(f.nextID.Add(1))
104 viewBridge := NewCef2gtkAdapter()107 viewBridge := NewCef2gtkAdapter()
108+ if viewBridge == nil {
109+ return nil, fmt.Errorf("create cef2gtk view")
110+ }
105 111
106 wv := &WebView{112 wv := &WebView{
107 id: id,113 id: id,
@@ -117,18 +123,21 @@ func (f *WebViewFactory) newWebView(ctx context.Context) *WebView {
117 wv.handlers = handlers123 wv.handlers = handlers
118 wv.findCtrl = newFindController()124 wv.findCtrl = newFindController()
119 125
120- if viewBridge != nil {126+ var prepareErr error
121- wv.runOnGTKSync(func() {127+ wv.runOnGTKSync(func() {
122- if err := viewBridge.PrepareOnGTKThread(); err != nil && ctx != nil {128+ prepareErr = viewBridge.PrepareOnGTKThread()
123- logging.FromContext(ctx).Warn().Err(err).Uint64("webview_id", uint64(id)).Msg("cef: failed to prepare cef2gtk view")129+ })
124- }130+ if prepareErr != nil {
125- })131+ if ctx != nil {
132+ logging.FromContext(ctx).Error().Err(prepareErr).Uint64("webview_id", uint64(id)).Msg("cef: failed to prepare cef2gtk view")
133+ }
134+ return nil, fmt.Errorf("prepare cef2gtk view: %w", prepareErr)
126 }135 }
127 136
128 // Build a CEF client backed by our handlerSet.137 // Build a CEF client backed by our handlerSet.
129 // Store on WebView to prevent GC collection before CEF AddRef's it.138 // Store on WebView to prevent GC collection before CEF AddRef's it.
130 wv.client = purecef.NewClient(wv.handlers)139 wv.client = purecef.NewClient(wv.handlers)
131- return wv140+ return wv, nil
132 }141 }
133 142
134 func (f *WebViewFactory) configureInitialBrowserCreation(143 func (f *WebViewFactory) configureInitialBrowserCreation(
@@ -292,7 +301,10 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie
292 return nil, fmt.Errorf("parent webview %d is destroyed", parentID)301 return nil, fmt.Errorf("parent webview %d is destroyed", parentID)
293 }302 }
294 303
295- popupWV := f.newWebView(ctx)304+ popupWV, err := f.newWebView(ctx)
305+ if err != nil {
306+ return nil, err
307+ }
296 popupWV.markNativePopupCandidate(parent)308 popupWV.markNativePopupCandidate(parent)
297 309
298 windowInfo := purecef.NewWindowInfo()310 windowInfo := purecef.NewWindowInfo()
M internal/infrastructure/cef/webview_handlers.go +9 -4
@@ -548,7 +548,7 @@ func (h *handlerSet) attachAfterCreatedBrowser(
548548 if wv.viewBridge == nil {
549549 return
550550 }
551- _ = wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{
551+ if err := wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{
552552 Scale: wv.viewBridgeScale(),
553553 OnMiddleClick: func(_, _ float64) bool {
554554 return wv.handleMiddleClickFromBridge()
@@ -559,7 +559,9 @@ func (h *handlerSet) attachAfterCreatedBrowser(
559559 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
560560 }
561561 },
562- })
562+ }); err != nil && wv.ctx != nil {
563+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to attach input to cef2gtk bridge")
564+ }
563565 })
564566 }
565567
@@ -643,8 +645,11 @@ func (h *handlerSet) OnBeforeClose(browser purecef.Browser) {
643645 if h.wv.viewBridge != nil {
644646 wv := h.wv
645647 wv.runOnGTK(func() {
646- if wv.viewBridge != nil {
647- _ = wv.viewBridge.SetInputHost(nil)
648+ if wv.viewBridge == nil {
649+ return
650+ }
651+ if err := wv.viewBridge.SetInputHost(nil); err != nil && wv.ctx != nil {
652+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to clear cef2gtk input host")
648653 }
649654 })
650655 }
@@ -548,7 +548,7 @@ func (h *handlerSet) attachAfterCreatedBrowser(
548 if wv.viewBridge == nil {548 if wv.viewBridge == nil {
549 return549 return
550 }550 }
551- _ = wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{551+ if err := wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{
552 Scale: wv.viewBridgeScale(),552 Scale: wv.viewBridgeScale(),
553 OnMiddleClick: func(_, _ float64) bool {553 OnMiddleClick: func(_, _ float64) bool {
554 return wv.handleMiddleClickFromBridge()554 return wv.handleMiddleClickFromBridge()
@@ -559,7 +559,9 @@ func (h *handlerSet) attachAfterCreatedBrowser(
559 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)559 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
560 }560 }
561 },561 },
562- })562+ }); err != nil && wv.ctx != nil {
563+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to attach input to cef2gtk bridge")
564+ }
563 })565 })
564 }566 }
565 567
@@ -643,8 +645,11 @@ func (h *handlerSet) OnBeforeClose(browser purecef.Browser) {
643 if h.wv.viewBridge != nil {645 if h.wv.viewBridge != nil {
644 wv := h.wv646 wv := h.wv
645 wv.runOnGTK(func() {647 wv.runOnGTK(func() {
646- if wv.viewBridge != nil {648+ if wv.viewBridge == nil {
647- _ = wv.viewBridge.SetInputHost(nil)649+ return
650+ }
651+ if err := wv.viewBridge.SetInputHost(nil); err != nil && wv.ctx != nil {
652+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to clear cef2gtk input host")
648 }653 }
649 })654 })
650 }655 }

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