detached e5061b6

fix(cef): use released size observer

commit
e5061b62111cd696977df15faccb2075a2cb5ea7
author
brice · · 2026-05-24 13:18 UTC
parent
c48b63f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +279 -34
M go.mod +1 -1
@@ -7,7 +7,7 @@ require (
77 github.com/andybalholm/brotli v1.2.1
88 github.com/bnema/purego v0.11.0-bnema.2
99 github.com/bnema/purego-cef v0.13.0
10- github.com/bnema/purego-cef2gtk v0.5.2
10+ github.com/bnema/purego-cef2gtk v0.5.3
1111 github.com/bnema/purego-pipewire v0.1.3
1212 github.com/bnema/purego-sqlite v0.1.2
1313 github.com/bnema/puregotk v0.5.2
@@ -7,7 +7,7 @@ require (
7 github.com/andybalholm/brotli v1.2.17 github.com/andybalholm/brotli v1.2.1
8 github.com/bnema/purego v0.11.0-bnema.28 github.com/bnema/purego v0.11.0-bnema.2
9 github.com/bnema/purego-cef v0.13.09 github.com/bnema/purego-cef v0.13.0
10- github.com/bnema/purego-cef2gtk v0.5.210+ github.com/bnema/purego-cef2gtk v0.5.3
11 github.com/bnema/purego-pipewire v0.1.311 github.com/bnema/purego-pipewire v0.1.3
12 github.com/bnema/purego-sqlite v0.1.212 github.com/bnema/purego-sqlite v0.1.2
13 github.com/bnema/puregotk v0.5.213 github.com/bnema/puregotk v0.5.2
M go.sum +2 -2 generated file: collapsed
M internal/infrastructure/cef/factory.go +98 -31
@@ -177,18 +177,7 @@ func (f *WebViewFactory) configureInitialBrowserCreation(
177177 // the one-shot initial resize path; once that path has been consumed,
178178 // later size events must go through normal viewport sync even if popup
179179 // attachment keeps pendingCreate alive a little longer.
180- if wv.shouldStartBrowserCreateFromSizeObserver() {
181- if err := wv.viewBridge.PrepareOnGTKThread(); err != nil {
182- logging.FromContext(ctx).Warn().
183- Err(err).
184- Uint64("webview_id", uint64(wv.id)).
185- Int32("resize_width", w).
186- Int32("resize_height", h).
187- Msg("cef: cef2gtk view not ready after resize, will retry")
188- return
189- }
190- wv.markInitialBrowserCreateResizeHandled()
191- onFirstResize(w, h)
180+ if f.handleInitialBrowserCreateSizeObserver(ctx, wv, onFirstResize, wv.viewBridge.PrepareOnGTKThread, w, h) {
192181 return
193182 }
194183
@@ -203,6 +192,49 @@ func (f *WebViewFactory) configureInitialBrowserCreation(
203192 })
204193 }
205194
195+func (f *WebViewFactory) handleInitialBrowserCreateSizeObserver(
196+ ctx context.Context,
197+ wv *WebView,
198+ onFirstResize func(w, h int32),
199+ prepareOnGTK func() error,
200+ w, h int32,
201+) bool {
202+ if wv == nil || !wv.shouldStartBrowserCreateFromSizeObserver() {
203+ return false
204+ }
205+ if !initialBrowserCreateSizeReadyFromObserver(w, h) {
206+ if wv.awaitsNativePopupAttachment() && onFirstResize != nil {
207+ onFirstResize(w, h)
208+ }
209+ logging.FromContext(ctx).Debug().
210+ Uint64("webview_id", uint64(wv.id)).
211+ Int32("resize_width", w).
212+ Int32("resize_height", h).
213+ Msg("cef: ignoring bootstrap size observer event before real view size is ready")
214+ return true
215+ }
216+ if prepareOnGTK != nil {
217+ if err := prepareOnGTK(); err != nil {
218+ logging.FromContext(ctx).Warn().
219+ Err(err).
220+ Uint64("webview_id", uint64(wv.id)).
221+ Int32("resize_width", w).
222+ Int32("resize_height", h).
223+ Msg("cef: cef2gtk view not ready after resize, will retry")
224+ return true
225+ }
226+ }
227+ wv.markInitialBrowserCreateResizeHandled()
228+ if onFirstResize != nil {
229+ onFirstResize(w, h)
230+ }
231+ return true
232+}
233+
234+func initialBrowserCreateSizeReadyFromObserver(width, height int32) bool {
235+ return observedViewportSizeReady(width, height, width, height)
236+}
237+
206238 func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebView, w, h int32) {
207239 log := logging.FromContext(ctx)
208240 pc := wv.takePendingCreate()
@@ -329,26 +361,61 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie
329361 }
330362
331363 f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) {
332- log := logging.FromContext(ctx)
333- if popupWV.awaitsNativePopupAttachment() {
334- log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, awaiting native popup attach")
335- popupWV.scheduleNativePopupFallback(nativePopupAttachFallbackDelay, func() {
336- if !popupWV.startNativePopupFallback() {
337- return
338- }
339- logging.FromContext(ctx).Warn().
340- Uint64("webview_id", uint64(popupWV.id)).
341- Msg("cef: native popup attach timed out, creating popup browser directly")
342- f.postPendingBrowserCreate(ctx, popupWV, w, h)
343- })
344- return
345- }
364+ f.handlePopupShellInitialResize(ctx, popupWV, func(w, h int32) {
365+ f.postPendingBrowserCreate(ctx, popupWV, w, h)
366+ }, w, h)
367+ })
368+ return popupWV, nil
369+}
346370
347- if !popupWV.preparePopupShellDirectBrowserCreation() {
348- return
371+func (f *WebViewFactory) handlePopupShellInitialResize(
372+ ctx context.Context,
373+ popupWV *WebView,
374+ postPendingCreate func(w, h int32),
375+ w, h int32,
376+) {
377+ if popupWV == nil {
378+ return
379+ }
380+ log := logging.FromContext(ctx)
381+ if popupWV.awaitsNativePopupAttachment() {
382+ log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell initial size observed, awaiting native popup attach")
383+ f.schedulePopupShellNativeFallback(ctx, popupWV)
384+ return
385+ }
386+ if popupWV.awaitsBrowserCreateFromNativePopupFallback() {
387+ log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell size ready after fallback, creating browser directly")
388+ if postPendingCreate != nil {
389+ postPendingCreate(w, h)
349390 }
350- log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, creating browser directly")
351- f.postPendingBrowserCreate(ctx, popupWV, w, h)
391+ return
392+ }
393+ if !popupWV.preparePopupShellDirectBrowserCreation() {
394+ return
395+ }
396+ log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, creating browser directly")
397+ if postPendingCreate != nil {
398+ postPendingCreate(w, h)
399+ }
400+}
401+
402+func (f *WebViewFactory) schedulePopupShellNativeFallback(ctx context.Context, popupWV *WebView) {
403+ if popupWV == nil {
404+ return
405+ }
406+ popupWV.scheduleNativePopupFallback(nativePopupAttachFallbackDelay, func() {
407+ popupWV.runOnGTK(func() {
408+ if !popupWV.startNativePopupFallback() {
409+ return
410+ }
411+ logging.FromContext(ctx).Warn().
412+ Uint64("webview_id", uint64(popupWV.id)).
413+ Msg("cef: native popup attach timed out, starting direct popup fallback")
414+ synced := popupWV.syncViewportNowOnGTK(ctx, "native-popup-fallback-timeout")
415+ logging.FromContext(ctx).Debug().
416+ Uint64("webview_id", uint64(popupWV.id)).
417+ Bool("browser_ready", synced).
418+ Msg("cef: popup fallback viewport sync requested")
419+ })
352420 })
353- return popupWV, nil
354421 }
@@ -177,18 +177,7 @@ func (f *WebViewFactory) configureInitialBrowserCreation(
177 // the one-shot initial resize path; once that path has been consumed,177 // the one-shot initial resize path; once that path has been consumed,
178 // later size events must go through normal viewport sync even if popup178 // later size events must go through normal viewport sync even if popup
179 // attachment keeps pendingCreate alive a little longer.179 // attachment keeps pendingCreate alive a little longer.
180- if wv.shouldStartBrowserCreateFromSizeObserver() {180+ if f.handleInitialBrowserCreateSizeObserver(ctx, wv, onFirstResize, wv.viewBridge.PrepareOnGTKThread, w, h) {
181- if err := wv.viewBridge.PrepareOnGTKThread(); err != nil {
182- logging.FromContext(ctx).Warn().
183- Err(err).
184- Uint64("webview_id", uint64(wv.id)).
185- Int32("resize_width", w).
186- Int32("resize_height", h).
187- Msg("cef: cef2gtk view not ready after resize, will retry")
188- return
189- }
190- wv.markInitialBrowserCreateResizeHandled()
191- onFirstResize(w, h)
192 return181 return
193 }182 }
194 183
@@ -203,6 +192,49 @@ func (f *WebViewFactory) configureInitialBrowserCreation(
203 })192 })
204 }193 }
205 194
195+func (f *WebViewFactory) handleInitialBrowserCreateSizeObserver(
196+ ctx context.Context,
197+ wv *WebView,
198+ onFirstResize func(w, h int32),
199+ prepareOnGTK func() error,
200+ w, h int32,
201+) bool {
202+ if wv == nil || !wv.shouldStartBrowserCreateFromSizeObserver() {
203+ return false
204+ }
205+ if !initialBrowserCreateSizeReadyFromObserver(w, h) {
206+ if wv.awaitsNativePopupAttachment() && onFirstResize != nil {
207+ onFirstResize(w, h)
208+ }
209+ logging.FromContext(ctx).Debug().
210+ Uint64("webview_id", uint64(wv.id)).
211+ Int32("resize_width", w).
212+ Int32("resize_height", h).
213+ Msg("cef: ignoring bootstrap size observer event before real view size is ready")
214+ return true
215+ }
216+ if prepareOnGTK != nil {
217+ if err := prepareOnGTK(); err != nil {
218+ logging.FromContext(ctx).Warn().
219+ Err(err).
220+ Uint64("webview_id", uint64(wv.id)).
221+ Int32("resize_width", w).
222+ Int32("resize_height", h).
223+ Msg("cef: cef2gtk view not ready after resize, will retry")
224+ return true
225+ }
226+ }
227+ wv.markInitialBrowserCreateResizeHandled()
228+ if onFirstResize != nil {
229+ onFirstResize(w, h)
230+ }
231+ return true
232+}
233+
234+func initialBrowserCreateSizeReadyFromObserver(width, height int32) bool {
235+ return observedViewportSizeReady(width, height, width, height)
236+}
237+
206 func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebView, w, h int32) {238 func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebView, w, h int32) {
207 log := logging.FromContext(ctx)239 log := logging.FromContext(ctx)
208 pc := wv.takePendingCreate()240 pc := wv.takePendingCreate()
@@ -329,26 +361,61 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie
329 }361 }
330 362
331 f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) {363 f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) {
332- log := logging.FromContext(ctx)364+ f.handlePopupShellInitialResize(ctx, popupWV, func(w, h int32) {
333- if popupWV.awaitsNativePopupAttachment() {365+ f.postPendingBrowserCreate(ctx, popupWV, w, h)
334- log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, awaiting native popup attach")366+ }, w, h)
335- popupWV.scheduleNativePopupFallback(nativePopupAttachFallbackDelay, func() {367+ })
336- if !popupWV.startNativePopupFallback() {368+ return popupWV, nil
337- return369+}
338- }
339- logging.FromContext(ctx).Warn().
340- Uint64("webview_id", uint64(popupWV.id)).
341- Msg("cef: native popup attach timed out, creating popup browser directly")
342- f.postPendingBrowserCreate(ctx, popupWV, w, h)
343- })
344- return
345- }
346 370
347- if !popupWV.preparePopupShellDirectBrowserCreation() {371+func (f *WebViewFactory) handlePopupShellInitialResize(
348- return372+ ctx context.Context,
373+ popupWV *WebView,
374+ postPendingCreate func(w, h int32),
375+ w, h int32,
376+) {
377+ if popupWV == nil {
378+ return
379+ }
380+ log := logging.FromContext(ctx)
381+ if popupWV.awaitsNativePopupAttachment() {
382+ log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell initial size observed, awaiting native popup attach")
383+ f.schedulePopupShellNativeFallback(ctx, popupWV)
384+ return
385+ }
386+ if popupWV.awaitsBrowserCreateFromNativePopupFallback() {
387+ log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell size ready after fallback, creating browser directly")
388+ if postPendingCreate != nil {
389+ postPendingCreate(w, h)
349 }390 }
350- log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, creating browser directly")391+ return
351- f.postPendingBrowserCreate(ctx, popupWV, w, h)392+ }
393+ if !popupWV.preparePopupShellDirectBrowserCreation() {
394+ return
395+ }
396+ log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, creating browser directly")
397+ if postPendingCreate != nil {
398+ postPendingCreate(w, h)
399+ }
400+}
401+
402+func (f *WebViewFactory) schedulePopupShellNativeFallback(ctx context.Context, popupWV *WebView) {
403+ if popupWV == nil {
404+ return
405+ }
406+ popupWV.scheduleNativePopupFallback(nativePopupAttachFallbackDelay, func() {
407+ popupWV.runOnGTK(func() {
408+ if !popupWV.startNativePopupFallback() {
409+ return
410+ }
411+ logging.FromContext(ctx).Warn().
412+ Uint64("webview_id", uint64(popupWV.id)).
413+ Msg("cef: native popup attach timed out, starting direct popup fallback")
414+ synced := popupWV.syncViewportNowOnGTK(ctx, "native-popup-fallback-timeout")
415+ logging.FromContext(ctx).Debug().
416+ Uint64("webview_id", uint64(popupWV.id)).
417+ Bool("browser_ready", synced).
418+ Msg("cef: popup fallback viewport sync requested")
419+ })
352 })420 })
353- return popupWV, nil
354 }421 }
M internal/infrastructure/cef/factory_resize_test.go +169 -0
@@ -55,6 +55,175 @@ func TestShouldStartBrowserCreateFromSizeObserver_FalseAfterPendingCreateConsume
5555 require.False(t, wv.shouldStartBrowserCreateFromSizeObserver())
5656 }
5757
58+func TestInitialBrowserCreateSizeReadyFromObserver(t *testing.T) {
59+ tests := []struct {
60+ name string
61+ width int32
62+ height int32
63+ want bool
64+ }{
65+ {name: "rejects bootstrap one by one", width: 1, height: 1, want: false},
66+ {name: "rejects zero width", width: 0, height: 480, want: false},
67+ {name: "rejects one pixel height", width: 640, height: 1, want: false},
68+ {name: "accepts real positive size", width: 640, height: 480, want: true},
69+ }
70+
71+ for _, tt := range tests {
72+ t.Run(tt.name, func(t *testing.T) {
73+ require.Equal(t, tt.want, initialBrowserCreateSizeReadyFromObserver(tt.width, tt.height))
74+ })
75+ }
76+}
77+
78+func TestHandleInitialBrowserCreateSizeObserver_IgnoresBootstrapForNormalWebView(t *testing.T) {
79+ factory := &WebViewFactory{}
80+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
81+ prepared := false
82+ resized := false
83+
84+ handled := factory.handleInitialBrowserCreateSizeObserver(
85+ context.Background(),
86+ wv,
87+ func(int32, int32) { resized = true },
88+ func() error {
89+ prepared = true
90+ return nil
91+ },
92+ 1,
93+ 1,
94+ )
95+
96+ require.True(t, handled)
97+ require.False(t, prepared)
98+ require.False(t, resized)
99+ require.True(t, wv.shouldStartBrowserCreateFromSizeObserver())
100+}
101+
102+func TestHandleInitialBrowserCreateSizeObserver_BootstrapPopupArmsFallbackWithoutPreparing(t *testing.T) {
103+ factory := &WebViewFactory{}
104+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
105+ prepared := false
106+ var scheduled func()
107+ wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer {
108+ scheduled = fn
109+ return stubStoppableTimer{}
110+ }
111+
112+ handled := factory.handleInitialBrowserCreateSizeObserver(
113+ context.Background(),
114+ wv,
115+ func(w, h int32) {
116+ factory.handlePopupShellInitialResize(context.Background(), wv, nil, w, h)
117+ },
118+ func() error {
119+ prepared = true
120+ return nil
121+ },
122+ 1,
123+ 1,
124+ )
125+
126+ require.True(t, handled)
127+ require.False(t, prepared)
128+ require.NotNil(t, scheduled)
129+ require.True(t, wv.shouldStartBrowserCreateFromSizeObserver())
130+}
131+
132+func TestHandleInitialBrowserCreateSizeObserver_ReadySizePreparesAndConsumesInitialResize(t *testing.T) {
133+ factory := &WebViewFactory{}
134+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
135+ prepared := 0
136+ resizes := 0
137+
138+ handled := factory.handleInitialBrowserCreateSizeObserver(
139+ context.Background(),
140+ wv,
141+ func(int32, int32) { resizes++ },
142+ func() error {
143+ prepared++
144+ return nil
145+ },
146+ 640,
147+ 480,
148+ )
149+
150+ require.True(t, handled)
151+ require.Equal(t, 1, prepared)
152+ require.Equal(t, 1, resizes)
153+ require.False(t, wv.shouldStartBrowserCreateFromSizeObserver())
154+}
155+
156+func TestHandleInitialBrowserCreateSizeObserver_ReadySizeAfterPopupFallbackPostsCreate(t *testing.T) {
157+ factory := &WebViewFactory{}
158+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
159+ prepared := 0
160+ posted := 0
161+ var scheduled func()
162+ wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer {
163+ scheduled = fn
164+ return stubStoppableTimer{}
165+ }
166+ onFirstResize := func(w, h int32) {
167+ factory.handlePopupShellInitialResize(context.Background(), wv, func(int32, int32) {
168+ posted++
169+ }, w, h)
170+ }
171+
172+ handled := factory.handleInitialBrowserCreateSizeObserver(
173+ context.Background(),
174+ wv,
175+ onFirstResize,
176+ func() error {
177+ prepared++
178+ return nil
179+ },
180+ 0,
181+ 480,
182+ )
183+ require.True(t, handled)
184+ require.Equal(t, 0, prepared)
185+ require.Equal(t, 0, posted)
186+ require.NotNil(t, scheduled)
187+ require.True(t, wv.shouldStartBrowserCreateFromSizeObserver())
188+
189+ scheduled()
190+ require.True(t, wv.awaitsBrowserCreateFromNativePopupFallback())
191+
192+ handled = factory.handleInitialBrowserCreateSizeObserver(
193+ context.Background(),
194+ wv,
195+ onFirstResize,
196+ func() error {
197+ prepared++
198+ return nil
199+ },
200+ 640,
201+ 480,
202+ )
203+ require.True(t, handled)
204+ require.Equal(t, 1, prepared)
205+ require.Equal(t, 1, posted)
206+ require.False(t, wv.shouldStartBrowserCreateFromSizeObserver())
207+}
208+
209+func TestSchedulePopupShellNativeFallback_StartsFallbackOnTimeout(t *testing.T) {
210+ factory := &WebViewFactory{}
211+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
212+ var scheduled func()
213+ wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer {
214+ scheduled = fn
215+ return stubStoppableTimer{}
216+ }
217+
218+ factory.schedulePopupShellNativeFallback(context.Background(), wv)
219+ require.NotNil(t, scheduled)
220+
221+ scheduled()
222+
223+ require.True(t, wv.nativePopupFallbackStarted)
224+ require.False(t, wv.nativePopupCandidate)
225+}
226+
58227 func TestPostPendingBrowserCreate_RetriesWhenPostingTaskFails(t *testing.T) {
59228 oldNewTask := cefNewTask
60229 oldPostTask := cefPostTask
@@ -55,6 +55,175 @@ func TestShouldStartBrowserCreateFromSizeObserver_FalseAfterPendingCreateConsume
55 require.False(t, wv.shouldStartBrowserCreateFromSizeObserver())55 require.False(t, wv.shouldStartBrowserCreateFromSizeObserver())
56 }56 }
57 57
58+func TestInitialBrowserCreateSizeReadyFromObserver(t *testing.T) {
59+ tests := []struct {
60+ name string
61+ width int32
62+ height int32
63+ want bool
64+ }{
65+ {name: "rejects bootstrap one by one", width: 1, height: 1, want: false},
66+ {name: "rejects zero width", width: 0, height: 480, want: false},
67+ {name: "rejects one pixel height", width: 640, height: 1, want: false},
68+ {name: "accepts real positive size", width: 640, height: 480, want: true},
69+ }
70+
71+ for _, tt := range tests {
72+ t.Run(tt.name, func(t *testing.T) {
73+ require.Equal(t, tt.want, initialBrowserCreateSizeReadyFromObserver(tt.width, tt.height))
74+ })
75+ }
76+}
77+
78+func TestHandleInitialBrowserCreateSizeObserver_IgnoresBootstrapForNormalWebView(t *testing.T) {
79+ factory := &WebViewFactory{}
80+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
81+ prepared := false
82+ resized := false
83+
84+ handled := factory.handleInitialBrowserCreateSizeObserver(
85+ context.Background(),
86+ wv,
87+ func(int32, int32) { resized = true },
88+ func() error {
89+ prepared = true
90+ return nil
91+ },
92+ 1,
93+ 1,
94+ )
95+
96+ require.True(t, handled)
97+ require.False(t, prepared)
98+ require.False(t, resized)
99+ require.True(t, wv.shouldStartBrowserCreateFromSizeObserver())
100+}
101+
102+func TestHandleInitialBrowserCreateSizeObserver_BootstrapPopupArmsFallbackWithoutPreparing(t *testing.T) {
103+ factory := &WebViewFactory{}
104+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
105+ prepared := false
106+ var scheduled func()
107+ wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer {
108+ scheduled = fn
109+ return stubStoppableTimer{}
110+ }
111+
112+ handled := factory.handleInitialBrowserCreateSizeObserver(
113+ context.Background(),
114+ wv,
115+ func(w, h int32) {
116+ factory.handlePopupShellInitialResize(context.Background(), wv, nil, w, h)
117+ },
118+ func() error {
119+ prepared = true
120+ return nil
121+ },
122+ 1,
123+ 1,
124+ )
125+
126+ require.True(t, handled)
127+ require.False(t, prepared)
128+ require.NotNil(t, scheduled)
129+ require.True(t, wv.shouldStartBrowserCreateFromSizeObserver())
130+}
131+
132+func TestHandleInitialBrowserCreateSizeObserver_ReadySizePreparesAndConsumesInitialResize(t *testing.T) {
133+ factory := &WebViewFactory{}
134+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
135+ prepared := 0
136+ resizes := 0
137+
138+ handled := factory.handleInitialBrowserCreateSizeObserver(
139+ context.Background(),
140+ wv,
141+ func(int32, int32) { resizes++ },
142+ func() error {
143+ prepared++
144+ return nil
145+ },
146+ 640,
147+ 480,
148+ )
149+
150+ require.True(t, handled)
151+ require.Equal(t, 1, prepared)
152+ require.Equal(t, 1, resizes)
153+ require.False(t, wv.shouldStartBrowserCreateFromSizeObserver())
154+}
155+
156+func TestHandleInitialBrowserCreateSizeObserver_ReadySizeAfterPopupFallbackPostsCreate(t *testing.T) {
157+ factory := &WebViewFactory{}
158+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
159+ prepared := 0
160+ posted := 0
161+ var scheduled func()
162+ wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer {
163+ scheduled = fn
164+ return stubStoppableTimer{}
165+ }
166+ onFirstResize := func(w, h int32) {
167+ factory.handlePopupShellInitialResize(context.Background(), wv, func(int32, int32) {
168+ posted++
169+ }, w, h)
170+ }
171+
172+ handled := factory.handleInitialBrowserCreateSizeObserver(
173+ context.Background(),
174+ wv,
175+ onFirstResize,
176+ func() error {
177+ prepared++
178+ return nil
179+ },
180+ 0,
181+ 480,
182+ )
183+ require.True(t, handled)
184+ require.Equal(t, 0, prepared)
185+ require.Equal(t, 0, posted)
186+ require.NotNil(t, scheduled)
187+ require.True(t, wv.shouldStartBrowserCreateFromSizeObserver())
188+
189+ scheduled()
190+ require.True(t, wv.awaitsBrowserCreateFromNativePopupFallback())
191+
192+ handled = factory.handleInitialBrowserCreateSizeObserver(
193+ context.Background(),
194+ wv,
195+ onFirstResize,
196+ func() error {
197+ prepared++
198+ return nil
199+ },
200+ 640,
201+ 480,
202+ )
203+ require.True(t, handled)
204+ require.Equal(t, 1, prepared)
205+ require.Equal(t, 1, posted)
206+ require.False(t, wv.shouldStartBrowserCreateFromSizeObserver())
207+}
208+
209+func TestSchedulePopupShellNativeFallback_StartsFallbackOnTimeout(t *testing.T) {
210+ factory := &WebViewFactory{}
211+ wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
212+ var scheduled func()
213+ wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer {
214+ scheduled = fn
215+ return stubStoppableTimer{}
216+ }
217+
218+ factory.schedulePopupShellNativeFallback(context.Background(), wv)
219+ require.NotNil(t, scheduled)
220+
221+ scheduled()
222+
223+ require.True(t, wv.nativePopupFallbackStarted)
224+ require.False(t, wv.nativePopupCandidate)
225+}
226+
58 func TestPostPendingBrowserCreate_RetriesWhenPostingTaskFails(t *testing.T) {227 func TestPostPendingBrowserCreate_RetriesWhenPostingTaskFails(t *testing.T) {
59 oldNewTask := cefNewTask228 oldNewTask := cefNewTask
60 oldPostTask := cefPostTask229 oldPostTask := cefPostTask
M internal/infrastructure/cef/webview.go +9 -0
@@ -767,6 +767,15 @@ func (wv *WebView) awaitsNativePopupAttachment() bool {
767767 return wv.browser == nil && !wv.nativePopupFallbackStarted && (wv.nativePopupCandidate || wv.nativePopupID != 0)
768768 }
769769
770+func (wv *WebView) awaitsBrowserCreateFromNativePopupFallback() bool {
771+ if wv == nil {
772+ return false
773+ }
774+ wv.mu.RLock()
775+ defer wv.mu.RUnlock()
776+ return wv.browser == nil && wv.nativePopupFallbackStarted && wv.pendingCreate != nil
777+}
778+
770779 func (wv *WebView) nativePopupFallbackScheduler() func(time.Duration, func()) stoppableTimer {
771780 if wv != nil && wv.nativePopupFallbackSchedule != nil {
772781 return wv.nativePopupFallbackSchedule
@@ -767,6 +767,15 @@ func (wv *WebView) awaitsNativePopupAttachment() bool {
767 return wv.browser == nil && !wv.nativePopupFallbackStarted && (wv.nativePopupCandidate || wv.nativePopupID != 0)767 return wv.browser == nil && !wv.nativePopupFallbackStarted && (wv.nativePopupCandidate || wv.nativePopupID != 0)
768 }768 }
769 769
770+func (wv *WebView) awaitsBrowserCreateFromNativePopupFallback() bool {
771+ if wv == nil {
772+ return false
773+ }
774+ wv.mu.RLock()
775+ defer wv.mu.RUnlock()
776+ return wv.browser == nil && wv.nativePopupFallbackStarted && wv.pendingCreate != nil
777+}
778+
770 func (wv *WebView) nativePopupFallbackScheduler() func(time.Duration, func()) stoppableTimer {779 func (wv *WebView) nativePopupFallbackScheduler() func(time.Duration, func()) stoppableTimer {
771 if wv != nil && wv.nativePopupFallbackSchedule != nil {780 if wv != nil && wv.nativePopupFallbackSchedule != nil {
772 return wv.nativePopupFallbackSchedule781 return wv.nativePopupFallbackSchedule

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