diff --git a/event.go b/event.go index 0f006056fd..8c730d95a6 100644 --- a/event.go +++ b/event.go @@ -35,3 +35,13 @@ type DragEvent struct { PointEvent Dragged Delta } + +type EventFunc interface { + Execute() +} + +type SimpleEventFunc func() + +func (fn SimpleEventFunc) Execute() { + fn() +} diff --git a/internal/async/chan_eventfunc.go b/internal/async/chan_eventfunc.go new file mode 100755 index 0000000000..5300907aac --- /dev/null +++ b/internal/async/chan_eventfunc.go @@ -0,0 +1,102 @@ +//go:build !go1.21 + +// Code generated by go run gen.go; DO NOT EDIT. +package async + +import "fyne.io/fyne/v2" + +// UnboundedEventFuncChan is a channel with an unbounded buffer for caching +// EventFunc objects. A channel must be closed via Close method. +type UnboundedEventFuncChan struct { + in, out chan fyne.EventFunc + close chan struct{} + q []fyne.EventFunc +} + +// NewUnboundedEventFuncChan returns a unbounded channel with unlimited capacity. +func NewUnboundedEventFuncChan() *UnboundedEventFuncChan { + ch := &UnboundedEventFuncChan{ + // The size of EventFunc is less than 16 bytes, we use 16 to fit + // a CPU cache line (L2, 256 Bytes), which may reduce cache misses. + in: make(chan fyne.EventFunc, 16), + out: make(chan fyne.EventFunc, 16), + close: make(chan struct{}), + } + go ch.processing() + return ch +} + +// In returns the send channel of the given channel, which can be used to +// send values to the channel. +func (ch *UnboundedEventFuncChan) In() chan<- fyne.EventFunc { return ch.in } + +// Out returns the receive channel of the given channel, which can be used +// to receive values from the channel. +func (ch *UnboundedEventFuncChan) Out() <-chan fyne.EventFunc { return ch.out } + +// Close closes the channel. +func (ch *UnboundedEventFuncChan) Close() { ch.close <- struct{}{} } + +func (ch *UnboundedEventFuncChan) processing() { + // This is a preallocation of the internal unbounded buffer. + // The size is randomly picked. But if one changes the size, the + // reallocation size at the subsequent for loop should also be + // changed too. Furthermore, there is no memory leak since the + // queue is garbage collected. + ch.q = make([]fyne.EventFunc, 0, 1<<10) + for { + select { + case e, ok := <-ch.in: + if !ok { + // We don't want the input channel be accidentally closed + // via close() instead of Close(). If that happens, it is + // a misuse, do a panic as warning. + panic("async: misuse of unbounded channel, In() was closed") + } + ch.q = append(ch.q, e) + case <-ch.close: + ch.closed() + return + } + for len(ch.q) > 0 { + select { + case ch.out <- ch.q[0]: + ch.q[0] = nil // de-reference earlier to help GC + ch.q = ch.q[1:] + case e, ok := <-ch.in: + if !ok { + // We don't want the input channel be accidentally closed + // via close() instead of Close(). If that happens, it is + // a misuse, do a panic as warning. + panic("async: misuse of unbounded channel, In() was closed") + } + ch.q = append(ch.q, e) + case <-ch.close: + ch.closed() + return + } + } + // If the remaining capacity is too small, we prefer to + // reallocate the entire buffer. + if cap(ch.q) < 1<<5 { + ch.q = make([]fyne.EventFunc, 0, 1<<10) + } + } +} + +func (ch *UnboundedEventFuncChan) closed() { + close(ch.in) + for e := range ch.in { + ch.q = append(ch.q, e) + } + for len(ch.q) > 0 { + select { + case ch.out <- ch.q[0]: + ch.q[0] = nil // de-reference earlier to help GC + ch.q = ch.q[1:] + default: + } + } + close(ch.out) + close(ch.close) +} diff --git a/internal/async/chan_go1.21.go b/internal/async/chan_go1.21.go index 5e912b124d..0ac2dd9e62 100644 --- a/internal/async/chan_go1.21.go +++ b/internal/async/chan_go1.21.go @@ -26,11 +26,17 @@ func NewUnboundedInterfaceChan() *UnboundedInterfaceChan { // CanvasObject objects. A channel must be closed via Close method. type UnboundedCanvasObjectChan = UnboundedChan[fyne.CanvasObject] +type UnboundedEventFuncChan = UnboundedChan[fyne.EventFunc] + // NewUnboundedCanvasObjectChan returns a unbounded channel, of canvas objects, with unlimited capacity. func NewUnboundedCanvasObjectChan() *UnboundedChan[fyne.CanvasObject] { return NewUnboundedChan[fyne.CanvasObject]() } +func NewUnboundedEventFuncChan() *UnboundedChan[fyne.EventFunc] { + return NewUnboundedChan[fyne.EventFunc]() +} + // UnboundedChan is a channel with an unbounded buffer for caching // Func objects. A channel must be closed via Close method. type UnboundedChan[T any] struct { diff --git a/internal/async/gen.go b/internal/async/gen.go index 48d2701e4f..c69da8c571 100644 --- a/internal/async/gen.go +++ b/internal/async/gen.go @@ -43,6 +43,11 @@ func main() { Name: "Interface", Imports: "", }, + "chan_eventfunc.go": { + Type: "fyne.EventFunc", + Name: "EventFunc", + Imports: `import "fyne.io/fyne/v2"`, + }, }, } diff --git a/internal/driver/common/window.go b/internal/driver/common/window.go index a01576819a..cc3c576fe2 100644 --- a/internal/driver/common/window.go +++ b/internal/driver/common/window.go @@ -1,12 +1,13 @@ package common import ( + "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal/async" ) // Window defines common functionality for windows. type Window struct { - eventQueue *async.UnboundedFuncChan + eventQueue *async.UnboundedEventFuncChan } // DestroyEventQueue destroys the event queue. @@ -17,12 +18,12 @@ func (w *Window) DestroyEventQueue() { // InitEventQueue initializes the event queue. func (w *Window) InitEventQueue() { // This channel should be closed when the window is closed. - w.eventQueue = async.NewUnboundedFuncChan() + w.eventQueue = async.NewUnboundedEventFuncChan() } // QueueEvent uses this method to queue up a callback that handles an event. This ensures // user interaction events for a given window are processed in order. -func (w *Window) QueueEvent(fn func()) { +func (w *Window) QueueEvent(fn fyne.EventFunc) { w.eventQueue.In() <- fn } @@ -30,7 +31,7 @@ func (w *Window) QueueEvent(fn func()) { // This function blocks. func (w *Window) RunEventQueue() { for fn := range w.eventQueue.Out() { - fn() + fn.Execute() } } @@ -39,7 +40,7 @@ func (w *Window) WaitForEvents() { done := DonePool.Get() defer DonePool.Put(done) - w.eventQueue.In() <- func() { done <- struct{}{} } + w.eventQueue.In() <- fyne.SimpleEventFunc(func() { done <- struct{}{} }) <-done } diff --git a/internal/driver/glfw/driver.go b/internal/driver/glfw/driver.go index 5f9f254b58..c927f05a5f 100644 --- a/internal/driver/glfw/driver.go +++ b/internal/driver/glfw/driver.go @@ -93,7 +93,7 @@ func (d *gLDriver) Device() fyne.Device { func (d *gLDriver) Quit() { if curWindow != nil { if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnExitedForeground(); f != nil { - curWindow.QueueEvent(f) + curWindow.QueueEvent(fyne.SimpleEventFunc(f)) } curWindow = nil if d.trayStop != nil { diff --git a/internal/driver/glfw/menu_darwin.go b/internal/driver/glfw/menu_darwin.go index c6a4b8bc95..711df5afb7 100644 --- a/internal/driver/glfw/menu_darwin.go +++ b/internal/driver/glfw/menu_darwin.go @@ -246,7 +246,7 @@ func registerCallback(w *window, item *fyne.MenuItem, nextItemID int) int { callbacks = append(callbacks, &menuCallbacks{ action: func() { if item.Action != nil { - w.QueueEvent(item.Action) + w.QueueEvent(fyne.SimpleEventFunc(item.Action)) } }, enabled: func() bool { diff --git a/internal/driver/glfw/window.go b/internal/driver/glfw/window.go index 6639d79ba7..7426b1b94c 100644 --- a/internal/driver/glfw/window.go +++ b/internal/driver/glfw/window.go @@ -210,7 +210,7 @@ func (w *window) Close() { // trigger callbacks - early so window still exists if w.onClosed != nil { - w.QueueEvent(w.onClosed) + w.QueueEvent(fyne.SimpleEventFunc(w.onClosed)) } // set w.closing flag inside draw thread to ensure we can free textures @@ -268,7 +268,7 @@ func (w *window) Canvas() fyne.Canvas { func (w *window) processClosed() { if w.onCloseIntercepted != nil { - w.QueueEvent(w.onCloseIntercepted) + w.QueueEvent(fyne.SimpleEventFunc(w.onCloseIntercepted)) return } @@ -414,7 +414,7 @@ func (w *window) processMouseMoved(xpos float64, ypos float64) { if hovered, ok := obj.(desktop.Hoverable); ok { if hovered == mouseOver { - w.QueueEvent(func() { hovered.MouseMoved(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { hovered.MouseMoved(ev) })) } else { w.mouseOut() w.mouseIn(hovered, ev) @@ -452,7 +452,7 @@ func (w *window) processMouseMoved(xpos float64, ypos float64) { ev.Position = mousePos.Subtract(mouseDraggedOffset).Add(draggedObjDelta) ev.Dragged = fyne.NewDelta(mousePos.X-mouseDragPos.X, mousePos.Y-mouseDragPos.Y) wd := mouseDragged - w.QueueEvent(func() { wd.Dragged(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wd.Dragged(ev) })) } w.mouseLock.Lock() @@ -471,18 +471,18 @@ func (w *window) objIsDragged(obj any) bool { } func (w *window) mouseIn(obj desktop.Hoverable, ev *desktop.MouseEvent) { - w.QueueEvent(func() { + w.QueueEvent(fyne.SimpleEventFunc(func() { if obj != nil { obj.MouseIn(ev) } w.mouseLock.Lock() w.mouseOver = obj w.mouseLock.Unlock() - }) + })) } func (w *window) mouseOut() { - w.QueueEvent(func() { + w.QueueEvent(fyne.SimpleEventFunc(func() { w.mouseLock.RLock() mouseOver := w.mouseOver w.mouseLock.RUnlock() @@ -492,7 +492,7 @@ func (w *window) mouseOut() { w.mouseOver = nil w.mouseLock.Unlock() } - }) + })) } func (w *window) processMouseClicked(button desktop.MouseButton, action action, modifiers fyne.KeyModifier) { @@ -570,7 +570,7 @@ func (w *window) processMouseClicked(button desktop.MouseButton, action action, if action == release && mouseDragged != nil { if mouseDragStarted { - w.QueueEvent(mouseDragged.DragEnd) + w.QueueEvent(fyne.SimpleEventFunc(mouseDragged.DragEnd)) w.mouseLock.Lock() w.mouseDragStarted = false w.mouseLock.Unlock() @@ -593,7 +593,7 @@ func (w *window) processMouseClicked(button desktop.MouseButton, action action, } else if action == release { if co == mousePressed { if button == desktop.MouseButtonSecondary && altTap { - w.QueueEvent(func() { secondary.TappedSecondary(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { secondary.TappedSecondary(ev) })) } } } @@ -608,20 +608,20 @@ func (w *window) processMouseClicked(button desktop.MouseButton, action action, func (w *window) mouseClickedHandleMouseable(mev *desktop.MouseEvent, action action, wid desktop.Mouseable) { mousePos := mev.AbsolutePosition if action == press { - w.QueueEvent(func() { wid.MouseDown(mev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.MouseDown(mev) })) } else if action == release { w.mouseLock.RLock() mouseDragged := w.mouseDragged mouseDraggedOffset := w.mouseDraggedOffset w.mouseLock.RUnlock() if mouseDragged == nil { - w.QueueEvent(func() { wid.MouseUp(mev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.MouseUp(mev) })) } else { if dragged, ok := mouseDragged.(desktop.Mouseable); ok { mev.Position = mousePos.Subtract(mouseDraggedOffset) - w.QueueEvent(func() { dragged.MouseUp(mev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { dragged.MouseUp(mev) })) } else { - w.QueueEvent(func() { wid.MouseUp(mev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.MouseUp(mev) })) } } } @@ -643,7 +643,7 @@ func (w *window) mouseClickedHandleTapDoubleTap(co fyne.CanvasObject, ev *fyne.P } else { w.mouseLock.Lock() if wid, ok := co.(fyne.Tappable); ok && co == w.mousePressed { - w.QueueEvent(func() { wid.Tapped(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.Tapped(ev) })) } w.mousePressed = nil w.mouseLock.Unlock() @@ -664,11 +664,11 @@ func (w *window) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent) { if w.mouseClickCount == 2 && w.mouseLastClick == co { if wid, ok := co.(fyne.DoubleTappable); ok { - w.QueueEvent(func() { wid.DoubleTapped(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.DoubleTapped(ev) })) } } else if co == w.mousePressed { if wid, ok := co.(fyne.Tappable); ok { - w.QueueEvent(func() { wid.Tapped(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.Tapped(ev) })) } } @@ -712,10 +712,10 @@ func (w *window) capturesTab(modifier fyne.KeyModifier) bool { if !captures { switch modifier { case 0: - w.QueueEvent(w.canvas.FocusNext) + w.QueueEvent(fyne.SimpleEventFunc(w.canvas.FocusNext)) return false case fyne.KeyModifierShift: - w.QueueEvent(w.canvas.FocusPrevious) + w.QueueEvent(fyne.SimpleEventFunc(w.canvas.FocusPrevious)) return false } } @@ -745,10 +745,10 @@ func (w *window) processKeyPressed(keyName fyne.KeyName, keyASCII fyne.KeyName, if w.canvas.Focused() != nil { if focused, ok := w.canvas.Focused().(desktop.Keyable); ok { - w.QueueEvent(func() { focused.KeyUp(keyEvent) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { focused.KeyUp(keyEvent) })) } } else if w.canvas.onKeyUp != nil { - w.QueueEvent(func() { w.canvas.onKeyUp(keyEvent) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { w.canvas.onKeyUp(keyEvent) })) } return // ignore key up in other core events case press: @@ -763,10 +763,10 @@ func (w *window) processKeyPressed(keyName fyne.KeyName, keyASCII fyne.KeyName, } if w.canvas.Focused() != nil { if focused, ok := w.canvas.Focused().(desktop.Keyable); ok { - w.QueueEvent(func() { focused.KeyDown(keyEvent) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { focused.KeyDown(keyEvent) })) } } else if w.canvas.onKeyDown != nil { - w.QueueEvent(func() { w.canvas.onKeyDown(keyEvent) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { w.canvas.onKeyDown(keyEvent) })) } default: // key repeat will fall through to TypedKey and TypedShortcut @@ -783,9 +783,9 @@ func (w *window) processKeyPressed(keyName fyne.KeyName, keyASCII fyne.KeyName, // No shortcut detected, pass down to TypedKey focused := w.canvas.Focused() if focused != nil { - w.QueueEvent(func() { focused.TypedKey(keyEvent) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { focused.TypedKey(keyEvent) })) } else if w.canvas.onTypedKey != nil { - w.QueueEvent(func() { w.canvas.onTypedKey(keyEvent) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { w.canvas.onTypedKey(keyEvent) })) } } @@ -795,9 +795,9 @@ func (w *window) processKeyPressed(keyName fyne.KeyName, keyASCII fyne.KeyName, // Characters do not map 1:1 to physical keys, as a key may produce zero, one or more characters. func (w *window) processCharInput(char rune) { if focused := w.canvas.Focused(); focused != nil { - w.QueueEvent(func() { focused.TypedRune(char) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { focused.TypedRune(char) })) } else if w.canvas.onTypedRune != nil { - w.QueueEvent(func() { w.canvas.onTypedRune(char) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { w.canvas.onTypedRune(char) })) } } @@ -805,13 +805,13 @@ func (w *window) processFocused(focus bool) { if focus { if curWindow == nil { if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnEnteredForeground(); f != nil { - w.QueueEvent(f) + w.QueueEvent(fyne.SimpleEventFunc(f)) } } curWindow = w - w.QueueEvent(w.canvas.FocusGained) + w.QueueEvent(fyne.SimpleEventFunc(w.canvas.FocusGained)) } else { - w.QueueEvent(w.canvas.FocusLost) + w.QueueEvent(fyne.SimpleEventFunc(w.canvas.FocusLost)) w.mouseLock.Lock() w.mousePos = fyne.Position{} w.mouseLock.Unlock() @@ -824,7 +824,7 @@ func (w *window) processFocused(focus bool) { curWindow = nil if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnExitedForeground(); f != nil { - w.QueueEvent(f) + w.QueueEvent(fyne.SimpleEventFunc(f)) } }() } @@ -909,11 +909,11 @@ func (w *window) triggersShortcut(localizedKeyName fyne.KeyName, key fyne.KeyNam shouldRunShortcut = shortcut.ShortcutName() == "Copy" } if shouldRunShortcut { - w.QueueEvent(func() { focused.TypedShortcut(shortcut) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { focused.TypedShortcut(shortcut) })) } return shouldRunShortcut } - w.QueueEvent(func() { w.canvas.TypedShortcut(shortcut) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { w.canvas.TypedShortcut(shortcut) })) return true } diff --git a/internal/driver/glfw/window_desktop.go b/internal/driver/glfw/window_desktop.go index fc42f6da6b..67ab6d7797 100644 --- a/internal/driver/glfw/window_desktop.go +++ b/internal/driver/glfw/window_desktop.go @@ -157,9 +157,9 @@ func (w *window) SetOnDropped(dropped func(pos fyne.Position, items []fyne.URI)) uris[i] = storage.NewFileURI(name) } - w.QueueEvent(func() { + w.QueueEvent(fyne.SimpleEventFunc(func() { dropped(w.mousePos, uris) - }) + })) }) }) } diff --git a/internal/driver/mobile/driver.go b/internal/driver/mobile/driver.go index b987b02d32..3773c1f868 100644 --- a/internal/driver/mobile/driver.go +++ b/internal/driver/mobile/driver.go @@ -252,7 +252,7 @@ func (d *driver) handleLifecycle(e lifecycle.Event, w *window) { switch e.Crosses(lifecycle.StageFocused) { case lifecycle.CrossOn: // foregrounding if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnEnteredForeground(); f != nil { - w.QueueEvent(f) + w.QueueEvent(fyne.SimpleEventFunc(f)) } case lifecycle.CrossOff: // will enter background if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { @@ -265,7 +265,7 @@ func (d *driver) handleLifecycle(e lifecycle.Event, w *window) { d.app.Publish() } if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnExitedForeground(); f != nil { - w.QueueEvent(f) + w.QueueEvent(fyne.SimpleEventFunc(f)) } } } @@ -309,7 +309,7 @@ func (d *driver) onStart() { func (d *driver) onStop() { l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) if f := l.OnStopped(); f != nil { - l.QueueEvent(f) + l.QueueEvent(fyne.SimpleEventFunc(f)) } } @@ -387,7 +387,7 @@ func (d *driver) tapMoveCanvas(w *window, x, y float32, tapID touch.Sequence) { pos := fyne.NewPos(tapX, tapY+tapYOffset) w.canvas.tapMove(pos, int(tapID), func(wid fyne.Draggable, ev *fyne.DragEvent) { - w.QueueEvent(func() { wid.Dragged(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.Dragged(ev) })) }) } @@ -397,14 +397,14 @@ func (d *driver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) { pos := fyne.NewPos(tapX, tapY+tapYOffset) w.canvas.tapUp(pos, int(tapID), func(wid fyne.Tappable, ev *fyne.PointEvent) { - w.QueueEvent(func() { wid.Tapped(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.Tapped(ev) })) }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { - w.QueueEvent(func() { wid.TappedSecondary(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.TappedSecondary(ev) })) }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { - w.QueueEvent(func() { wid.DoubleTapped(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.DoubleTapped(ev) })) }, func(wid fyne.Draggable, ev *fyne.DragEvent) { if math.Abs(float64(ev.Dragged.DX)) <= tapMoveEndThreshold && math.Abs(float64(ev.Dragged.DY)) <= tapMoveEndThreshold { - w.QueueEvent(wid.DragEnd) + w.QueueEvent(fyne.SimpleEventFunc(wid.DragEnd)) return } @@ -417,11 +417,11 @@ func (d *driver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) { ev.Dragged.DY *= tapMoveDecay } - w.QueueEvent(func() { wid.Dragged(ev) }) + w.QueueEvent(fyne.SimpleEventFunc(func() { wid.Dragged(ev) })) time.Sleep(time.Millisecond * 16) } - w.QueueEvent(wid.DragEnd) + w.QueueEvent(fyne.SimpleEventFunc(wid.DragEnd)) }() }) } diff --git a/internal/driver/mobile/window.go b/internal/driver/mobile/window.go index b119429cae..241ea414d8 100644 --- a/internal/driver/mobile/window.go +++ b/internal/driver/mobile/window.go @@ -142,7 +142,7 @@ func (w *window) Hide() { func (w *window) tryClose() { if w.onCloseIntercepted != nil { - w.QueueEvent(w.onCloseIntercepted) + w.QueueEvent(fyne.SimpleEventFunc(w.onCloseIntercepted)) return } @@ -169,9 +169,9 @@ func (w *window) Close() { } }) - w.QueueEvent(func() { + w.QueueEvent(fyne.SimpleEventFunc(func() { cache.CleanCanvas(w.canvas) - }) + })) // Call this in a go routine, because this function could be called // inside a button which callback would be queued in this event queue