Skip to content

Commit

Permalink
Merge pull request #5448 from andydotxyz/feature/fynedoasync
Browse files Browse the repository at this point in the history
Creating a version of fyne.Do that does not wait
  • Loading branch information
andydotxyz authored Jan 23, 2025
2 parents 0d08945 + 3daa7ad commit 9afc137
Show file tree
Hide file tree
Showing 13 changed files with 77 additions and 50 deletions.
6 changes: 3 additions & 3 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ type Driver interface {
// Since: 2.5
SetDisableScreenBlanking(bool)

// DoFromGoroutine provides a way to queue a function that is running on a goroutine back to
// the central thread for Fyne updates.
// DoFromGoroutine provides a way to queue a function `fn` that is running on a goroutine back to
// the central thread for Fyne updates, waiting for it to return if `wait` is true.
// The driver provides the implementation normally accessed through [fyne.Do].
// This is required when background tasks want to execute code safely in the graphical context.
//
// Since: 2.6
DoFromGoroutine(func())
DoFromGoroutine(fn func(), wait bool)
}
4 changes: 2 additions & 2 deletions internal/app/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ func (l *Lifecycle) QueueEvent(fn func()) {

// RunEventQueue runs the event queue. This should called inside a go routine.
// This function blocks.
func (l *Lifecycle) RunEventQueue(run func(func())) {
func (l *Lifecycle) RunEventQueue(run func(func(), bool)) {
for fn := range l.eventQueue.Out() {
run(fn)
run(fn, true)
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/app/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestLifecycle(t *testing.T) {

var entered, exited, start, stop, hookedStop, called bool
life.InitEventQueue()
go life.RunEventQueue(func(fn func()) {
go life.RunEventQueue(func(fn func(), _ bool) {
fn()
})
life.QueueEvent(func() { called = true })
Expand Down
4 changes: 2 additions & 2 deletions internal/driver/glfw/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ func toOSIcon(icon []byte) ([]byte, error) {
return buf.Bytes(), nil
}

func (d *gLDriver) DoFromGoroutine(f func()) {
func (d *gLDriver) DoFromGoroutine(f func(), wait bool) {
async.EnsureNotMain(func() {
runOnMain(f)
runOnMainWithWait(f, wait)
})
}

Expand Down
27 changes: 19 additions & 8 deletions internal/driver/glfw/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"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"
Expand All @@ -20,7 +21,7 @@ type funcData struct {
}

// channel for queuing functions on the main thread
var funcQueue = make(chan funcData)
var funcQueue = async.NewUnboundedChan[funcData]()
var running atomic.Bool
var initOnce = &sync.Once{}

Expand All @@ -31,19 +32,27 @@ func init() {

// 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 just execute - otherwise add it to the main queue and wait.
// The "running" variable is normally false when we are on the main thread.
if !running.Load() {
f()
return
}

done := common.DonePool.Get()
defer common.DonePool.Put(done)

funcQueue <- funcData{f: f, done: done}
if wait {
done := common.DonePool.Get()
defer common.DonePool.Put(done)

<-done
funcQueue.In() <- funcData{f: f, done: done}
<-done
} else {
funcQueue.In() <- funcData{f: f}
}
}

// Preallocate to avoid allocations on every drawSingleFrame.
Expand Down Expand Up @@ -108,9 +117,11 @@ func (d *gLDriver) runGL() {
l.QueueEvent(f)
}
return
case f := <-funcQueue:
case f := <-funcQueue.Out():
f.f()
f.done <- struct{}{}
if f.done != nil {
f.done <- struct{}{}
}
case <-eventTick.C:
d.pollEvents()
for i := 0; i < len(d.windows); i++ {
Expand Down
6 changes: 1 addition & 5 deletions internal/driver/glfw/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,11 +867,7 @@ func (w *window) Context() any {

func (w *window) runOnMainWhenCreated(fn func()) {
if w.view() != nil {
if async.IsMainGoroutine() {
fn()
return
}
fyne.Do(fn)
async.EnsureMain(fn)
return
}

Expand Down
30 changes: 16 additions & 14 deletions internal/driver/glfw/window_desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,25 @@ type window struct {
}

func (w *window) SetFullScreen(full bool) {
w.fullScreen = full
if !w.visible {
return
}
w.runOnMainWhenCreated(func() {
w.fullScreen = full
if !w.visible {
return
}

monitor := w.getMonitorForWindow()
mode := monitor.GetVideoMode()
monitor := w.getMonitorForWindow()
mode := monitor.GetVideoMode()

if full {
w.viewport.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate)
} else {
if w.width == 0 && w.height == 0 { // if we were fullscreen on creation...
s := w.canvas.Size().Max(w.canvas.MinSize())
w.width, w.height = w.screenSize(s)
if full {
w.viewport.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate)
} else {
if w.width == 0 && w.height == 0 { // if we were fullscreen on creation...
s := w.canvas.Size().Max(w.canvas.MinSize())
w.width, w.height = w.screenSize(s)
}
w.viewport.SetMonitor(nil, w.xpos, w.ypos, w.width, w.height, 0)
}
w.viewport.SetMonitor(nil, w.xpos, w.ypos, w.width, w.height, 0)
}
})
}

func (w *window) CenterOnScreen() {
Expand Down
2 changes: 1 addition & 1 deletion internal/driver/mobile/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ func (c *canvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tap
c.touchCancelFunc = nil
c.touchLastTapped = nil
c.touchCancelLock.Unlock()
})
}, true)
}

func (c *canvas) windowHeadIsDisplacing() bool {
Expand Down
8 changes: 4 additions & 4 deletions internal/driver/mobile/canvas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func Test_canvas_Focusable(t *testing.T) {
c.tapUp(pos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) {
wid.Tapped(ev)
}, nil, nil, nil)
})
}, true)

waitAndCheck(tapDoubleDelay/time.Millisecond+150, func() {
assert.Equal(t, 1, content.focusedTimes)
Expand All @@ -154,7 +154,7 @@ func Test_canvas_Focusable(t *testing.T) {
c.tapUp(pos, 1, func(wid fyne.Tappable, ev *fyne.PointEvent) {
wid.Tapped(ev)
}, nil, nil, nil)
})
}, true)
waitAndCheck(tapDoubleDelay/time.Millisecond+150, func() {
assert.Equal(t, 1, content.focusedTimes)
assert.Equal(t, 0, content.unfocusedTimes)
Expand All @@ -177,7 +177,7 @@ func Test_canvas_Focusable(t *testing.T) {
c.tapDown(fyne.NewPos(10, 10), 2)
assert.Equal(t, 1, content.focusedTimes)
assert.Equal(t, 1, content.unfocusedTimes)
})
}, true)
}

func Test_canvas_InteractiveArea(t *testing.T) {
Expand Down Expand Up @@ -534,7 +534,7 @@ func waitAndCheck(msWait time.Duration, fn func()) {
fn()

waitForCheck <- struct{}{}
})
}, true)
}()
<-waitForCheck
}
21 changes: 14 additions & 7 deletions internal/driver/mobile/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,24 @@ func init() {
runtime.LockOSThread()
}

