package glfw import ( "runtime" "sync/atomic" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/async" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/driver/common" "fyne.io/fyne/v2/internal/painter" "fyne.io/fyne/v2/internal/scale" ) type funcData struct { f func() done chan struct{} // Zero allocation signalling channel } // channel for queuing functions on the main thread var funcQueue = async.NewUnboundedChan[funcData]() var running, drained atomic.Bool // Arrange that main.main runs on main thread. func init() { runtime.LockOSThread() async.SetMainGoroutine() } // force a function f to run on the main thread func runOnMain(f func()) { runOnMainWithWait(f, true) } // force a function f to run on the main thread and specify if we should wait for it to return func runOnMainWithWait(f func(), wait bool) { // If we are on main before app run just execute - otherwise add it to the main queue and wait. // We also need to run it as-is if the app is in the process of shutting down as the queue will be stopped. if (!running.Load() && async.IsMainGoroutine()) || drained.Load() { f() return } if wait { done := common.DonePool.Get() defer common.DonePool.Put(done) funcQueue.In() <- funcData{f: f, done: done} <-done } else { funcQueue.In() <- funcData{f: f} } } func (d *gLDriver) drawSingleFrame() { refreshed := false for _, win := range d.windowList() { w := win.(*window) if w.closing { continue } // CheckDirtyAndClear must be checked after visibility, // because when a window becomes visible, it could be // showing old content without a dirty flag set to true. // Do the clear if and only if the window is visible. if !w.visible || !w.canvas.CheckDirtyAndClear() { // Window hidden or not being redrawn, mark canvasForObject // cache alive if it hasn't been done recently // n.b. we need to make sure threshold is a bit *after* // time.Now() - CacheDuration() threshold := time.Now().Add(10*time.Second - cache.ValidDuration) if w.lastWalkedTime.Before(threshold) { w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) { // marks canvas for object cache entry alive _ = cache.GetCanvasForObject(node.Obj()) // marks renderer cache entry alive if wid, ok := node.Obj().(fyne.Widget); ok { _, _ = cache.CachedRenderer(wid) } }) w.lastWalkedTime = time.Now() } continue } w.RunWithContext(func() { if w.driver.repaintWindow(w) { refreshed = true } }) } cache.Clean(refreshed) } func (d *gLDriver) runGL() { if !running.CompareAndSwap(false, true) { return // Run was called twice. } d.init() if d.trayStart != nil { d.trayStart() } fyne.CurrentApp().Settings().AddListener(func(set fyne.Settings) { painter.ClearFontCache() cache.ResetThemeCaches() app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) { c, ok := w.Canvas().(*glCanvas) if !ok { return } c.applyThemeOutOfTreeObjects() c.reloadScale() }) }) if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnStarted(); f != nil { f() } eventTick := time.NewTicker(time.Second / 60) for { select { case <-d.done: eventTick.Stop() d.Terminate() l := fyne.CurrentApp().Lifecycle().(*app.Lifecycle) if f := l.OnStopped(); f != nil { l.QueueEvent(f) } // as we are shutting down make sure we drain the pending funcQueue and close it out. for len(funcQueue.Out()) > 0 { f := <-funcQueue.Out() if f.done != nil { f.done <- struct{}{} } } drained.Store(true) funcQueue.Close() return case f := <-funcQueue.Out(): f.f() if f.done != nil { f.done <- struct{}{} } case <-eventTick.C: d.pollEvents() for i := 0; i < len(d.windows); i++ { w := d.windows[i].(*window) if w.viewport == nil { continue } if w.viewport.ShouldClose() { d.destroyWindow(w, i) i-- // Trailing windows are moved forward one step. continue } expand := w.shouldExpand fullScreen := w.fullScreen if expand && !fullScreen { w.fitContent() shouldExpand := w.shouldExpand w.shouldExpand = false view := w.viewport if shouldExpand && runtime.GOOS != "js" { view.SetSize(w.shouldWidth, w.shouldHeight) } } } d.animation.TickAnimations() d.drawSingleFrame() } } } func (d *gLDriver) destroyWindow(w *window, index int) { w.visible = false w.viewport.Destroy() w.destroy(d) if index < len(d.windows)-1 { copy(d.windows[index:], d.windows[index+1:]) } d.windows[len(d.windows)-1] = nil d.windows = d.windows[:len(d.windows)-1] if len(d.windows) == 0 { d.Quit() } } func (d *gLDriver) repaintWindow(w *window) bool { canvas := w.canvas freed := false if canvas.EnsureMinSize() { w.shouldExpand = true } freed = canvas.FreeDirtyTextures() > 0 updateGLContext(w) canvas.paint(canvas.Size()) view := w.viewport visible := w.visible if view != nil && visible { view.SwapBuffers() } // mark that we have walked the window and don't // need to walk it again to mark caches alive w.lastWalkedTime = time.Now() return freed } // refreshWindow requests that the specified window be redrawn func refreshWindow(w *window) { w.canvas.SetDirty() } func updateGLContext(w *window) { canvas := w.canvas size := canvas.Size() // w.width and w.height are not correct if we are maximised, so figure from canvas winWidth := float32(scale.ToScreenCoordinate(canvas, size.Width)) * canvas.texScale winHeight := float32(scale.ToScreenCoordinate(canvas, size.Height)) * canvas.texScale canvas.Painter().SetFrameBufferScale(canvas.texScale) canvas.Painter().SetOutputSize(int(winWidth), int(winHeight)) }