adam-gui/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop.go
2024-04-29 19:13:50 +02:00

292 lines
6.4 KiB
Go

package glfw
import (
"runtime"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/app"
"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
}
type drawData struct {
f func()
win *window
done chan struct{} // Zero allocation signalling channel
}
type runFlag struct {
sync.Cond
flag bool
}
// channel for queuing functions on the main thread
var funcQueue = make(chan funcData)
var drawFuncQueue = make(chan drawData)
var run = &runFlag{Cond: sync.Cond{L: &sync.Mutex{}}}
var initOnce = &sync.Once{}
// Arrange that main.main runs on main thread.
func init() {
runtime.LockOSThread()
mainGoroutineID = goroutineID()
}
// force a function f to run on the main thread
func runOnMain(f func()) {
// If we are on main just execute - otherwise add it to the main queue and wait.
// The "running" variable is normally false when we are on the main thread.
run.L.Lock()
running := !run.flag
run.L.Unlock()
if running {
f()
} else {
done := common.DonePool.Get().(chan struct{})
defer common.DonePool.Put(done)
funcQueue <- funcData{f: f, done: done}
<-done
}
}
// force a function f to run on the draw thread
func runOnDraw(w *window, f func()) {
if drawOnMainThread {
runOnMain(func() { w.RunWithContext(f) })
return
}
done := common.DonePool.Get().(chan struct{})
defer common.DonePool.Put(done)
drawFuncQueue <- drawData{f: f, win: w, done: done}
<-done
}
// Preallocate to avoid allocations on every drawSingleFrame.
// Note that the capacity of this slice can only grow,
// but its length will never be longer than the total number of
// window canvases that are dirty on a single frame.
// So its memory impact should be negligible and does not
// need periodic shrinking.
var refreshingCanvases []fyne.Canvas
func (d *gLDriver) drawSingleFrame() {
for _, win := range d.windowList() {
w := win.(*window)
w.viewLock.RLock()
canvas := w.canvas
closing := w.closing
visible := w.visible
w.viewLock.RUnlock()
// 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 closing || !visible || !canvas.CheckDirtyAndClear() {
continue
}
d.repaintWindow(w)
refreshingCanvases = append(refreshingCanvases, canvas)
}
cache.CleanCanvases(refreshingCanvases)
// cleanup refreshingCanvases slice
for i := 0; i < len(refreshingCanvases); i++ {
refreshingCanvases[i] = nil
}
refreshingCanvases = refreshingCanvases[:0]
}
func (d *gLDriver) runGL() {
eventTick := time.NewTicker(time.Second / 60)
run.L.Lock()
run.flag = true
run.L.Unlock()
run.Broadcast()
d.initGLFW()
if d.trayStart != nil {
d.trayStart()
}
fyne.CurrentApp().Lifecycle().(*app.Lifecycle).TriggerStarted()
for {
select {
case <-d.done:
eventTick.Stop()
d.drawDone <- nil // wait for draw thread to stop
d.Terminate()
fyne.CurrentApp().Lifecycle().(*app.Lifecycle).TriggerStopped()
return
case f := <-funcQueue:
f.f()
if f.done != nil {
f.done <- struct{}{}
}
case <-eventTick.C:
d.pollEvents()
windowsToRemove := 0
for _, win := range d.windowList() {
w := win.(*window)
if w.viewport == nil {
continue
}
if w.viewport.ShouldClose() {
windowsToRemove++
continue
}
w.viewLock.RLock()
expand := w.shouldExpand
fullScreen := w.fullScreen
w.viewLock.RUnlock()
if expand && !fullScreen {
w.fitContent()
w.viewLock.Lock()
shouldExpand := w.shouldExpand
w.shouldExpand = false
view := w.viewport
w.viewLock.Unlock()
if shouldExpand {
view.SetSize(w.shouldWidth, w.shouldHeight)
}
}
if drawOnMainThread {
d.drawSingleFrame()
}
}
if windowsToRemove > 0 {
oldWindows := d.windowList()
newWindows := make([]fyne.Window, 0, len(oldWindows)-windowsToRemove)
for _, win := range oldWindows {
w := win.(*window)
if w.viewport == nil {
continue
}
if w.viewport.ShouldClose() {
w.viewLock.Lock()
w.visible = false
v := w.viewport
w.viewLock.Unlock()
// remove window from window list
v.Destroy()
w.destroy(d)
continue
}
newWindows = append(newWindows, win)
}
d.windowLock.Lock()
d.windows = newWindows
d.windowLock.Unlock()
if len(newWindows) == 0 {
d.Quit()
}
}
}
}
}
func (d *gLDriver) repaintWindow(w *window) {
canvas := w.canvas
w.RunWithContext(func() {
if canvas.EnsureMinSize() {
w.viewLock.Lock()
w.shouldExpand = true
w.viewLock.Unlock()
}
canvas.FreeDirtyTextures()
updateGLContext(w)
canvas.paint(canvas.Size())
w.viewLock.RLock()
view := w.viewport
visible := w.visible
w.viewLock.RUnlock()
if view != nil && visible {
view.SwapBuffers()
}
})
}
func (d *gLDriver) startDrawThread() {
settingsChange := make(chan fyne.Settings)
fyne.CurrentApp().Settings().AddChangeListener(settingsChange)
var drawCh <-chan time.Time
if drawOnMainThread {
drawCh = make(chan time.Time) // don't tick when on M1
} else {
drawCh = time.NewTicker(time.Second / 60).C
}
go func() {
runtime.LockOSThread()
for {
select {
case <-d.drawDone:
return
case f := <-drawFuncQueue:
f.win.RunWithContext(f.f)
if f.done != nil {
f.done <- struct{}{}
}
case set := <-settingsChange:
painter.ClearFontCache()
cache.ResetThemeCaches()
app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) {
c, ok := w.Canvas().(*glCanvas)
if !ok {
return
}
c.applyThemeOutOfTreeObjects()
go c.reloadScale()
})
case <-drawCh:
d.drawSingleFrame()
}
}
}()
}
// refreshWindow requests that the specified window be redrawn
func refreshWindow(w *window) {
w.canvas.SetDirty()
}
func updateGLContext(w *window) {
canvas := w.Canvas().(*glCanvas)
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)
w.canvas.Painter().SetOutputSize(int(winWidth), int(winHeight))
}