func (d *driver) DoFromGoroutine(fn func()) {
func (d *driver) DoFromGoroutine(fn func(), wait bool) {
async.EnsureNotMain(func() {
done := common.DonePool.Get()
defer common.DonePool.Put(done)
var done chan struct{}
if wait {
done = common.DonePool.Get()
defer common.DonePool.Put(done)
}

d.queuedFuncs.In() <- func() {
fn()
done <- struct{}{}
if wait {
done <- struct{}{}
}
}

<-done
if wait {
<-done
}
})
}

Expand Down Expand Up @@ -446,11 +453,11 @@ func (d *driver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) {

d.DoFromGoroutine(func() {
wid.Dragged(ev)
})
}, true)
time.Sleep(time.Millisecond * 16)
}

d.DoFromGoroutine(wid.DragEnd)
d.DoFromGoroutine(wid.DragEnd, true)
}()
})
}
Expand Down
2 changes: 1 addition & 1 deletion test/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func (s *testSettings) apply() {
cache.ResetThemeCaches()
intapp.ApplySettings(s, s.app)
s.app.propertyLock.Unlock()
})
}, false)

s.app.propertyLock.Lock()
s.app.appliedTheme = s.Theme()
Expand Down
3 changes: 2 additions & 1 deletion test/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ func NewDriverWithPainter(painter SoftwarePainter) fyne.Driver {
return &driver{painter: painter}
}

func (d *driver) DoFromGoroutine(f func()) {
// DoFromGoroutine on a test driver ignores the wait flag as our threading is simple
func (d *driver) DoFromGoroutine(f func(), _ bool) {
// Tests all run on a single (but potentially different per-test) thread
async.EnsureNotMain(f)
}
Expand Down
12 changes: 11 additions & 1 deletion thread.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,15 @@ package fyne
//
// Since: 2.6
func Do(fn func()) {
CurrentApp().Driver().DoFromGoroutine(fn)
CurrentApp().Driver().DoFromGoroutine(fn, true)
}

// DoAsync is used to execute a specified function in the main Fyne runtime context without waiting.
// This is required when a background process wishes to adjust graphical elements of a running app.
// Developers should use this only from within goroutines they have created and when the result does not have to
// be waited for.
//
// Since: 2.6
func DoAsync(fn func()) {
CurrentApp().Driver().DoFromGoroutine(fn, false)
}

0 comments on commit 9afc137

Please sign in to comment.