1package cef
2
3import (
4 "context"
5 "strings"
6 "time"
7
8 purecef "github.com/bnema/purego-cef/cef"
9 "github.com/bnema/puregotk/v4/gobject"
10 "github.com/bnema/puregotk/v4/gtk"
11
12 "github.com/bnema/dumber/internal/logging"
13 "github.com/bnema/dumber/internal/shared/syncdispatch"
14)
15
16type viewportSyncBrowserHost interface {
17 resizeNotifiableBrowserHost
18 NotifyScreenInfoChanged()
19 WasHidden(int32)
20}
21
22type pendingBrowserCreateObservedSizeBridge interface {
23 Size() (int32, int32)
24 RefreshObservedSizeOnGTKThread() (int32, int32)
25}
26
27// viewportWidgetVisibility is the effective GTK state of a widget in its
28// hierarchy. Gtk.Widget.IsVisible already includes ancestor visibility; mapped
29// is checked separately because a visible widget can still be unmapped.
30type viewportWidgetVisibility struct {
31 visible bool
32 mapped bool
33}
34
35func effectiveViewportVisibility(states []viewportWidgetVisibility) bool {
36 if len(states) == 0 {
37 return false
38 }
39 for _, state := range states {
40 if !state.visible || !state.mapped {
41 return false
42 }
43 }
44 return true
45}
46
47func effectiveViewportWidgetVisibility(widget *gtk.Widget) bool {
48 if widget == nil {
49 return false
50 }
51 // IsVisible is GTK's ancestor-aware visibility predicate. Pairing it with
52 // GetMapped makes hidden/unmapped ancestors effective CEF-hidden states.
53 return effectiveViewportVisibility([]viewportWidgetVisibility{{
54 visible: widget.IsVisible(),
55 mapped: widget.GetMapped(),
56 }})
57}
58
59const (
60 // pendingBrowserCreateObservedSizeRetryDelay spaces retries while waiting
61 // for the GTK bridge to observe the widget's allocated size.
62 pendingBrowserCreateObservedSizeRetryDelay = 16 * time.Millisecond
63 // pendingBrowserCreateObservedSizeMaxRetries caps observed-size retries
64 // before browser creation proceeds despite a remaining size mismatch.
65 pendingBrowserCreateObservedSizeMaxRetries = 8
66)
67
68type pendingBrowserCreateObservedSizeRetryAction uint8
69
70const (
71 pendingBrowserCreateObservedSizeRetryUnavailable pendingBrowserCreateObservedSizeRetryAction = iota
72 pendingBrowserCreateObservedSizeRetryAlreadyScheduled
73 pendingBrowserCreateObservedSizeRetryScheduled
74 pendingBrowserCreateObservedSizeRetryProceedWithoutDelay
75)
76
77// SyncViewport requests an explicit viewport resync for CEF OSR browsers.
78//
79// This is a hardening path for UI lifecycle transitions that may not emit a
80// fresh size change (reparenting, stacked visibility toggles, floating pane
81// updates, popup show, sibling promotion after close, etc.). Calls are
82// coalesced and executed on the GTK thread before notifying the CEF host.
83func (wv *WebView) SyncViewport(ctx context.Context, reason string) {
84 if wv == nil || wv.destroyed.Load() {
85 return
86 }
87 reason = strings.TrimSpace(reason)
88 if reason == "" {
89 reason = "manual"
90 }
91
92 wv.viewportSyncMu.Lock()
93 wv.viewportSyncReason = reason
94 if wv.viewportSyncPending {
95 wv.viewportSyncMu.Unlock()
96 return
97 }
98 wv.viewportSyncPending = true
99 wv.viewportSyncMu.Unlock()
100
101 wv.runOnGTK(func() {
102 wv.syncViewportOnGTK(wv.viewportSyncContext(ctx))
103 })
104}
105
106func (wv *WebView) viewportSyncContext(ctx context.Context) context.Context {
107 if ctx != nil {
108 return ctx
109 }
110 if wv != nil && wv.ctx != nil {
111 return wv.ctx
112 }
113 return context.Background()
114}
115
116func (wv *WebView) takeViewportSyncReason() string {
117 wv.viewportSyncMu.Lock()
118 defer wv.viewportSyncMu.Unlock()
119 reason := wv.viewportSyncReason
120 wv.viewportSyncReason = ""
121 wv.viewportSyncPending = false
122 return reason
123}
124
125func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
126 if wv == nil {
127 return
128 }
129 reason := wv.takeViewportSyncReason()
130 ctx = wv.viewportSyncContext(ctx)
131 if wv.syncViewportNowOnGTK(ctx, reason) {
132 wv.scheduleResizeRepaintPulse(ctx, reason)
133 }
134}
135
136func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool {
137 if wv == nil {
138 return false
139 }
140 reason = strings.TrimSpace(reason)
141 if reason == "" {
142 reason = "manual"
143 }
144 if wv.destroyed.Load() || wv.viewBridge == nil {
145 return false
146 }
147
148 if wv.tryStartPendingBrowserCreateOnGTKThread(ctx, reason) {
149 return false
150 }
151
152 wv.mu.RLock()
153 host := wv.host
154 wv.mu.RUnlock()
155 if host == nil {
156 logging.FromContext(ctx).Debug().
157 Uint64("webview_id", uint64(wv.id)).
158 Str("reason", reason).
159 Msg("cef: viewport sync skipped; browser host not ready")
160 return false
161 }
162
163 widget := wv.viewBridge.Widget()
164 visible := effectiveViewportWidgetVisibility(widget)
165 wv.notifyViewportSyncOnCEFUIThread(host, visible)
166 wv.syncZoomForBackingScaleOnCEFUIThread(host, reason)
167 logging.FromContext(ctx).Debug().
168 Uint64("webview_id", uint64(wv.id)).
169 Bool("visible", visible).
170 Str("reason", reason).
171 Msg("cef: viewport sync requested")
172 return true
173}
174
175func (wv *WebView) syncResizeViewportOnGTK(ctx context.Context, reason string) bool {
176 if !wv.syncViewportNowOnGTK(ctx, reason) {
177 return false
178 }
179 wv.scheduleResizeRepaintPulse(ctx, reason)
180 return true
181}
182
183// resizeViewportResyncDelaysMs re-run the full viewport resize notification
184// shortly after a GTK size change.
185var resizeViewportResyncDelaysMs = [...]int64{16, 48}
186
187// resizeRepaintOnlyDelaysMs keep requesting refresh frames until CEF's OSR
188// video capturer adopts the new frame size. Until then captured frames are
189// letterboxed; Invalidate forces a fresh capture instead of waiting for the
190// capturer's idle refresh timer (up to ~1s on static pages). These pulses skip
191// NotifyScreenInfoChanged, which would reallocate the surface ID each time.
192var resizeRepaintOnlyDelaysMs = [...]int64{96, 160, 250, 400}
193
194func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string) {
195 if wv == nil || wv.destroyed.Load() {
196 return
197 }
198 wv.mu.RLock()
199 host := wv.host
200 wv.mu.RUnlock()
201 if host == nil {
202 return
203 }
204 seq := wv.viewportResizePulseSeq.Add(1)
205 for _, delayMs := range resizeViewportResyncDelaysMs {
206 wv.postResizePulse(ctx, host, seq, delayMs, reason, true)
207 }
208 for _, delayMs := range resizeRepaintOnlyDelaysMs {
209 wv.postResizePulse(ctx, host, seq, delayMs, reason, false)
210 }
211}
212
213func (wv *WebView) postResizePulse(
214 ctx context.Context,
215 host viewportSyncBrowserHost,
216 seq uint64,
217 delayMs int64,
218 reason string,
219 resync bool,
220) {
221 task := cefNewTask(cefTaskFunc(func() {
222 if wv == nil || wv.destroyed.Load() || wv.viewportResizePulseSeq.Load() != seq {
223 return
224 }
225 wv.mu.RLock()
226 currentHost := wv.host
227 wv.mu.RUnlock()
228 if currentHost != host {
229 return
230 }
231 if resync {
232 notifyBrowserViewportResize(host)
233 } else {
234 host.Invalidate(purecef.PaintElementTypePetView)
235 }
236 logging.FromContext(ctx).Debug().
237 Uint64("webview_id", uint64(wv.id)).
238 Int64("delay_ms", delayMs).
239 Bool("resync", resync).
240 Str("reason", reason).
241 Msg("cef: delayed resize viewport sync")
242 }))
243 if task == nil {
244 return
245 }
246 cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs)
247}
248
249func (wv *WebView) tryStartPendingBrowserCreateOnGTKThread(ctx context.Context, reason string) bool {
250 if wv == nil || wv.destroyed.Load() || wv.factory == nil || wv.viewBridge == nil {
251 return false
252 }
253
254 wv.mu.RLock()
255 hasHost := wv.host != nil
256 pending := wv.pendingCreate != nil
257 wv.mu.RUnlock()
258 if hasHost || !pending {
259 return false
260 }
261
262 widget := wv.viewBridge.Widget()
263 if widget == nil {
264 return false
265 }
266 width := int32(widget.GetAllocatedWidth())
267 height := int32(widget.GetAllocatedHeight())
268 if width <= 0 {
269 width = int32(widget.GetWidth())
270 }
271 if height <= 0 {
272 height = int32(widget.GetHeight())
273 }
274 if width <= 0 || height <= 0 {
275 logging.FromContext(ctx).Debug().
276 Uint64("webview_id", uint64(wv.id)).
277 Int32("width", width).
278 Int32("height", height).
279 Str("reason", reason).
280 Msg("cef: viewport sync skipped pending browser creation; widget has no allocation yet")
281 return false
282 }
283
284 observedWidth, observedHeight, observedReady := pendingBrowserCreateObservedSize(wv.viewBridge, width, height)
285 if !observedReady {
286 widget.QueueAllocate()
287 widget.QueueResize()
288 attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(ctx, reason)
289 switch action {
290 case pendingBrowserCreateObservedSizeRetryUnavailable:
291 return false
292 case pendingBrowserCreateObservedSizeRetryAlreadyScheduled:
293 logging.FromContext(ctx).Debug().
294 Uint64("webview_id", uint64(wv.id)).
295 Int32("observed_width", observedWidth).
296 Int32("observed_height", observedHeight).
297 Int32("width", width).
298 Int32("height", height).
299 Int("attempt", attempt).
300 Str("reason", reason).
301 Msg("cef: pending browser creation retry already scheduled while awaiting bridge size")
302 return false
303 case pendingBrowserCreateObservedSizeRetryScheduled:
304 logging.FromContext(ctx).Debug().
305 Uint64("webview_id", uint64(wv.id)).
306 Int32("observed_width", observedWidth).
307 Int32("observed_height", observedHeight).
308 Int32("width", width).
309 Int32("height", height).
310 Int("attempt", attempt).
311 Int("max_attempts", pendingBrowserCreateObservedSizeMaxRetries).
312 Str("reason", reason).
313 Msg("cef: delaying pending browser creation until bridge observes widget size")
314 return false
315 case pendingBrowserCreateObservedSizeRetryProceedWithoutDelay:
316 // Creating the browser here is intentional: after bounded retries we
317 // fall back to the current GTK allocation rather than stalling popup
318 // creation.
319 logging.FromContext(ctx).Debug().
320 Uint64("webview_id", uint64(wv.id)).
321 Int32("observed_width", observedWidth).
322 Int32("observed_height", observedHeight).
323 Int32("width", width).
324 Int32("height", height).
325 Int("attempt", attempt).
326 Str("reason", reason).
327 Msg("cef: bridge size observation did not settle before browser creation; continuing")
328 }
329 }
330
331 if wv.shouldDeferPendingBrowserCreateFromViewportSync() {
332 logging.FromContext(ctx).Debug().
333 Uint64("webview_id", uint64(wv.id)).
334 Int32("width", width).
335 Int32("height", height).
336 Str("reason", reason).
337 Msg("cef: viewport sync deferred pending browser creation while awaiting native popup attach")
338 return false
339 }
340
341 if err := wv.viewBridge.PrepareOnGTKThread(); err != nil {
342 logging.FromContext(ctx).Debug().
343 Err(err).
344 Uint64("webview_id", uint64(wv.id)).
345 Int32("width", width).
346 Int32("height", height).
347 Str("reason", reason).
348 Msg("cef: viewport sync could not prepare bridge for pending browser creation")
349 return false
350 }
351
352 wv.markInitialBrowserCreateResizeHandled()
353 wv.factory.postPendingBrowserCreate(ctx, wv, width, height)
354 logging.FromContext(ctx).Debug().
355 Uint64("webview_id", uint64(wv.id)).
356 Int32("width", width).
357 Int32("height", height).
358 Str("reason", reason).
359 Msg("cef: viewport sync nudged pending browser creation")
360 return true
361}
362
363func (wv *WebView) shouldDeferPendingBrowserCreateFromViewportSync() bool {
364 return wv != nil && wv.awaitsNativePopupAttachment()
365}
366
367func pendingBrowserCreateObservedSize(
368 bridge pendingBrowserCreateObservedSizeBridge,
369 allocatedWidth, allocatedHeight int32,
370) (observedWidth, observedHeight int32, ready bool) {
371 if bridge == nil {
372 return 0, 0, false
373 }
374 observedWidth, observedHeight = bridge.Size()
375 if observedViewportSizeReady(observedWidth, observedHeight, allocatedWidth, allocatedHeight) {
376 return observedWidth, observedHeight, true
377 }
378 observedWidth, observedHeight = bridge.RefreshObservedSizeOnGTKThread()
379 return observedWidth, observedHeight, observedViewportSizeReady(observedWidth, observedHeight, allocatedWidth, allocatedHeight)
380}
381
382// observedViewportSizeReady reports whether observedWidth/observedHeight match
383// allocatedWidth/allocatedHeight within a ±1px tolerance.
384//
385// Fallback observed sizes (<=1) and non-positive allocated dimensions are
386// treated as not ready.
387func observedViewportSizeReady(observedWidth, observedHeight, allocatedWidth, allocatedHeight int32) bool {
388 if allocatedWidth <= 0 || allocatedHeight <= 0 {
389 return false
390 }
391 if observedWidth <= 1 || observedHeight <= 1 {
392 return false
393 }
394 return abs32(observedWidth-allocatedWidth) <= 1 && abs32(observedHeight-allocatedHeight) <= 1
395}
396
397// abs32 returns the absolute value of v.
398func abs32(v int32) int32 {
399 if v < 0 {
400 return -v
401 }
402 return v
403}
404
405// preparePendingBrowserCreateObservedSizeRetry updates the observed-size retry
406// state machine for a pending browser create.
407//
408// It returns the current attempt count and the resulting action. Scheduling is
409// coalesced so only one delayed retry is pending at a time, while the max-retry
410// path proceeds without leaving the scheduled flag set.
411func (wv *WebView) preparePendingBrowserCreateObservedSizeRetry(
412 ctx context.Context,
413 reason string,
414) (attempt int, action pendingBrowserCreateObservedSizeRetryAction) {
415 if wv == nil {
416 return 0, pendingBrowserCreateObservedSizeRetryUnavailable
417 }
418
419 shouldSchedule := false
420 wv.mu.Lock()
421 if wv.pendingCreate == nil {
422 wv.mu.Unlock()
423 return 0, pendingBrowserCreateObservedSizeRetryUnavailable
424 }
425 if wv.pendingCreate.observedSizeRetryScheduled {
426 attempt = wv.pendingCreate.observedSizeRetries
427 wv.mu.Unlock()
428 return attempt, pendingBrowserCreateObservedSizeRetryAlreadyScheduled
429 }
430 wv.pendingCreate.observedSizeRetries++
431 attempt = wv.pendingCreate.observedSizeRetries
432 if attempt <= pendingBrowserCreateObservedSizeMaxRetries {
433 wv.pendingCreate.observedSizeRetryScheduled = true
434 shouldSchedule = true
435 }
436 wv.mu.Unlock()
437
438 if !shouldSchedule {
439 return attempt, pendingBrowserCreateObservedSizeRetryProceedWithoutDelay
440 }
441 wv.schedulePendingBrowserCreateObservedSizeRetry(ctx, reason)
442 return attempt, pendingBrowserCreateObservedSizeRetryScheduled
443}
444
445// clearPendingBrowserCreateObservedSizeRetry clears the scheduled-retry flag
446// so a future observed-size reservation can schedule another attempt.
447func (wv *WebView) clearPendingBrowserCreateObservedSizeRetry() {
448 if wv == nil {
449 return
450 }
451 wv.mu.Lock()
452 defer wv.mu.Unlock()
453 if wv.pendingCreate != nil {
454 wv.pendingCreate.observedSizeRetryScheduled = false
455 }
456}
457
458// schedulePendingBrowserCreateObservedSizeRetry schedules a delayed viewport
459// sync retry using ctx after pendingBrowserCreateObservedSizeRetryDelay.
460//
461// It clears the scheduled flag before re-invoking syncViewportNowOnGTK with a
462// retry-specific suffix appended to reason.
463func (wv *WebView) schedulePendingBrowserCreateObservedSizeRetry(ctx context.Context, reason string) {
464 if wv == nil || wv.destroyed.Load() {
465 return
466 }
467 cefScheduleAfter(pendingBrowserCreateObservedSizeRetryDelay, func() {
468 if wv == nil || wv.destroyed.Load() {
469 return
470 }
471 wv.clearPendingBrowserCreateObservedSizeRetry()
472 wv.runOnGTK(func() {
473 wv.syncViewportNowOnGTK(ctx, reason+"-await-observed-size")
474 })
475 })
476}
477
478func (wv *WebView) installViewportSyncHooks() syncdispatch.SyncDispatchResult {
479 if wv == nil || wv.viewBridge == nil {
480 return syncdispatch.SyncDispatchResult{Label: "cef.install_viewport_sync_hooks", Status: syncdispatch.SyncDispatchInline}
481 }
482 return wv.runOnGTKSyncLabel("cef.install_viewport_sync_hooks", func() {
483 wv.installViewportSyncHooksOnGTKThread()
484 })
485}
486
487func (wv *WebView) installViewportSyncHooksOnGTKThread() {
488 if wv == nil || wv.viewBridge == nil || wv.viewportMapFunc != nil {
489 return
490 }
491 widget := wv.viewBridge.Widget()
492 if widget == nil {
493 return
494 }
495
496 wv.viewportMapFunc = func(gtk.Widget) {
497 wv.SyncViewport(wv.ctx, "gtk-map")
498 }
499 wv.viewportShowFunc = func(gtk.Widget) {
500 wv.SyncViewport(wv.ctx, "gtk-show")
501 }
502 wv.viewportHideFunc = func(gtk.Widget) {
503 wv.SyncViewport(wv.ctx, "gtk-hide")
504 }
505 wv.viewportUnmapFunc = func(gtk.Widget) {
506 wv.SyncViewport(wv.ctx, "gtk-unmap")
507 }
508 wv.viewportRealizeFunc = func(gtk.Widget) {
509 wv.SyncViewport(wv.ctx, "gtk-realize")
510 }
511
512 wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc)
513 wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc)
514 wv.viewportHideSignalID = widget.ConnectHide(&wv.viewportHideFunc)
515 wv.viewportUnmapSignalID = widget.ConnectUnmap(&wv.viewportUnmapFunc)
516 wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc)
517}
518
519func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() {
520 if wv == nil || wv.viewBridge == nil {
521 return
522 }
523 widget := wv.viewBridge.Widget()
524 if widget != nil {
525 widgetPtr := widget.GoPointer()
526 if widgetPtr != 0 {
527 obj := gobject.ObjectNewFromInternalPtr(widgetPtr)
528 if wv.viewportMapSignalID != 0 {
529 gobject.SignalHandlerDisconnect(obj, wv.viewportMapSignalID)
530 wv.viewportMapSignalID = 0
531 }
532 if wv.viewportShowSignalID != 0 {
533 gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID)
534 wv.viewportShowSignalID = 0
535 }
536 if wv.viewportHideSignalID != 0 {
537 gobject.SignalHandlerDisconnect(obj, wv.viewportHideSignalID)
538 wv.viewportHideSignalID = 0
539 }
540 if wv.viewportUnmapSignalID != 0 {
541 gobject.SignalHandlerDisconnect(obj, wv.viewportUnmapSignalID)
542 wv.viewportUnmapSignalID = 0
543 }
544 if wv.viewportRealizeSignalID != 0 {
545 gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID)
546 wv.viewportRealizeSignalID = 0
547 }
548 }
549 }
550 wv.viewportMapFunc = nil
551 wv.viewportShowFunc = nil
552 wv.viewportHideFunc = nil
553 wv.viewportUnmapFunc = nil
554 wv.viewportRealizeFunc = nil
555}
556
557func (wv *WebView) notifyViewportSyncOnCEFUIThread(host viewportSyncBrowserHost, visible bool) {
558 if host == nil {
559 return
560 }
561 task := cefNewTask(cefTaskFunc(func() {
562 if wv == nil || wv.destroyed.Load() {
563 return
564 }
565 wv.mu.RLock()
566 currentHost := wv.host
567 wv.mu.RUnlock()
568 if currentHost != host {
569 return
570 }
571 wv.applyEffectiveVisibility(host, visible)
572 notifyBrowserViewportResize(host)
573 }))
574 if task == nil {
575 return
576 }
577 cefPostDelayedTask(purecef.ThreadIDTidUi, task, 0)
578}
579
580func (wv *WebView) syncZoomForBackingScaleOnCEFUIThread(host purecef.BrowserHost, reason string) {
581 if host == nil || wv == nil {
582 return
583 }
584 compensation := wv.pageZoomCompensation()
585 if !wv.shouldReapplyZoomForCompensation(compensation) {
586 return
587 }
588 task := cefNewTask(cefTaskFunc(func() {
589 if wv == nil || wv.destroyed.Load() {
590 return
591 }
592 wv.mu.RLock()
593 currentHost := wv.host
594 wv.mu.RUnlock()
595 if currentHost != host {
596 return
597 }
598 // Re-check on execution: a newer output scale may have superseded the
599 // compensation captured when this task was scheduled.
600 if !wv.shouldReapplyZoomForCompensation(wv.pageZoomCompensation()) {
601 return
602 }
603 wv.reapplyCurrentZoomForBackingScale(reason)
604 }))
605 if task == nil {
606 return
607 }
608 cefPostDelayedTask(purecef.ThreadIDTidUi, task, 0)
609}
610
611// applyEffectiveVisibility reports the initial effective visibility and later
612// transitions exactly once. CEF OSR begins hidden, so even an initially hidden
613// GTK widget must be explicitly reported to keep both lifecycles aligned.
614func (wv *WebView) applyEffectiveVisibility(host viewportSyncBrowserHost, visible bool) {
615 if wv == nil || host == nil || wv.destroyed.Load() {
616 return
617 }
618 wv.mu.Lock()
619 if wv.effectiveVisibilityKnown && wv.effectiveVisible == visible {
620 wv.mu.Unlock()
621 return
622 }
623 wv.effectiveVisibilityKnown = true
624 wv.effectiveVisible = visible
625 // Consume the deferred GPU recovery in the same critical section that
626 // publishes visibility: a concurrent deferral either saw the view hidden
627 // (and set pending before this point) or sees it visible and does not defer.
628 resumeGPURecovery := visible && wv.gpuRecoveryPending
629 if visible {
630 wv.gpuRecoveryPending = false
631 }
632 wv.mu.Unlock()
633 if visible {
634 host.WasHidden(0)
635 if resumeGPURecovery {
636 wv.startGPURecovery()
637 }
638 return
639 }
640 // The hidden transition retires scroll motion: a re-shown page never
641 // resumes old motion, and delayed cleanup cannot cancel a newer visible
642 // session because cleanup is epoch-scoped.
643 wv.invalidateScrollMotion()
644 host.WasHidden(1)
645}
646
647func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) {
648 if host == nil {
649 return
650 }
651 if visible {
652 host.WasHidden(0)
653 }
654 notifyBrowserViewportResize(host)
655}
656
657func notifyBrowserViewportResize(host viewportSyncBrowserHost) {
658 if host == nil {
659 return
660 }
661 host.NotifyScreenInfoChanged()
662 notifyBrowserResize(host)
663}