mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-29 07:49:14 +01:00
[client] Separate a window being painted from its frontend being mounted
One flag gated both showing a window and emitting to it, so the fallback timer set it for a frontend that had not subscribed yet: the queued events were flushed into a window that could not hear them, losing the login trigger and the settings tab selection. Showing is now gated on painted and emitting on mounted, and only a real frontend report sets mounted. The fallback timer also moved to its own helper so the runtime-ready hook can rearm it, giving the frontend a full budget to mount rather than sharing one with webview boot.
This commit is contained in:
@@ -122,7 +122,12 @@ type WindowManager struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
createMu sync.Mutex
|
createMu sync.Mutex
|
||||||
newMain func(startURL string) *application.WebviewWindow
|
newMain func(startURL string) *application.WebviewWindow
|
||||||
ready map[uint]bool
|
// painted gates showing a window: set by the frontend's first render, or by the
|
||||||
|
// fallback timer so a webview that never wakes up still becomes visible.
|
||||||
|
painted map[uint]bool
|
||||||
|
// mounted gates emitting to a window: set only by a real frontend report, since an
|
||||||
|
// event emitted to a frontend that has not subscribed yet is dropped, not queued.
|
||||||
|
mounted map[uint]bool
|
||||||
showPending map[uint]bool
|
showPending map[uint]bool
|
||||||
pendingTab map[uint]string
|
pendingTab map[uint]string
|
||||||
pendingEmits map[uint][]string
|
pendingEmits map[uint][]string
|
||||||
@@ -147,7 +152,8 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo
|
|||||||
translator: translator,
|
translator: translator,
|
||||||
prefs: prefs,
|
prefs: prefs,
|
||||||
linuxIcon: linuxIcon,
|
linuxIcon: linuxIcon,
|
||||||
ready: map[uint]bool{},
|
painted: map[uint]bool{},
|
||||||
|
mounted: map[uint]bool{},
|
||||||
showPending: map[uint]bool{},
|
showPending: map[uint]bool{},
|
||||||
pendingTab: map[uint]string{},
|
pendingTab: map[uint]string{},
|
||||||
pendingEmits: map[uint][]string{},
|
pendingEmits: map[uint][]string{},
|
||||||
@@ -212,13 +218,13 @@ func (s *WindowManager) OpenSettings(tab string) {
|
|||||||
w, _ := s.ensureWindow(&s.settings, s.newSettingsWindow)
|
w, _ := s.ensureWindow(&s.settings, s.newSettingsWindow)
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
ready := s.ready[w.ID()]
|
mounted := s.mounted[w.ID()]
|
||||||
if !ready {
|
if !mounted {
|
||||||
s.pendingTab[w.ID()] = target
|
s.pendingTab[w.ID()] = target
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
if ready {
|
if mounted {
|
||||||
s.app.Event.Emit(EventSettingsOpen, target)
|
s.app.Event.Emit(EventSettingsOpen, target)
|
||||||
}
|
}
|
||||||
s.showWhenReady(w)
|
s.showWhenReady(w)
|
||||||
@@ -559,14 +565,14 @@ func (s *WindowManager) ShowMainAndEmit(event string) {
|
|||||||
|
|
||||||
id := w.ID()
|
id := w.ID()
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
ready := s.ready[id]
|
mounted := s.mounted[id]
|
||||||
if !ready {
|
if !mounted {
|
||||||
s.pendingEmits[id] = append(s.pendingEmits[id], event)
|
s.pendingEmits[id] = append(s.pendingEmits[id], event)
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
s.showWhenReady(w)
|
s.showWhenReady(w)
|
||||||
if ready {
|
if mounted {
|
||||||
s.app.Event.Emit(event)
|
s.app.Event.Emit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -608,24 +614,41 @@ func (s *WindowManager) ensureWindow(slot **application.WebviewWindow, factory f
|
|||||||
return w, true
|
return w, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// armReady starts the fallback that shows w even if its frontend never reports a first
|
||||||
|
// render. The timer starts at creation, because a hidden webview can be suspended before
|
||||||
|
// it reaches WindowRuntimeReady — the very case this fallback covers. That makes the first
|
||||||
|
// budget cover webview boot as well, so the runtime-ready hook rearms it to give the
|
||||||
|
// frontend its own full budget to mount and paint.
|
||||||
func (s *WindowManager) armReady(w *application.WebviewWindow) {
|
func (s *WindowManager) armReady(w *application.WebviewWindow) {
|
||||||
if w == nil {
|
if w == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.armPaintedFallback(w)
|
||||||
|
w.RegisterHook(events.Common.WindowRuntimeReady, func(_ *application.WindowEvent) {
|
||||||
|
s.armPaintedFallback(w)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WindowManager) armPaintedFallback(w *application.WebviewWindow) {
|
||||||
id := w.ID()
|
id := w.ID()
|
||||||
timer := time.AfterFunc(paintedFallback, func() {
|
timer := time.AfterFunc(paintedFallback, func() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
ready := s.ready[id]
|
painted := s.painted[id]
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
if ready {
|
if painted {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Warnf("window %q never reported a first render, showing it anyway", w.Name())
|
log.Warnf("window %q never reported a first render, showing it anyway", w.Name())
|
||||||
s.markReady(w)
|
s.markPainted(w)
|
||||||
})
|
})
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if s.ready[id] {
|
if prev := s.fallbackTimers[id]; prev != nil {
|
||||||
|
prev.Stop()
|
||||||
|
}
|
||||||
|
if s.painted[id] {
|
||||||
timer.Stop()
|
timer.Stop()
|
||||||
|
delete(s.fallbackTimers, id)
|
||||||
} else {
|
} else {
|
||||||
s.fallbackTimers[id] = timer
|
s.fallbackTimers[id] = timer
|
||||||
}
|
}
|
||||||
@@ -642,7 +665,8 @@ func (s *WindowManager) watchPainted() {
|
|||||||
log.Debugf("ignoring stale painted report for window %q", e.Sender)
|
log.Debugf("ignoring stale painted report for window %q", e.Sender)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.markReady(w)
|
s.markPainted(w)
|
||||||
|
s.markMounted(w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,7 +678,7 @@ func (s *WindowManager) watchTriggerLogin() {
|
|||||||
s.headlessTimer = nil
|
s.headlessTimer = nil
|
||||||
}
|
}
|
||||||
w := s.mainWindow
|
w := s.mainWindow
|
||||||
ready := w != nil && s.ready[w.ID()]
|
ready := w != nil && s.mounted[w.ID()]
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
if ready {
|
if ready {
|
||||||
return
|
return
|
||||||
@@ -669,7 +693,7 @@ func (s *WindowManager) watchTriggerLogin() {
|
|||||||
if created {
|
if created {
|
||||||
s.headlessMain = true
|
s.headlessMain = true
|
||||||
}
|
}
|
||||||
pending := !s.ready[w.ID()]
|
pending := !s.mounted[w.ID()]
|
||||||
if pending {
|
if pending {
|
||||||
s.pendingEmits[w.ID()] = append(s.pendingEmits[w.ID()], EventTriggerLogin)
|
s.pendingEmits[w.ID()] = append(s.pendingEmits[w.ID()], EventTriggerLogin)
|
||||||
}
|
}
|
||||||
@@ -730,7 +754,8 @@ func (s *WindowManager) forgetWindowLocked(w *application.WebviewWindow) {
|
|||||||
timer.Stop()
|
timer.Stop()
|
||||||
}
|
}
|
||||||
delete(s.fallbackTimers, id)
|
delete(s.fallbackTimers, id)
|
||||||
delete(s.ready, id)
|
delete(s.painted, id)
|
||||||
|
delete(s.mounted, id)
|
||||||
delete(s.showPending, id)
|
delete(s.showPending, id)
|
||||||
delete(s.pendingTab, id)
|
delete(s.pendingTab, id)
|
||||||
delete(s.pendingEmits, id)
|
delete(s.pendingEmits, id)
|
||||||
@@ -791,19 +816,35 @@ func (s *WindowManager) windowByName(name string) *application.WebviewWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WindowManager) markReady(w *application.WebviewWindow) {
|
func (s *WindowManager) markPainted(w *application.WebviewWindow) {
|
||||||
id := w.ID()
|
id := w.ID()
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
already := s.ready[id]
|
already := s.painted[id]
|
||||||
s.ready[id] = true
|
s.painted[id] = true
|
||||||
wanted := s.showPending[id]
|
wanted := s.showPending[id]
|
||||||
tab, hasTab := s.pendingTab[id]
|
delete(s.showPending, id)
|
||||||
emits := s.pendingEmits[id]
|
|
||||||
if timer := s.fallbackTimers[id]; timer != nil {
|
if timer := s.fallbackTimers[id]; timer != nil {
|
||||||
timer.Stop()
|
timer.Stop()
|
||||||
delete(s.fallbackTimers, id)
|
delete(s.fallbackTimers, id)
|
||||||
}
|
}
|
||||||
delete(s.showPending, id)
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if already || !wanted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.showNow(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// markMounted records that the window's frontend is subscribed, and flushes the events
|
||||||
|
// held back for it. The fallback timer never calls this: showing a blank window is
|
||||||
|
// recoverable, emitting into a frontend that cannot hear it is not.
|
||||||
|
func (s *WindowManager) markMounted(w *application.WebviewWindow) {
|
||||||
|
id := w.ID()
|
||||||
|
s.mu.Lock()
|
||||||
|
already := s.mounted[id]
|
||||||
|
s.mounted[id] = true
|
||||||
|
tab, hasTab := s.pendingTab[id]
|
||||||
|
emits := s.pendingEmits[id]
|
||||||
delete(s.pendingTab, id)
|
delete(s.pendingTab, id)
|
||||||
delete(s.pendingEmits, id)
|
delete(s.pendingEmits, id)
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@@ -816,10 +857,6 @@ func (s *WindowManager) markReady(w *application.WebviewWindow) {
|
|||||||
s.app.Event.Emit(EventSettingsOpen, tab)
|
s.app.Event.Emit(EventSettingsOpen, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
if wanted {
|
|
||||||
s.showNow(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, event := range emits {
|
for _, event := range emits {
|
||||||
s.app.Event.Emit(event)
|
s.app.Event.Emit(event)
|
||||||
}
|
}
|
||||||
@@ -832,13 +869,13 @@ func (s *WindowManager) showWhenReady(w *application.WebviewWindow) {
|
|||||||
|
|
||||||
id := w.ID()
|
id := w.ID()
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
ready := s.ready[id]
|
painted := s.painted[id]
|
||||||
if !ready {
|
if !painted {
|
||||||
s.showPending[id] = true
|
s.showPending[id] = true
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
if ready {
|
if painted {
|
||||||
s.showNow(w)
|
s.showNow(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user