12 Commits

Author SHA1 Message Date
Elias Naur 8e47316332 app: [Windows] suppress double-click behaviour for custom decorations
Fixes: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-07-15 13:01:41 +08:00
Chris Waldon 55ae5c5b84 app: [Wayland] prevent recursive scroll event processing
This commit zeroes the accumulated scroll distance on the window before invoking the
event delivery code, since the event delivery code is able to call back into the scroll
processing. Prior to this change, the callback could re-processing the scroll delta
while magnifying it by a factor of 10.

Updates: https://todo.sr.ht/~eliasnaur/gio/599
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-07-08 10:12:04 -04:00
Elias Naur 86349775b7 app: ensure Invalidate can be invoked when window is closing
This commit ensures that it is safe to invoke Invalidate() from another goroutine
while a Gio window may be in the process of closing. It can be difficult to prevent
this from happening, as window handles can easily be managed by a type that doesn't
know the exact moment of window close (it might be waiting on the window event loop
to return, but that hasn't happened yet). Without this change, the nil window
driver results in a panic in this situation.

Co-authored-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-07-02 15:06:52 +02:00
Chris Waldon 4a1b4c2642 app: add cross-platform empty view event detection
Custom rendering applications need to be prepared to handle empty view events,
as an empty view event is sent during window shutdown. However, the current
implementation requires applications to write a platform-specific helper
function for each supported platform in order to check whether a received
view event is empty. This commit provides a safe, convenient, cross-platform
method that applications can use to detect this special view event and respond
to it.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-06-28 08:46:36 +02:00
Elias Naur c900d58fb3 app: [macOS] fix ANGLE renderering
Setting CAMetalLayer.presentWithTransaction to YES breaks ANGLE rendering.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 17:48:08 +02:00
Elias Naur 74ccc9c2c7 app: use empty frame when FrameEvent.Frame isn't called
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 16:37:48 +02:00
Elias Naur 3f671afea8 app: ignore Invalidate for Windows not yet created
While here, don't overflow the Windows event queue.

Fixes: https://todo.sr.ht/~eliasnaur/gio/596
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 16:37:48 +02:00
Elias Naur 42357a29e0 app: reset Window when DestroyEvent is received
Fixes: https://todo.sr.ht/~eliasnaur/gio/595
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 10:41:22 +02:00
Elias Naur 8fb6d3da2b io/input: deliver all observed events before deferring the rest
Even when a command defers event delivery to the next frame, the already
observed events must still be delivered in the current frame. This
matters for pointer events that hit more than one event handler.

Fixes: https://todo.sr.ht/~eliasnaur/gio/594
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 15:26:33 +02:00
Elias Naur 706940ff9b io/input: improve documentation, code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 13:23:36 +02:00
Chris Waldon 5542aac772 app: queue system actions before first call to Event()
This commit ensures that attempting to perform a system window action prior
to the first call to Event() does not panic. It adopts a similar strategy to
handling Option() prior to the first call to Event(): make a slice of the arguments
and apply them during window initialization.

Fixes: https://todo.sr.ht/~eliasnaur/gio/593
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-06-20 12:18:42 +02:00
Elias Naur 026d3f9daa .builds: increase file descriptor limit for Android's sdkmanager
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 09:53:17 +02:00
18 changed files with 180 additions and 129 deletions
+2
View File
@@ -80,6 +80,8 @@ tasks:
unzip -q ndk.zip unzip -q ndk.zip
rm ndk.zip rm ndk.zip
mv android-ndk-* ndk-bundle mv android-ndk-* ndk-bundle
# sdkmanager needs lots of file descriptors
ulimit -n 10000
yes|sdkmanager --licenses yes|sdkmanager --licenses
sdkmanager "platforms;android-31" "build-tools;32.0.0" sdkmanager "platforms;android-31" "build-tools;32.0.0"
- test_android: | - test_android: |
+4
View File
@@ -61,6 +61,10 @@ type FrameEvent struct {
type ViewEvent interface { type ViewEvent interface {
implementsViewEvent() implementsViewEvent()
ImplementsEvent() ImplementsEvent()
// Valid will return true when the ViewEvent does contains valid handles.
// If a window receives an invalid ViewEvent, it should deinitialize any
// state referring to handles from a previous ViewEvent.
Valid() bool
} }
// Insets is the space taken up by // Insets is the space taken up by
+1 -1
View File
@@ -7,7 +7,7 @@
#include <OpenGL/OpenGL.h> #include <OpenGL/OpenGL.h>
#include "_cgo_export.h" #include "_cgo_export.h"
CALayer *gio_layerFactory(void) { CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool { @autoreleasepool {
return [CALayer layer]; return [CALayer layer];
} }
+1
View File
@@ -266,6 +266,7 @@ const (
WM_MOUSEWHEEL = 0x020A WM_MOUSEWHEEL = 0x020A
WM_MOUSEHWHEEL = 0x020E WM_MOUSEHWHEEL = 0x020E
WM_NCACTIVATE = 0x0086 WM_NCACTIVATE = 0x0086
WM_NCLBUTTONDBLCLK = 0x00A3
WM_NCHITTEST = 0x0084 WM_NCHITTEST = 0x0084
WM_NCCALCSIZE = 0x0083 WM_NCCALCSIZE = 0x0083
WM_PAINT = 0x000F WM_PAINT = 0x000F
+2 -2
View File
@@ -12,12 +12,12 @@ package app
#import <QuartzCore/CAMetalLayer.h> #import <QuartzCore/CAMetalLayer.h>
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
CALayer *gio_layerFactory(void) { CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool { @autoreleasepool {
CAMetalLayer *l = [CAMetalLayer layer]; CAMetalLayer *l = [CAMetalLayer layer];
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable; l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
l.needsDisplayOnBoundsChange = YES; l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES; l.presentsWithTransaction = presentWithTrans;
return l; return l;
} }
} }
+3 -9
View File
@@ -173,19 +173,13 @@ type context interface {
Unlock() Unlock()
} }
// basicDriver is the subset of [driver] that may be called even after // driver is the interface for the platform implementation
// a window is destroyed. // of a window.
type basicDriver interface { type driver interface {
// Event blocks until an event is available and returns it. // Event blocks until an event is available and returns it.
Event() event.Event Event() event.Event
// Invalidate requests a FrameEvent. // Invalidate requests a FrameEvent.
Invalidate() Invalidate()
}
// driver is the interface for the platform implementation
// of a window.
type driver interface {
basicDriver
// SetAnimating sets the animation flag. When the window is animating, // SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them. // FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool) SetAnimating(anim bool)
+3
View File
@@ -1495,3 +1495,6 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
func (AndroidViewEvent) implementsViewEvent() {} func (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {} func (AndroidViewEvent) ImplementsEvent() {}
func (a AndroidViewEvent) Valid() bool {
return a != (AndroidViewEvent{})
}
+3
View File
@@ -441,3 +441,6 @@ func gio_runMain() {
func (UIKitViewEvent) implementsViewEvent() {} func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent() {} func (UIKitViewEvent) ImplementsEvent() {}
func (u UIKitViewEvent) Valid() bool {
return u != (UIKitViewEvent{})
}
+3
View File
@@ -822,3 +822,6 @@ func translateKey(k string) (key.Name, bool) {
func (JSViewEvent) implementsViewEvent() {} func (JSViewEvent) implementsViewEvent() {}
func (JSViewEvent) ImplementsEvent() {} func (JSViewEvent) ImplementsEvent() {}
func (j JSViewEvent) Valid() bool {
return !(j.Element.IsNull() || j.Element.IsUndefined())
}
+13 -4
View File
@@ -40,7 +40,7 @@ import (
#define MOUSE_SCROLL 4 #define MOUSE_SCROLL 4
__attribute__ ((visibility ("hidden"))) void gio_main(void); __attribute__ ((visibility ("hidden"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); __attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
@@ -925,7 +925,9 @@ func newWindow(win *callbacks, options []Option) {
w.loop = newEventLoop(w.w, w.wakeup) w.loop = newEventLoop(w.w, w.wakeup)
win.SetDriver(w) win.SetDriver(w)
res <- struct{}{} res <- struct{}{}
if err := w.init(); err != nil { var cnf Config
cnf.apply(unit.Metric{}, options)
if err := w.init(cnf.CustomRenderer); err != nil {
w.ProcessEvent(DestroyEvent{Err: err}) w.ProcessEvent(DestroyEvent{Err: err})
return return
} }
@@ -946,8 +948,12 @@ func newWindow(win *callbacks, options []Option) {
<-res <-res
} }
func (w *window) init() error { func (w *window) init(customRenderer bool) error {
view := C.gio_createView() presentWithTrans := 1
if customRenderer {
presentWithTrans = 0
}
view := C.gio_createView(C.int(presentWithTrans))
if view == 0 { if view == 0 {
return errors.New("newOSWindow: failed to create view") return errors.New("newOSWindow: failed to create view")
} }
@@ -1068,3 +1074,6 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
func (AppKitViewEvent) implementsViewEvent() {} func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent() {} func (AppKitViewEvent) ImplementsEvent() {}
func (a AppKitViewEvent) Valid() bool {
return a != (AppKitViewEvent{})
}
+5 -3
View File
@@ -6,7 +6,7 @@
#include "_cgo_export.h" #include "_cgo_export.h"
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void); __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans);
@interface GioAppDelegate : NSObject<NSApplicationDelegate> @interface GioAppDelegate : NSObject<NSApplicationDelegate>
@end @end
@@ -16,6 +16,7 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioView : NSView <CALayerDelegate,NSTextInputClient> @interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property uintptr_t handle; @property uintptr_t handle;
@property BOOL presentWithTrans;
@end @end
@implementation GioWindowDelegate @implementation GioWindowDelegate
@@ -88,7 +89,7 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
gio_onDraw(self.handle); gio_onDraw(self.handle);
} }
- (CALayer *)makeBackingLayer { - (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory(); CALayer *layer = gio_layerFactory(self.presentWithTrans);
layer.delegate = self; layer.delegate = self;
return layer; return layer;
} }
@@ -392,10 +393,11 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
} }
} }
CFTypeRef gio_createView(void) { CFTypeRef gio_createView(int presentWithTrans) {
@autoreleasepool { @autoreleasepool {
NSRect frame = NSMakeRect(0, 0, 0, 0); NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame]; GioView* view = [[GioView alloc] initWithFrame:frame];
view.presentWithTrans = presentWithTrans ? YES : NO;
view.wantsLayer = YES; view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
+6 -24
View File
@@ -9,7 +9,6 @@ import (
"errors" "errors"
"unsafe" "unsafe"
"gioui.org/io/event"
"gioui.org/io/pointer" "gioui.org/io/pointer"
) )
@@ -22,6 +21,9 @@ type X11ViewEvent struct {
func (X11ViewEvent) implementsViewEvent() {} func (X11ViewEvent) implementsViewEvent() {}
func (X11ViewEvent) ImplementsEvent() {} func (X11ViewEvent) ImplementsEvent() {}
func (x X11ViewEvent) Valid() bool {
return x != (X11ViewEvent{})
}
type WaylandViewEvent struct { type WaylandViewEvent struct {
// Display is the *wl_display returned by wl_display_connect. // Display is the *wl_display returned by wl_display_connect.
@@ -32,6 +34,9 @@ type WaylandViewEvent struct {
func (WaylandViewEvent) implementsViewEvent() {} func (WaylandViewEvent) implementsViewEvent() {}
func (WaylandViewEvent) ImplementsEvent() {} func (WaylandViewEvent) ImplementsEvent() {}
func (w WaylandViewEvent) Valid() bool {
return w != (WaylandViewEvent{})
}
func osMain() { func osMain() {
select {} select {}
@@ -57,35 +62,12 @@ func newWindow(window *callbacks, options []Option) {
errFirst = err errFirst = err
} }
} }
window.SetDriver(&dummyDriver{
win: window,
wakeups: make(chan event.Event, 1),
})
if errFirst == nil { if errFirst == nil {
errFirst = errors.New("app: no window driver available") errFirst = errors.New("app: no window driver available")
} }
window.ProcessEvent(DestroyEvent{Err: errFirst}) window.ProcessEvent(DestroyEvent{Err: errFirst})
} }
type dummyDriver struct {
win *callbacks
wakeups chan event.Event
}
func (d *dummyDriver) Event() event.Event {
if e, ok := d.win.nextEvent(); ok {
return e
}
return <-d.wakeups
}
func (d *dummyDriver) Invalidate() {
select {
case d.wakeups <- wakeupEvent{}:
default:
}
}
// xCursor contains mapping from pointer.Cursor to XCursor. // xCursor contains mapping from pointer.Cursor to XCursor.
var xCursor = [...]string{ var xCursor = [...]string{
pointer.CursorDefault: "left_ptr", pointer.CursorDefault: "left_ptr",
+9 -17
View File
@@ -217,10 +217,6 @@ type window struct {
wakeups chan struct{} wakeups chan struct{}
closing bool closing bool
// invMu avoids the race between the destruction of disp and
// Invalidate waking it up.
invMu sync.Mutex
} }
type poller struct { type poller struct {
@@ -1369,10 +1365,8 @@ func (w *window) close(err error) {
w.ProcessEvent(WaylandViewEvent{}) w.ProcessEvent(WaylandViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err}) w.ProcessEvent(DestroyEvent{Err: err})
w.destroy() w.destroy()
w.invMu.Lock()
w.disp.destroy() w.disp.destroy()
w.disp = nil w.disp = nil
w.invMu.Unlock()
} }
func (w *window) dispatch() { func (w *window) dispatch() {
@@ -1416,11 +1410,7 @@ func (w *window) Invalidate() {
default: default:
return return
} }
w.invMu.Lock() w.disp.wakeup()
defer w.invMu.Unlock()
if w.disp != nil {
w.disp.wakeup()
}
} }
func (w *window) Run(f func()) { func (w *window) Run(f func()) {
@@ -1643,6 +1633,14 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) { if total == (f32.Point{}) {
return return
} }
if w.scroll.steps == (image.Point{}) {
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
// Zero scroll distance prior to calling ProcessEvent, otherwise we may recursively
// re-process the scroll distance.
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
w.ProcessEvent(pointer.Event{ w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll, Kind: pointer.Scroll,
Source: pointer.Mouse, Source: pointer.Mouse,
@@ -1652,12 +1650,6 @@ func (w *window) flushScroll() {
Time: w.scroll.time, Time: w.scroll.time,
Modifiers: w.disp.xkb.Modifiers(), Modifiers: w.disp.xkb.Modifiers(),
}) })
if w.scroll.steps == (image.Point{}) {
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
} }
func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
+9 -12
View File
@@ -54,9 +54,6 @@ type window struct {
borderSize image.Point borderSize image.Point
config Config config Config
loop *eventLoop loop *eventLoop
// invMu avoids the race between destroying the window and Invalidate.
invMu sync.Mutex
} }
const _WM_WAKEUP = windows.WM_USER + iota const _WM_WAKEUP = windows.WM_USER + iota
@@ -304,10 +301,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
windows.ReleaseDC(w.hdc) windows.ReleaseDC(w.hdc)
w.hdc = 0 w.hdc = 0
} }
w.invMu.Lock()
// The system destroys the HWND for us. // The system destroys the HWND for us.
w.hwnd = 0 w.hwnd = 0
w.invMu.Unlock()
windows.PostQuitMessage(0) windows.PostQuitMessage(0)
case windows.WM_NCCALCSIZE: case windows.WM_NCCALCSIZE:
if w.config.Decorated { if w.config.Decorated {
@@ -331,6 +326,12 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
mi := windows.GetMonitorInfo(w.hwnd) mi := windows.GetMonitorInfo(w.hwnd)
szp.Rgrc[0] = mi.WorkArea szp.Rgrc[0] = mi.WorkArea
return 0 return 0
case windows.WM_NCLBUTTONDBLCLK:
if !w.config.Decorated {
// Override Windows behaviour when we
// draw decorations.
return 0
}
case windows.WM_PAINT: case windows.WM_PAINT:
w.draw(true) w.draw(true)
case windows.WM_SIZE: case windows.WM_SIZE:
@@ -620,13 +621,6 @@ func (w *window) Frame(frame *op.Ops) {
} }
func (w *window) wakeup() { func (w *window) wakeup() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.hwnd == 0 {
w.loop.Wakeup()
w.loop.FlushEvents()
return
}
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil { if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err) panic(err)
} }
@@ -993,3 +987,6 @@ func configForDPI(dpi int) unit.Metric {
func (Win32ViewEvent) implementsViewEvent() {} func (Win32ViewEvent) implementsViewEvent() {}
func (Win32ViewEvent) ImplementsEvent() {} func (Win32ViewEvent) ImplementsEvent() {}
func (w Win32ViewEvent) Valid() bool {
return w != (Win32ViewEvent{})
}
-10
View File
@@ -113,9 +113,6 @@ type x11Window struct {
wakeups chan struct{} wakeups chan struct{}
handler x11EventHandler handler x11EventHandler
buf [100]byte buf [100]byte
// invMy avoids the race between destroy and Invalidate.
invMu sync.Mutex
} }
var ( var (
@@ -416,11 +413,6 @@ func (w *x11Window) Invalidate() {
case w.wakeups <- struct{}{}: case w.wakeups <- struct{}{}:
default: default:
} }
w.invMu.Lock()
defer w.invMu.Unlock()
if w.x == nil {
return
}
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN { if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err)) panic(fmt.Errorf("failed to write to pipe: %v", err))
} }
@@ -509,8 +501,6 @@ func (w *x11Window) dispatch() {
} }
func (w *x11Window) destroy() { func (w *x11Window) destroy() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.notify.write != 0 { if w.notify.write != 0 {
syscall.Close(w.notify.write) syscall.Close(w.notify.write)
w.notify.write = 0 w.notify.write = 0
+63 -22
View File
@@ -7,8 +7,8 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"reflect"
"runtime" "runtime"
"sync"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -41,7 +41,8 @@ type Option func(unit.Metric, *Config)
// //
// More than one Window is not supported on iOS, Android, WebAssembly. // More than one Window is not supported on iOS, Android, WebAssembly.
type Window struct { type Window struct {
initialOpts []Option initialOpts []Option
initialActions []system.Action
ctx context ctx context
gpu gpu.GPU gpu gpu.GPU
@@ -88,8 +89,11 @@ type Window struct {
} }
imeState editorState imeState editorState
driver driver driver driver
// basic is the driver interface that is needed even after the window is gone.
basic basicDriver // invMu protects mayInvalidate.
invMu sync.Mutex
mayInvalidate bool
// coalesced tracks the most recent events waiting to be delivered // coalesced tracks the most recent events waiting to be delivered
// to the client. // to the client.
coalesced eventSummary coalesced eventSummary
@@ -103,11 +107,12 @@ type Window struct {
} }
type eventSummary struct { type eventSummary struct {
wakeup bool wakeup bool
cfg *ConfigEvent cfg *ConfigEvent
view *ViewEvent view *ViewEvent
frame *frameEvent frame *frameEvent
destroy *DestroyEvent framePending bool
destroy *DestroyEvent
} }
type callbacks struct { type callbacks struct {
@@ -214,6 +219,7 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
} }
func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) { func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
w.coalesced.framePending = false
wrapper := &w.decorations.Ops wrapper := &w.decorations.Ops
off := op.Offset(w.lastFrame.off).Push(wrapper) off := op.Offset(w.lastFrame.off).Push(wrapper)
ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal)) ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
@@ -271,8 +277,11 @@ func (w *Window) updateState() {
// //
// Invalidate is safe for concurrent use. // Invalidate is safe for concurrent use.
func (w *Window) Invalidate() { func (w *Window) Invalidate() {
if w.basic != nil { w.invMu.Lock()
w.basic.Invalidate() defer w.invMu.Unlock()
if w.mayInvalidate {
w.mayInvalidate = false
w.driver.Invalidate()
} }
} }
@@ -282,7 +291,7 @@ func (w *Window) Option(opts ...Option) {
if len(opts) == 0 { if len(opts) == 0 {
return return
} }
if w.basic == nil { if w.driver == nil {
w.initialOpts = append(w.initialOpts, opts...) w.initialOpts = append(w.initialOpts, opts...)
return return
} }
@@ -377,11 +386,11 @@ func (w *Window) setNextFrame(at time.Time) {
} }
} }
func (c *callbacks) SetDriver(d basicDriver) { func (c *callbacks) SetDriver(d driver) {
c.w.basic = d if d == nil {
if d, ok := d.(driver); ok { panic("nil driver")
c.w.driver = d
} }
c.w.driver = d
} }
func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) { func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) {
@@ -548,10 +557,20 @@ func (c *callbacks) Invalidate() {
} }
func (c *callbacks) nextEvent() (event.Event, bool) { func (c *callbacks) nextEvent() (event.Event, bool) {
s := &c.w.coalesced return c.w.nextEvent()
// Every event counts as a wakeup. }
defer func() { s.wakeup = false }()
func (w *Window) nextEvent() (event.Event, bool) {
s := &w.coalesced
defer func() {
// Every event counts as a wakeup.
s.wakeup = false
}()
switch { switch {
case s.framePending:
// If the user didn't call FrameEvent.Event, process
// an empty frame.
w.processFrame(new(op.Ops), nil)
case s.view != nil: case s.view != nil:
e := *s.view e := *s.view
s.view = nil s.view = nil
@@ -568,10 +587,14 @@ func (c *callbacks) nextEvent() (event.Event, bool) {
case s.frame != nil: case s.frame != nil:
e := *s.frame e := *s.frame
s.frame = nil s.frame = nil
s.framePending = true
return e.FrameEvent, true return e.FrameEvent, true
case s.wakeup: case s.wakeup:
return wakeupEvent{}, true return wakeupEvent{}, true
} }
w.invMu.Lock()
defer w.invMu.Unlock()
w.mayInvalidate = w.driver != nil
return nil, false return nil, false
} }
@@ -615,6 +638,9 @@ func (w *Window) processEvent(e event.Event) bool {
w.coalesced.frame = &e2 w.coalesced.frame = &e2
case DestroyEvent: case DestroyEvent:
w.destroyGPU() w.destroyGPU()
w.invMu.Lock()
w.mayInvalidate = false
w.invMu.Unlock()
w.driver = nil w.driver = nil
if q := w.timer.quit; q != nil { if q := w.timer.quit; q != nil {
q <- struct{}{} q <- struct{}{}
@@ -622,7 +648,7 @@ func (w *Window) processEvent(e event.Event) bool {
} }
w.coalesced.destroy = &e2 w.coalesced.destroy = &e2
case ViewEvent: case ViewEvent:
if reflect.ValueOf(e2).IsZero() && w.gpu != nil { if !e2.Valid() && w.gpu != nil {
w.ctx.Lock() w.ctx.Lock()
w.gpu.Release() w.gpu.Release()
w.gpu = nil w.gpu = nil
@@ -686,10 +712,17 @@ func (w *Window) processEvent(e event.Event) bool {
// [FrameEvent], or until [Invalidate] is called. The window is created // [FrameEvent], or until [Invalidate] is called. The window is created
// and shown the first time Event is called. // and shown the first time Event is called.
func (w *Window) Event() event.Event { func (w *Window) Event() event.Event {
if w.basic == nil { if w.driver == nil {
w.init() w.init()
} }
return w.basic.Event() if w.driver == nil {
e, ok := w.nextEvent()
if !ok {
panic("window initializion failed without a DestroyEvent")
}
return e
}
return w.driver.Event()
} }
func (w *Window) init() { func (w *Window) init() {
@@ -727,6 +760,10 @@ func (w *Window) init() {
w.imeState.compose = key.Range{Start: -1, End: -1} w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode) w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
newWindow(&callbacks{w}, options) newWindow(&callbacks{w}, options)
for _, acts := range w.initialActions {
w.Perform(acts)
}
w.initialActions = nil
} }
func (w *Window) updateCursor() { func (w *Window) updateCursor() {
@@ -826,6 +863,10 @@ func (w *Window) Perform(actions system.Action) {
if acts == 0 { if acts == 0 {
return return
} }
if w.driver == nil {
w.initialActions = append(w.initialActions, acts)
return
}
w.Run(func() { w.Run(func() {
w.driver.Perform(actions) w.driver.Perform(actions)
}) })
+27
View File
@@ -1086,6 +1086,33 @@ func TestPassCursor(t *testing.T) {
} }
} }
func TestPartialEvent(t *testing.T) {
var ops op.Ops
var r Router
rect := clip.Rect(image.Rect(0, 0, 100, 100))
background := rect.Push(&ops)
event.Op(&ops, 1)
background.Pop()
overlayPass := pointer.PassOp{}.Push(&ops)
overlay := rect.Push(&ops)
event.Op(&ops, 2)
overlay.Pop()
overlayPass.Pop()
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 1, Kinds: pointer.Press}))
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 2, Kinds: pointer.Press}))
r.Frame(&ops)
r.Queue(pointer.Event{
Kind: pointer.Press,
})
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 1, Kinds: pointer.Press}, key.FocusFilter{Target: 1}),
key.FocusEvent{}, pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Priority: pointer.Shared})
r.Source().Execute(key.FocusCmd{Tag: 1})
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 2, Kinds: pointer.Press}),
pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Priority: pointer.Foremost})
}
// offer satisfies io.ReadCloser for use in data transfers. // offer satisfies io.ReadCloser for use in data transfers.
type offer struct { type offer struct {
data string data string
+26 -25
View File
@@ -274,28 +274,29 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
} }
} }
} }
if !q.deferring { for i := range q.changes {
for i := range q.changes { if q.deferring && i > 0 {
change := &q.changes[i] break
for j, evt := range change.events { }
match := false change := &q.changes[i]
switch e := evt.event.(type) { for j, evt := range change.events {
case key.Event: match := false
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false) switch e := evt.event.(type) {
default: case key.Event:
for _, tf := range q.scratchFilters { match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
if evt.tag == tf.tag && tf.filter.Matches(evt.event) { default:
match = true for _, tf := range q.scratchFilters {
break if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
} match = true
break
} }
} }
if match { }
change.events = append(change.events[:j], change.events[j+1:]...) if match {
// Fast forward state to last matched. change.events = append(change.events[:j], change.events[j+1:]...)
q.collapseState(i) // Fast forward state to last matched.
return evt.event, true q.collapseState(i)
} return evt.event, true
} }
} }
} }
@@ -313,15 +314,15 @@ func (q *Router) collapseState(idx int) {
} }
first := &q.changes[0] first := &q.changes[0]
first.state = q.changes[idx].state first.state = q.changes[idx].state
for i := 1; i <= idx; i++ { for _, ch := range q.changes[1 : idx+1] {
first.events = append(first.events, q.changes[i].events...) first.events = append(first.events, ch.events...)
} }
q.changes = append(q.changes[:1], q.changes[idx+1:]...) q.changes = append(q.changes[:1], q.changes[idx+1:]...)
} }
// Frame replaces the declared handlers from the supplied // Frame completes the current frame and starts a new with the
// operation list. The text input state, wakeup time and whether // handlers from the frame argument. Remaining events are discarded,
// there are active profile handlers is also saved. // unless they were deferred by a command.
func (q *Router) Frame(frame *op.Ops) { func (q *Router) Frame(frame *op.Ops) {
var remaining []event.Event var remaining []event.Event
if n := len(q.changes); n > 0 { if n := len(q.changes); n > 0 {