app: prepare Window for removal of Main and asynchronous FrameEvents

This is mostly a refactor, but there are two user-visible effects:
- Window.NextEvent may be called even after DestroyEvent is returned.
- Window.Invalidate always wakes up a blocking NextEvent, even when a
FrameEvent cannot be generated.

As a nice side-effect, X11, Wayland and Wasm no longer require separate
goroutines for their window loops.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-12-03 15:08:56 -06:00
parent 5cda660e6e
commit 6879a30582
15 changed files with 1081 additions and 779 deletions
+1 -1
View File
@@ -65,8 +65,8 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
private final InputMethodManager imm;
private final float scrollXScale;
private final float scrollYScale;
private final AccessibilityManager accessManager;
private int keyboardHint;
private AccessibilityManager accessManager;
private long nhandle;
+11 -1
View File
@@ -69,7 +69,17 @@ func (c *wlContext) Refresh() error {
}
c.eglWin = eglWin
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
return c.Context.CreateSurface(eglSurf, width, height)
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
defer c.Context.ReleaseCurrent()
// We're in charge of the frame callbacks, don't let eglSwapBuffers
// wait for callbacks that may never arrive.
c.Context.EnableVSync(false)
return nil
}
func (c *wlContext) Lock() error {
+1 -1
View File
@@ -46,8 +46,8 @@ func (c *x11Context) Refresh() error {
if err := c.Context.MakeCurrent(); err != nil {
return err
}
defer c.Context.ReleaseCurrent()
c.Context.EnableVSync(true)
c.Context.ReleaseCurrent()
return nil
}
+167 -19
View File
@@ -7,7 +7,9 @@ import (
"image"
"image/color"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/op"
"gioui.org/gpu"
"gioui.org/io/pointer"
@@ -131,6 +133,28 @@ func (o Orientation) String() string {
return ""
}
// eventLoop implements the functionality required for drivers where
// window event loops must run on a separate thread.
type eventLoop struct {
win *callbacks
// wakeup is the callback to wake up the event loop.
wakeup func()
// driverFuncs is a channel of functions to run the next
// time the window loop waits for events.
driverFuncs chan func()
// invalidates is notified when an invalidate is requested by the client.
invalidates chan struct{}
// immediateInvalidates is an optimistic invalidates that doesn't require a wakeup.
immediateInvalidates chan struct{}
// events is where the platform backend delivers events bound for the
// user program.
events chan event.Event
frames chan *op.Ops
frameAck chan struct{}
// delivering avoids re-entrant event delivery.
delivering bool
}
type frameEvent struct {
FrameEvent
@@ -147,9 +171,19 @@ type context interface {
Unlock()
}
// Driver is the interface for the platform implementation
// basicDriver is the subset of [driver] that may be called even after
// a window is destroyed.
type basicDriver interface {
// Event blocks until an even is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
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,
// FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool)
@@ -166,17 +200,23 @@ type driver interface {
// SetCursor updates the current cursor to name.
SetCursor(cursor pointer.Cursor)
// Wakeup wakes up the event loop and sends a WakeupEvent.
Wakeup()
// Wakeup()
// Perform actions on the window.
Perform(system.Action)
// EditorStateChanged notifies the driver that the editor state changed.
EditorStateChanged(old, new editorState)
// Run a function on the window thread.
Run(f func())
// Frame receives a frame.
Frame(frame *op.Ops)
// ProcessEvent processes an event.
ProcessEvent(e event.Event)
}
type windowRendezvous struct {
in chan windowAndConfig
out chan windowAndConfig
errs chan error
in chan windowAndConfig
out chan windowAndConfig
windows chan struct{}
}
type windowAndConfig struct {
@@ -186,32 +226,137 @@ type windowAndConfig struct {
func newWindowRendezvous() *windowRendezvous {
wr := &windowRendezvous{
in: make(chan windowAndConfig),
out: make(chan windowAndConfig),
errs: make(chan error),
in: make(chan windowAndConfig),
out: make(chan windowAndConfig),
windows: make(chan struct{}),
}
go func() {
var main windowAndConfig
in := wr.in
var window windowAndConfig
var out chan windowAndConfig
for {
select {
case w := <-wr.in:
var err error
if main.window != nil {
err = errors.New("multiple windows are not supported")
}
wr.errs <- err
main = w
case w := <-in:
window = w
out = wr.out
case out <- main:
case out <- window:
}
}
}()
return wr
}
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
func newEventLoop(w *callbacks, wakeup func()) *eventLoop {
return &eventLoop{
win: w,
wakeup: wakeup,
events: make(chan event.Event),
invalidates: make(chan struct{}, 1),
immediateInvalidates: make(chan struct{}),
frames: make(chan *op.Ops),
frameAck: make(chan struct{}),
driverFuncs: make(chan func(), 1),
}
}
// Frame receives a frame and waits for its processing. It is called by
// the client goroutine.
func (e *eventLoop) Frame(frame *op.Ops) {
e.frames <- frame
<-e.frameAck
}
// Event returns the next available event. It is called by the client
// goroutine.
func (e *eventLoop) Event() event.Event {
for {
evt := <-e.events
// Receiving a flushEvent indicates to the platform backend that
// all previous events have been processed by the user program.
if _, ok := evt.(flushEvent); ok {
continue
}
return evt
}
}
// Invalidate requests invalidation of the window. It is called by the client
// goroutine.
func (e *eventLoop) Invalidate() {
select {
case e.immediateInvalidates <- struct{}{}:
// The event loop was waiting, no need for a wakeup.
case e.invalidates <- struct{}{}:
// The event loop is sleeping, wake it up.
e.wakeup()
default:
// A redraw is pending.
}
}
// Run f in the window loop thread. It is called by the client goroutine.
func (e *eventLoop) Run(f func()) {
e.driverFuncs <- f
e.wakeup()
}
// FlushEvents delivers pending events to the client.
func (e *eventLoop) FlushEvents() {
if e.delivering {
return
}
e.delivering = true
defer func() { e.delivering = false }()
for {
evt, ok := e.win.nextEvent()
if !ok {
break
}
e.deliverEvent(evt)
}
}
func (e *eventLoop) deliverEvent(evt event.Event) {
var frames <-chan *op.Ops
for {
select {
case f := <-e.driverFuncs:
f()
case frame := <-frames:
// The client called FrameEvent.Frame.
frames = nil
e.win.ProcessFrame(frame, e.frameAck)
case e.events <- evt:
switch evt.(type) {
case flushEvent, DestroyEvent:
// DestroyEvents are not flushed.
return
case FrameEvent:
frames = e.frames
}
evt = theFlushEvent
case <-e.invalidates:
e.win.Invalidate()
case <-e.immediateInvalidates:
e.win.Invalidate()
}
}
}
func (e *eventLoop) Wakeup() {
for {
select {
case f := <-e.driverFuncs:
f()
case <-e.invalidates:
e.win.Invalidate()
case <-e.immediateInvalidates:
e.win.Invalidate()
default:
return
}
}
}
func walkActions(actions system.Action, do func(system.Action)) {
for a := system.Action(1); actions != 0; a <<= 1 {
@@ -221,3 +366,6 @@ func walkActions(actions system.Action, do func(system.Action)) {
}
}
}
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
+96 -52
View File
@@ -137,8 +137,10 @@ import (
"unsafe"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer"
@@ -150,6 +152,7 @@ import (
type window struct {
callbacks *callbacks
loop *eventLoop
view C.jobject
handle cgo.Handle
@@ -162,8 +165,9 @@ type window struct {
started bool
animating bool
win *C.ANativeWindow
config Config
win *C.ANativeWindow
config Config
inputHint key.InputHint
semantic struct {
hoverID input.SemanticID
@@ -487,24 +491,30 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
})
view = C.jni_NewGlobalRef(env, view)
wopts := <-mainWindow.out
var cnf Config
w, ok := windows[wopts.window]
if !ok {
w = &window{
callbacks: wopts.window,
}
w.loop = newEventLoop(w.callbacks, w.wakeup)
w.callbacks.SetDriver(w)
cnf.apply(unit.Metric{}, wopts.options)
windows[wopts.window] = w
} else {
cnf = w.config
}
mainWindow.windows <- struct{}{}
if w.view != 0 {
w.detach(env)
}
w.view = view
w.handle = cgo.NewHandle(w)
w.callbacks.SetDriver(w)
w.loadConfig(env, class)
w.Configure(wopts.options)
w.SetInputHint(key.HintAny)
w.setConfig(env, cnf)
w.SetInputHint(w.inputHint)
w.setStage(StagePaused)
w.callbacks.Event(ViewEvent{View: uintptr(view)})
w.processEvent(ViewEvent{View: uintptr(view)})
return C.jlong(w.handle)
}
@@ -579,7 +589,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
//export Java_org_gioui_GioView_onBack
func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean {
w := cgo.Handle(view).Value().(*window)
if w.callbacks.Event(key.Event{Name: key.NameBack}) {
if w.processEvent(key.Event{Name: key.NameBack}) {
return C.JNI_TRUE
}
return C.JNI_FALSE
@@ -588,7 +598,7 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
//export Java_org_gioui_GioView_onFocusChange
func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) {
w := cgo.Handle(view).Value().(*window)
w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
w.processEvent(key.FocusEvent{Focus: focus == C.JNI_TRUE})
}
//export Java_org_gioui_GioView_onWindowInsets
@@ -663,6 +673,34 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
}
}
func (w *window) ProcessEvent(e event.Event) {
w.processEvent(e)
}
func (w *window) processEvent(e event.Event) bool {
if !w.callbacks.ProcessEvent(e) {
return false
}
w.loop.FlushEvents()
return true
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode, off image.Point, info C.jobject) error {
for _, ch := range sem.Children {
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
@@ -767,8 +805,7 @@ func (w *window) semIDFor(virtID C.jint) input.SemanticID {
func (w *window) detach(env *C.JNIEnv) {
callVoidMethod(env, w.view, gioView.unregister)
w.callbacks.Event(ViewEvent{})
w.callbacks.SetDriver(nil)
w.processEvent(ViewEvent{})
w.handle.Delete()
C.jni_DeleteGlobalRef(env, w.view)
w.view = 0
@@ -788,7 +825,7 @@ func (w *window) setStage(stage Stage) {
return
}
w.stage = stage
w.callbacks.Event(StageEvent{stage})
w.processEvent(StageEvent{stage})
}
func (w *window) setVisual(visID int) error {
@@ -830,7 +867,7 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win)))
if size != w.config.Size {
w.config.Size = size
w.callbacks.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
if size.X == 0 || size.Y == 0 {
return
@@ -844,7 +881,7 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
Left: unit.Dp(w.insets.left) * dppp,
Right: unit.Dp(w.insets.right) * dppp,
}
w.callbacks.Event(frameEvent{
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
@@ -944,7 +981,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
if pressed == C.JNI_TRUE {
state = key.Press
}
w.callbacks.Event(key.Event{Name: n, State: state})
w.processEvent(key.Event{Name: n, State: state})
}
if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224).
w.callbacks.EditorInsert(string(rune(r)))
@@ -994,7 +1031,7 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
default:
return
}
w.callbacks.Event(pointer.Event{
w.processEvent(pointer.Event{
Kind: kind,
Source: src,
Buttons: btns,
@@ -1146,6 +1183,8 @@ func (w *window) ShowTextInput(show bool) {
}
func (w *window) SetInputHint(mode key.InputHint) {
w.inputHint = mode
// Constants defined at https://developer.android.com/reference/android/text/InputType.
const (
TYPE_NULL = 0
@@ -1292,9 +1331,9 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
func osMain() {
}
func newWindow(window *callbacks, options []Option) error {
func newWindow(window *callbacks, options []Option) {
mainWindow.in <- windowAndConfig{window, options}
return <-mainWindow.errs
<-mainWindow.windows
}
func (w *window) WriteClipboard(mime string, s []byte) {
@@ -1313,7 +1352,7 @@ func (w *window) ReadClipboard() {
return
}
content := goString(env, C.jstring(c))
w.callbacks.Event(transfer.DataEvent{
w.processEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
@@ -1323,42 +1362,46 @@ func (w *window) ReadClipboard() {
}
func (w *window) Configure(options []Option) {
cnf := w.config
cnf.apply(unit.Metric{}, options)
runInJVM(javaVM(), func(env *C.JNIEnv) {
prev := w.config
cnf := w.config
cnf.apply(unit.Metric{}, options)
// Decorations are never disabled.
cnf.Decorated = true
if prev.Orientation != cnf.Orientation {
w.config.Orientation = cnf.Orientation
setOrientation(env, w.view, cnf.Orientation)
}
if prev.NavigationColor != cnf.NavigationColor {
w.config.NavigationColor = cnf.NavigationColor
setNavigationColor(env, w.view, cnf.NavigationColor)
}
if prev.StatusColor != cnf.StatusColor {
w.config.StatusColor = cnf.StatusColor
setStatusColor(env, w.view, cnf.StatusColor)
}
if prev.Mode != cnf.Mode {
switch cnf.Mode {
case Fullscreen:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
w.config.Mode = Fullscreen
case Windowed:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
w.config.Mode = Windowed
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.callbacks.Event(ConfigEvent{Config: w.config})
w.setConfig(env, cnf)
})
}
func (w *window) setConfig(env *C.JNIEnv, cnf Config) {
prev := w.config
// Decorations are never disabled.
cnf.Decorated = true
if prev.Orientation != cnf.Orientation {
w.config.Orientation = cnf.Orientation
setOrientation(env, w.view, cnf.Orientation)
}
if prev.NavigationColor != cnf.NavigationColor {
w.config.NavigationColor = cnf.NavigationColor
setNavigationColor(env, w.view, cnf.NavigationColor)
}
if prev.StatusColor != cnf.StatusColor {
w.config.StatusColor = cnf.StatusColor
setStatusColor(env, w.view, cnf.StatusColor)
}
if prev.Mode != cnf.Mode {
switch cnf.Mode {
case Fullscreen:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
w.config.Mode = Fullscreen
case Windowed:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
w.config.Mode = Windowed
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.processEvent(ConfigEvent{Config: w.config})
}
func (w *window) Perform(system.Action) {}
func (w *window) SetCursor(cursor pointer.Cursor) {
@@ -1367,9 +1410,10 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
})
}
func (w *window) Wakeup() {
func (w *window) wakeup() {
runOnMain(func(env *C.JNIEnv) {
w.callbacks.Event(wakeupEvent{})
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
+3 -2
View File
@@ -260,8 +260,9 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
return to
}
func (w *window) Wakeup() {
func (w *window) wakeup() {
runOnMain(func() {
w.w.Event(wakeupEvent{})
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
+61 -31
View File
@@ -81,10 +81,12 @@ import (
"unsafe"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
@@ -97,10 +99,11 @@ type window struct {
view C.CFTypeRef
w *callbacks
displayLink *displayLink
loop *eventLoop
visible bool
cursor pointer.Cursor
config Config
hidden bool
cursor pointer.Cursor
config Config
pointerMap []C.CFTypeRef
}
@@ -116,23 +119,26 @@ func init() {
//export onCreate
func onCreate(view, controller C.CFTypeRef) {
wopts := <-mainWindow.out
w := &window{
view: view,
w: wopts.window,
}
w.loop = newEventLoop(w.w, w.wakeup)
w.w.SetDriver(w)
mainWindow.windows <- struct{}{}
dl, err := newDisplayLink(func() {
w.draw(false)
})
if err != nil {
panic(err)
w.w.ProcessEvent(DestroyEvent{Err: err})
return
}
w.displayLink = dl
wopts := <-mainWindow.out
w.w = wopts.window
w.w.SetDriver(w)
views[view] = w
w.Configure(wopts.options)
w.w.Event(StageEvent{Stage: StagePaused})
w.w.Event(ViewEvent{ViewController: uintptr(controller)})
w.ProcessEvent(StageEvent{Stage: StageRunning})
w.ProcessEvent(ViewEvent{ViewController: uintptr(controller)})
}
//export gio_onDraw
@@ -142,22 +148,20 @@ func gio_onDraw(view C.CFTypeRef) {
}
func (w *window) draw(sync bool) {
if w.hidden {
return
}
params := C.viewDrawParams(w.view)
if params.width == 0 || params.height == 0 {
return
}
wasVisible := w.visible
w.visible = true
if !wasVisible {
w.w.Event(StageEvent{Stage: StageRunning})
}
const inchPrDp = 1.0 / 163
m := unit.Metric{
PxPerDp: float32(params.dpi) * inchPrDp,
PxPerSp: float32(params.sdpi) * inchPrDp,
}
dppp := unit.Dp(1. / m.PxPerDp)
w.w.Event(frameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: image.Point{
@@ -179,24 +183,33 @@ func (w *window) draw(sync bool) {
//export onStop
func onStop(view C.CFTypeRef) {
w := views[view]
w.visible = false
w.w.Event(StageEvent{Stage: StagePaused})
w.hidden = true
w.ProcessEvent(StageEvent{Stage: StagePaused})
}
//export onStart
func onStart(view C.CFTypeRef) {
w := views[view]
w.hidden = false
w.ProcessEvent(StageEvent{Stage: StageRunning})
w.draw(true)
}
//export onDestroy
func onDestroy(view C.CFTypeRef) {
w := views[view]
delete(views, view)
w.w.Event(ViewEvent{})
w.w.Event(DestroyEvent{})
w.ProcessEvent(ViewEvent{})
w.ProcessEvent(DestroyEvent{})
w.displayLink.Close()
w.displayLink = nil
delete(views, view)
w.view = 0
}
//export onFocus
func onFocus(view C.CFTypeRef, focus int) {
w := views[view]
w.w.Event(key.FocusEvent{Focus: focus != 0})
w.ProcessEvent(key.FocusEvent{Focus: focus != 0})
}
//export onLowMemory
@@ -254,7 +267,7 @@ func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.C
w := views[view]
t := time.Duration(float64(ti) * float64(time.Second))
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Touch,
PointerID: w.lookupTouch(last != 0, touchRef),
@@ -267,7 +280,7 @@ func (w *window) ReadClipboard() {
cstr := C.readClipboard()
defer C.CFRelease(cstr)
content := nsstringToString(cstr)
w.w.Event(transfer.DataEvent{
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
@@ -287,7 +300,7 @@ func (w *window) WriteClipboard(mime string, s []byte) {
func (w *window) Configure([]Option) {
// Decorations are never disabled.
w.config.Decorated = true
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) EditorStateChanged(old, new editorState) {}
@@ -295,10 +308,6 @@ func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) Perform(system.Action) {}
func (w *window) SetAnimating(anim bool) {
v := w.view
if v == 0 {
return
}
if anim {
w.displayLink.Start()
} else {
@@ -311,7 +320,7 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
}
func (w *window) onKeyCommand(name key.Name) {
w.w.Event(key.Event{
w.ProcessEvent(key.Event{
Name: name,
})
}
@@ -350,9 +359,30 @@ func (w *window) ShowTextInput(show bool) {
func (w *window) SetInputHint(_ key.InputHint) {}
func newWindow(win *callbacks, options []Option) error {
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func newWindow(win *callbacks, options []Option) {
mainWindow.in <- windowAndConfig{win, options}
return <-mainWindow.errs
<-mainWindow.windows
}
func osMain() {
+1 -1
View File
@@ -56,7 +56,7 @@ CGFloat _keyboardHeight;
- (void)applicationWillEnterForeground:(UIApplication *)application {
UIView *drawView = self.view.subviews[0];
if (drawView != nil) {
gio_onDraw((__bridge CFTypeRef)drawView);
onStart((__bridge CFTypeRef)drawView);
}
}
+78 -75
View File
@@ -14,8 +14,10 @@ import (
"unicode/utf8"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
@@ -54,9 +56,6 @@ type window struct {
composing bool
requestFocus bool
chanAnimation chan struct{}
chanRedraw chan struct{}
config Config
inset f32.Point
scale float32
@@ -69,7 +68,7 @@ type window struct {
contextStatus contextStatus
}
func newWindow(win *callbacks, options []Option) error {
func newWindow(win *callbacks, options []Option) {
doc := js.Global().Get("document")
cont := getContainer(doc)
cnv := createCanvas(doc)
@@ -84,7 +83,9 @@ func newWindow(win *callbacks, options []Option) error {
head: doc.Get("head"),
clipboard: js.Global().Get("navigator").Get("clipboard"),
wakeups: make(chan struct{}, 1),
w: win,
}
w.w.SetDriver(w)
w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
w.browserHistory = w.window.Get("history")
w.visualViewport = w.window.Get("visualViewport")
@@ -94,15 +95,13 @@ func newWindow(win *callbacks, options []Option) error {
if screen := w.window.Get("screen"); screen.Truthy() {
w.screenOrientation = screen.Get("orientation")
}
w.chanAnimation = make(chan struct{}, 1)
w.chanRedraw = make(chan struct{}, 1)
w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
w.chanAnimation <- struct{}{}
w.draw(false)
return nil
})
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
content := args[0].String()
go win.Event(transfer.DataEvent{
w.processEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
@@ -112,29 +111,13 @@ func newWindow(win *callbacks, options []Option) error {
})
w.addEventListeners()
w.addHistory()
w.w = win
go func() {
defer w.cleanup()
w.w.SetDriver(w)
w.Configure(options)
w.blur()
w.w.Event(ViewEvent{Element: cont})
w.w.Event(StageEvent{Stage: StageRunning})
w.resize()
w.draw(true)
for {
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
case <-w.chanAnimation:
w.animCallback()
case <-w.chanRedraw:
w.draw(true)
}
}
}()
return nil
w.Configure(options)
w.blur()
w.processEvent(ViewEvent{Element: cont})
w.processEvent(StageEvent{Stage: StageRunning})
w.resize()
w.draw(true)
}
func getContainer(doc js.Value) js.Value {
@@ -194,12 +177,12 @@ func (w *window) addEventListeners() {
w.cnv.Set("width", 0)
w.cnv.Set("height", 0)
w.resize()
w.requestRedraw()
w.draw(true)
return nil
})
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
w.resize()
w.requestRedraw()
w.draw(true)
return nil
})
w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
@@ -207,7 +190,7 @@ func (w *window) addEventListeners() {
return nil
})
w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
if w.w.Event(key.Event{Name: key.NameBack}) {
if w.processEvent(key.Event{Name: key.NameBack}) {
return w.browserHistory.Call("forward")
}
return w.browserHistory.Call("back")
@@ -220,7 +203,7 @@ func (w *window) addEventListeners() {
default:
ev.Stage = StageRunning
}
w.w.Event(ev)
w.processEvent(ev)
return nil
})
w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
@@ -280,18 +263,18 @@ func (w *window) addEventListeners() {
w.touches[i] = js.Null()
}
w.touches = w.touches[:0]
w.w.Event(pointer.Event{
w.processEvent(pointer.Event{
Kind: pointer.Cancel,
Source: pointer.Touch,
})
return nil
})
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
w.w.Event(key.FocusEvent{Focus: true})
w.processEvent(key.FocusEvent{Focus: true})
return nil
})
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
w.w.Event(key.FocusEvent{Focus: false})
w.processEvent(key.FocusEvent{Focus: false})
w.blur()
return nil
})
@@ -380,10 +363,50 @@ func (w *window) keyEvent(e js.Value, ks key.State) {
Modifiers: modifiersFor(e),
State: ks,
}
w.w.Event(cmd)
w.processEvent(cmd)
}
}
func (w *window) ProcessEvent(e event.Event) {
w.processEvent(e)
}
func (w *window) processEvent(e event.Event) bool {
if !w.w.ProcessEvent(e) {
return false
}
select {
case w.wakeups <- struct{}{}:
default:
}
return true
}
func (w *window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if ok {
if _, destroy := evt.(DestroyEvent); destroy {
w.cleanup()
}
return evt
}
<-w.wakeups
}
}
func (w *window) Invalidate() {
w.w.Invalidate()
}
func (w *window) Run(f func()) {
f()
}
func (w *window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
}
// modifiersFor returns the modifier set for a DOM MouseEvent or
// KeyEvent.
func modifiersFor(e js.Value) key.Modifiers {
@@ -431,7 +454,7 @@ func (w *window) touchEvent(kind pointer.Kind, e js.Value) {
X: float32(x) * scale,
Y: float32(y) * scale,
}
w.w.Event(pointer.Event{
w.processEvent(pointer.Event{
Kind: kind,
Source: pointer.Touch,
Position: pos,
@@ -481,7 +504,7 @@ func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) {
if jbtns&4 != 0 {
btns |= pointer.ButtonTertiary
}
w.w.Event(pointer.Event{
w.processEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: btns,
@@ -508,17 +531,6 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
return jsf
}
func (w *window) animCallback() {
anim := w.animating
w.animRequested = anim
if anim {
w.requestAnimationFrame.Invoke(w.redraw)
}
if anim {
w.draw(false)
}
}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) SetAnimating(anim bool) {
@@ -574,7 +586,7 @@ func (w *window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.w.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
func (w *window) Perform(system.Action) {}
@@ -613,23 +625,14 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
style.Set("cursor", webCursor[cursor])
}
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
}
func (w *window) ShowTextInput(show bool) {
// Run in a goroutine to avoid a deadlock if the
// focus change result in an event.
go func() {
if show {
w.focus()
} else {
w.blur()
}
}()
if show {
w.focus()
} else {
w.blur()
}
}
func (w *window) SetInputHint(mode key.InputHint) {
@@ -646,7 +649,7 @@ func (w *window) resize() {
}
if size != w.config.Size {
w.config.Size = size
w.w.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
@@ -666,12 +669,19 @@ func (w *window) draw(sync bool) {
if w.contextStatus == contextStatusLost {
return
}
anim := w.animating
w.animRequested = anim
if anim {
w.requestAnimationFrame.Invoke(w.redraw)
} else if !sync {
return
}
size, insets, metric := w.getConfig()
if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
return
}
w.w.Event(frameEvent{
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: size,
@@ -741,13 +751,6 @@ func (w *window) navigationColor(c color.NRGBA) {
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
}
func (w *window) requestRedraw() {
select {
case w.chanRedraw <- struct{}{}:
default:
}
}
func osMain() {
select {}
}
+59 -46
View File
@@ -16,10 +16,12 @@ import (
"unicode/utf8"
"gioui.org/internal/f32"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
_ "gioui.org/internal/cocoainit"
@@ -256,6 +258,7 @@ type window struct {
redraw chan struct{}
cursor pointer.Cursor
pointerBtns pointer.Buttons
loop *eventLoop
scale float32
config Config
@@ -274,25 +277,13 @@ var nextTopLeft C.NSPoint
// mustView is like lookupView, except that it panics
// if the view isn't mapped.
func mustView(view C.CFTypeRef) *window {
w, ok := lookupView(view)
if !ok {
w, exists := viewMap[view]
if !exists {
panic("no window for view")
}
return w
}
func lookupView(view C.CFTypeRef) (*window, bool) {
w, exists := viewMap[view]
if !exists {
return nil, false
}
return w, true
}
func deleteView(view C.CFTypeRef) {
delete(viewMap, view)
}
func insertView(view C.CFTypeRef, w *window) {
viewMap[view] = w
}
@@ -307,7 +298,7 @@ func (w *window) ReadClipboard() {
defer C.CFRelease(cstr)
}
content := nsstringToString(cstr)
w.w.Event(transfer.DataEvent{
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
@@ -420,7 +411,7 @@ func (w *window) Configure(options []Option) {
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
}
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) setTitle(prev, cnf Config) {
@@ -493,7 +484,7 @@ func (w *window) setStage(stage Stage) {
return
}
w.stage = stage
w.w.Event(StageEvent{Stage: stage})
w.ProcessEvent(StageEvent{Stage: stage})
}
//export gio_onKeys
@@ -507,7 +498,7 @@ func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown
w := mustView(view)
for _, k := range str {
if n, ok := convertKey(k); ok {
w.w.Event(key.Event{
w.ProcessEvent(key.Event{
Name: n,
Modifiers: kmods,
State: ks,
@@ -562,7 +553,7 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
default:
panic("invalid direction")
}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: typ,
Source: pointer.Mouse,
Time: t,
@@ -582,7 +573,7 @@ func gio_onDraw(view C.CFTypeRef) {
//export gio_onFocus
func gio_onFocus(view C.CFTypeRef, focus C.int) {
w := mustView(view)
w.w.Event(key.FocusEvent{Focus: focus == 1})
w.ProcessEvent(key.FocusEvent{Focus: focus == 1})
if w.stage >= StageInactive {
if focus == 0 {
w.setStage(StageInactive)
@@ -776,14 +767,14 @@ func (w *window) draw() {
}
if sz != w.config.Size {
w.config.Size = sz
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
if sz.X == 0 || sz.Y == 0 {
return
}
cfg := configFor(w.scale)
w.setStage(StageRunning)
w.w.Event(frameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
@@ -793,6 +784,27 @@ func (w *window) draw() {
})
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func configFor(scale float32) unit.Metric {
return unit.Metric{
PxPerDp: scale,
@@ -803,11 +815,12 @@ func configFor(scale float32) unit.Metric {
//export gio_onClose
func gio_onClose(view C.CFTypeRef) {
w := mustView(view)
w.w.Event(ViewEvent{})
w.w.Event(DestroyEvent{})
w.ProcessEvent(ViewEvent{})
w.setStage(StagePaused)
w.ProcessEvent(DestroyEvent{})
w.displayLink.Close()
w.displayLink = nil
deleteView(view)
delete(viewMap, view)
C.CFRelease(w.view)
w.view = 0
}
@@ -828,14 +841,14 @@ func gio_onShow(view C.CFTypeRef) {
func gio_onFullscreen(view C.CFTypeRef) {
w := mustView(view)
w.config.Mode = Fullscreen
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onWindowed
func gio_onWindowed(view C.CFTypeRef) {
w := mustView(view)
w.config.Mode = Windowed
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onAppHide
@@ -857,20 +870,23 @@ func gio_onFinishLaunching() {
close(launched)
}
func newWindow(win *callbacks, options []Option) error {
func newWindow(win *callbacks, options []Option) {
<-launched
errch := make(chan error)
res := make(chan struct{})
runOnMain(func() {
w, err := newOSWindow()
if err != nil {
errch <- err
w := &window{
redraw: make(chan struct{}, 1),
w: win,
}
w.loop = newEventLoop(w.w, w.wakeup)
win.SetDriver(w)
res <- struct{}{}
if err := w.init(); err != nil {
w.ProcessEvent(DestroyEvent{Err: err})
return
}
errch <- nil
w.w = win
window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0)
w.updateWindowMode()
win.SetDriver(w)
w.Configure(options)
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
@@ -881,22 +897,18 @@ func newWindow(win *callbacks, options []Option) error {
// makeKeyAndOrderFront assumes ownership of our window reference.
C.makeKeyAndOrderFront(window)
layer := C.layerForView(w.view)
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
w.ProcessEvent(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
})
return <-errch
<-res
}
func newOSWindow() (*window, error) {
func (w *window) init() error {
view := C.gio_createView()
if view == 0 {
return nil, errors.New("newOSWindows: failed to create view")
return errors.New("newOSWindow: failed to create view")
}
scale := float32(C.getViewBackingScale(view))
w := &window{
view: view,
scale: scale,
redraw: make(chan struct{}, 1),
}
w.scale = scale
dl, err := newDisplayLink(func() {
select {
case w.redraw <- struct{}{}:
@@ -910,10 +922,11 @@ func newOSWindow() (*window, error) {
w.displayLink = dl
if err != nil {
C.CFRelease(view)
return nil, err
return err
}
insertView(view, w)
return w, nil
w.view = view
return nil
}
func osMain() {
+29 -5
View File
@@ -9,6 +9,7 @@ import (
"errors"
"unsafe"
"gioui.org/io/event"
"gioui.org/io/pointer"
)
@@ -49,7 +50,7 @@ type windowDriver func(*callbacks, []Option) error
// let each driver initialize these variables with their own version of createWindow.
var wlDriver, x11Driver windowDriver
func newWindow(window *callbacks, options []Option) error {
func newWindow(window *callbacks, options []Option) {
var errFirst error
for _, d := range []windowDriver{wlDriver, x11Driver} {
if d == nil {
@@ -57,16 +58,39 @@ func newWindow(window *callbacks, options []Option) error {
}
err := d(window, options)
if err == nil {
return nil
return
}
if errFirst == nil {
errFirst = err
}
}
if errFirst != nil {
return errFirst
window.SetDriver(&dummyDriver{
win: window,
wakeups: make(chan event.Event, 1),
})
if errFirst == nil {
errFirst = errors.New("app: no window driver available")
}
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:
}
return errors.New("app: no window driver available")
}
// xCursor contains mapping from pointer.Cursor to XCursor.
+156 -92
View File
@@ -15,6 +15,7 @@ import (
"math"
"os"
"os/exec"
"runtime"
"strconv"
"sync"
"time"
@@ -25,10 +26,12 @@ import (
"gioui.org/app/internal/xkb"
"gioui.org/f32"
"gioui.org/internal/fling"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
@@ -97,7 +100,9 @@ type wlDisplay struct {
read, write int
}
repeat repeatState
repeat repeatState
poller poller
readClipClose chan struct{}
}
type wlSeat struct {
@@ -137,7 +142,7 @@ type repeatState struct {
delay time.Duration
key uint32
win *callbacks
win *window
stopC chan struct{}
start time.Duration
@@ -195,11 +200,9 @@ type window struct {
}
stage Stage
dead bool
lastFrameCallback *C.struct_wl_callback
animating bool
redraw bool
// The most recent configure serial waiting to be ack'ed.
serial C.uint32_t
scale int
@@ -212,6 +215,10 @@ type window struct {
clipReads chan transfer.DataEvent
wakeups chan struct{}
// invMu avoids the race between the destruction of disp and
// Invalidate waking it up.
invMu sync.Mutex
}
type poller struct {
@@ -260,25 +267,17 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
return err
}
w.w = callbacks
go func() {
defer d.destroy()
defer w.destroy()
w.w.SetDriver(w)
w.w.SetDriver(w)
// Finish and commit setup from createNativeWindow.
w.Configure(options)
w.draw(true)
C.wl_surface_commit(w.surf)
// Finish and commit setup from createNativeWindow.
w.Configure(options)
C.wl_surface_commit(w.surf)
w.w.Event(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
err := w.loop()
w.w.Event(WaylandViewEvent{})
w.w.Event(DestroyEvent{Err: err})
}()
w.ProcessEvent(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
return nil
}
@@ -549,15 +548,15 @@ func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
w := callbackLoad(data).(*window)
w.serial = serial
w.redraw = true
C.xdg_surface_ack_configure(wmSurf, serial)
w.setStage(StageRunning)
w.draw(true)
}
//export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := callbackLoad(data).(*window)
w.dead = true
w.close(nil)
}
//export gio_onToplevelConfigure
@@ -586,8 +585,8 @@ func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_
} else {
w.size.Y += int(w.config.decoHeight)
}
w.w.Event(ConfigEvent{Config: w.config})
w.redraw = true
w.ProcessEvent(ConfigEvent{Config: w.config})
w.draw(true)
}
}
@@ -645,7 +644,7 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *
if w.config.Mode == Minimized {
// Minimized window got brought back up: it is no longer so.
w.config.Mode = Windowed
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
}
@@ -790,7 +789,7 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Press,
Source: pointer.Touch,
Position: w.lastTouch,
@@ -806,7 +805,7 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
s.serial = serial
w := s.touchFoci[id]
delete(s.touchFoci, id)
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Release,
Source: pointer.Touch,
Position: w.lastTouch,
@@ -824,7 +823,7 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Position: w.lastTouch,
Source: pointer.Touch,
@@ -843,7 +842,7 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
s := callbackLoad(data).(*wlSeat)
for id, w := range s.touchFoci {
delete(s.touchFoci, id)
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Cancel,
Source: pointer.Touch,
})
@@ -869,7 +868,7 @@ func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.ui
s.serial = serial
if w.inCompositor {
w.inCompositor = false
w.w.Event(pointer.Event{Kind: pointer.Cancel})
w.ProcessEvent(pointer.Event{Kind: pointer.Cancel})
}
}
@@ -930,7 +929,7 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
}
w.flushScroll()
w.resetFling()
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
@@ -1018,8 +1017,11 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis
}
func (w *window) ReadClipboard() {
if w.disp.readClipClose != nil {
return
}
w.disp.readClipClose = make(chan struct{})
r, err := w.disp.readClipboard()
// Send empty responses on unavailable clipboards or errors.
if r == nil || err != nil {
return
}
@@ -1027,13 +1029,17 @@ func (w *window) ReadClipboard() {
go func() {
defer r.Close()
data, _ := io.ReadAll(r)
w.clipReads <- transfer.DataEvent{
e := transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(data))
},
}
w.Wakeup()
select {
case w.clipReads <- e:
w.disp.wakeup()
case <-w.disp.readClipClose:
}
}()
}
@@ -1096,8 +1102,7 @@ func (w *window) Configure(options []Option) {
w.config.MaxSize = cnf.MaxSize
w.setWindowConstraints()
}
w.w.Event(ConfigEvent{Config: w.config})
w.redraw = true
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) setWindowConstraints() {
@@ -1134,7 +1139,7 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionClose:
w.dead = true
w.close(nil)
}
})
}
@@ -1217,7 +1222,7 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.keyboardFocus = w
s.disp.repeat.Stop(0)
w.w.Event(key.FocusEvent{Focus: true})
w.ProcessEvent(key.FocusEvent{Focus: true})
}
//export gio_onKeyboardLeave
@@ -1226,7 +1231,7 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
s.serial = serial
s.disp.repeat.Stop(0)
w := s.keyboardFocus
w.w.Event(key.FocusEvent{Focus: false})
w.ProcessEvent(key.FocusEvent{Focus: false})
}
//export gio_onKeyboardKey
@@ -1244,7 +1249,7 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.w.Event(e)
w.ProcessEvent(e)
}
}
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
@@ -1279,7 +1284,7 @@ func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
r.now = 0
r.stopC = stopC
r.key = keyCode
r.win = w.w
r.win = w
rate, delay := r.rate, r.delay
go func() {
timer := time.NewTimer(delay)
@@ -1337,9 +1342,9 @@ func (r *repeatState) Repeat(d *wlDisplay) {
for _, e := range d.xkb.DispatchKey(r.key, key.Press) {
if ee, ok := e.(key.EditEvent); ok {
// There's no support for IME yet.
r.win.EditorInsert(ee.Text)
r.win.w.EditorInsert(ee.Text)
} else {
r.win.Event(e)
r.win.ProcessEvent(e)
}
}
r.last += delay
@@ -1352,28 +1357,76 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
w := callbackLoad(data).(*window)
if w.lastFrameCallback == callback {
w.lastFrameCallback = nil
w.draw(false)
}
}
func (w *window) loop() error {
var p poller
for {
if err := w.disp.dispatch(&p); err != nil {
return err
}
select {
case e := <-w.clipReads:
w.w.Event(e)
case <-w.wakeups:
w.w.Event(wakeupEvent{})
default:
}
if w.dead {
break
}
w.draw()
func (w *window) close(err error) {
w.ProcessEvent(WaylandViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
}
func (w *window) dispatch() {
if w.disp == nil {
<-w.wakeups
w.w.Invalidate()
return
}
return nil
if err := w.disp.dispatch(); err != nil {
w.close(err)
return
}
select {
case e := <-w.clipReads:
w.disp.readClipClose = nil
w.ProcessEvent(e)
case <-w.wakeups:
w.w.Invalidate()
default:
}
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
}
func (w *window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if !ok {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
w.invMu.Lock()
w.disp.destroy()
w.disp = nil
w.invMu.Unlock()
}
return evt
}
}
func (w *window) Invalidate() {
select {
case w.wakeups <- struct{}{}:
default:
return
}
w.invMu.Lock()
defer w.invMu.Unlock()
if w.disp != nil {
w.disp.wakeup()
}
}
func (w *window) Run(f func()) {
f()
}
func (w *window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
}
// bindDataDevice initializes the dataDev field if and only if both
@@ -1389,13 +1442,21 @@ func (d *wlDisplay) bindDataDevice() {
}
}
func (d *wlDisplay) dispatch(p *poller) error {
func (d *wlDisplay) dispatch() error {
// wl_display_prepare_read records the current thread for
// use in wl_display_read_events or wl_display_cancel_events.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
dispfd := C.wl_display_get_fd(d.disp)
// Poll for events and notifications.
pollfds := append(p.pollfds[:0],
pollfds := append(d.poller.pollfds[:0],
syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR},
syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
)
for C.wl_display_prepare_read(d.disp) != 0 {
C.wl_display_dispatch_pending(d.disp)
}
dispFd := &pollfds[0]
if ret, err := C.wl_display_flush(d.disp); ret < 0 {
if err != syscall.EAGAIN {
@@ -1406,11 +1467,25 @@ func (d *wlDisplay) dispatch(p *poller) error {
dispFd.Events |= syscall.POLLOUT
}
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
C.wl_display_cancel_read(d.disp)
return fmt.Errorf("wayland: poll failed: %v", err)
}
if dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0 {
C.wl_display_cancel_read(d.disp)
return errors.New("wayland: display file descriptor gone")
}
// Handle events.
if dispFd.Revents&syscall.POLLIN != 0 {
if ret, err := C.wl_display_read_events(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_read_events failed: %v", err)
}
C.wl_display_dispatch_pending(d.disp)
} else {
C.wl_display_cancel_read(d.disp)
}
// Clear notifications.
for {
_, err := syscall.Read(d.notify.read, p.buf[:])
_, err := syscall.Read(d.notify.read, d.poller.buf[:])
if err == syscall.EAGAIN {
break
}
@@ -1418,29 +1493,15 @@ func (d *wlDisplay) dispatch(p *poller) error {
return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
}
}
// Handle events
switch {
case dispFd.Revents&syscall.POLLIN != 0:
if ret, err := C.wl_display_dispatch(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err)
}
case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0:
return errors.New("wayland: display file descriptor gone")
}
d.repeat.Repeat(d)
return nil
}
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
w.disp.wakeup()
}
func (w *window) SetAnimating(anim bool) {
w.animating = anim
if anim {
w.draw(false)
}
}
// Wakeup wakes up the event loop through the notification pipe.
@@ -1576,7 +1637,7 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) {
return
}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
@@ -1599,7 +1660,7 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Position: w.lastPos,
Buttons: w.pointerBtns,
@@ -1675,13 +1736,13 @@ func (w *window) updateOutputs() {
if found && scale != w.scale {
w.scale = scale
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
w.redraw = true
w.draw(true)
}
if !found {
w.setStage(StagePaused)
} else {
w.setStage(StageRunning)
w.redraw = true
w.draw(true)
}
}
@@ -1693,7 +1754,7 @@ func (w *window) getConfig() (image.Point, unit.Metric) {
}
}
func (w *window) draw() {
func (w *window) draw(sync bool) {
w.flushScroll()
size, cfg := w.getConfig()
if cfg == (unit.Metric{}) {
@@ -1701,11 +1762,9 @@ func (w *window) draw() {
}
if size != w.config.Size {
w.config.Size = size
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
anim := w.animating || w.fling.anim.Active()
sync := w.redraw
w.redraw = false
// Draw animation only when not waiting for frame callback.
redrawAnim := anim && w.lastFrameCallback == nil
if !redrawAnim && !sync {
@@ -1716,7 +1775,7 @@ func (w *window) draw() {
// Use the surface as listener data for gio_onFrameDone.
C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf))
}
w.w.Event(frameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
@@ -1731,7 +1790,7 @@ func (w *window) setStage(s Stage) {
return
}
w.stage = s
w.w.Event(StageEvent{Stage: s})
w.ProcessEvent(StageEvent{Stage: s})
}
func (w *window) display() *C.struct_wl_display {
@@ -1825,6 +1884,10 @@ func newWLDisplay() (*wlDisplay, error) {
}
func (d *wlDisplay) destroy() {
if d.readClipClose != nil {
close(d.readClipClose)
d.readClipClose = nil
}
if d.notify.write != 0 {
syscall.Close(d.notify.write)
d.notify.write = 0
@@ -1866,6 +1929,7 @@ func (d *wlDisplay) destroy() {
if d.disp != nil {
C.wl_display_disconnect(d.disp)
callbackDelete(unsafe.Pointer(d.disp))
d.disp = nil
}
}
+77 -40
View File
@@ -19,10 +19,12 @@ import (
syscall "golang.org/x/sys/windows"
"gioui.org/app/internal/windows"
"gioui.org/op"
"gioui.org/unit"
gowindows "golang.org/x/sys/windows"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
@@ -53,6 +55,10 @@ type window struct {
borderSize image.Point
config Config
loop *eventLoop
// invMu avoids the race between destroying the window and Invalidate.
invMu sync.Mutex
}
const _WM_WAKEUP = windows.WM_USER + iota
@@ -85,36 +91,38 @@ func osMain() {
select {}
}
func newWindow(window *callbacks, options []Option) error {
cerr := make(chan error)
func newWindow(win *callbacks, options []Option) {
done := make(chan struct{})
go func() {
// GetMessage and PeekMessage can filter on a window HWND, but
// then thread-specific messages such as WM_QUIT are ignored.
// Instead lock the thread so window messages arrive through
// unfiltered GetMessage calls.
runtime.LockOSThread()
w, err := createNativeWindow()
w := &window{
w: win,
}
w.loop = newEventLoop(w.w, w.wakeup)
w.w.SetDriver(w)
err := w.init()
done <- struct{}{}
if err != nil {
cerr <- err
w.ProcessEvent(DestroyEvent{Err: err})
return
}
cerr <- nil
winMap.Store(w.hwnd, w)
defer winMap.Delete(w.hwnd)
w.w = window
w.w.SetDriver(w)
w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)})
w.ProcessEvent(ViewEvent{HWND: uintptr(w.hwnd)})
w.Configure(options)
windows.SetForegroundWindow(w.hwnd)
windows.SetFocus(w.hwnd)
// Since the window class for the cursor is null,
// set it here to show the cursor.
w.SetCursor(pointer.CursorDefault)
if err := w.loop(); err != nil {
panic(err)
}
w.runLoop()
}()
return <-cerr
<-done
}
// initResources initializes the resources global.
@@ -149,13 +157,13 @@ func initResources() error {
const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE
func createNativeWindow() (*window, error) {
func (w *window) init() error {
var resErr error
resources.once.Do(func() {
resErr = initResources()
})
if resErr != nil {
return nil, resErr
return resErr
}
const dwStyle = windows.WS_OVERLAPPEDWINDOW
@@ -171,16 +179,15 @@ func createNativeWindow() (*window, error) {
resources.handle,
0)
if err != nil {
return nil, err
}
w := &window{
hwnd: hwnd,
return err
}
w.hdc, err = windows.GetDC(hwnd)
if err != nil {
return nil, err
windows.DestroyWindow(hwnd)
return err
}
return w, nil
w.hwnd = hwnd
return nil
}
// update() handles changes done by the user, and updates the configuration.
@@ -197,7 +204,7 @@ func (w *window) update() {
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
)
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
@@ -238,7 +245,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
e.State = key.Release
}
w.w.Event(e)
w.ProcessEvent(e)
if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) {
// Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs
@@ -259,15 +266,15 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MBUTTONUP:
w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
case windows.WM_CANCELMODE:
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Cancel,
})
case windows.WM_SETFOCUS:
w.focused = true
w.w.Event(key.FocusEvent{Focus: true})
w.ProcessEvent(key.FocusEvent{Focus: true})
case windows.WM_KILLFOCUS:
w.focused = false
w.w.Event(key.FocusEvent{Focus: false})
w.ProcessEvent(key.FocusEvent{Focus: false})
case windows.WM_NCACTIVATE:
if w.stage >= StageInactive {
if wParam == windows.TRUE {
@@ -288,7 +295,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MOUSEMOVE:
x, y := coordsFromlParam(lParam)
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Position: p,
@@ -301,14 +308,16 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MOUSEHWHEEL:
w.scrollEvent(wParam, lParam, true, getModifiers())
case windows.WM_DESTROY:
w.w.Event(ViewEvent{})
w.w.Event(DestroyEvent{})
w.ProcessEvent(ViewEvent{})
w.ProcessEvent(DestroyEvent{})
if w.hdc != 0 {
windows.ReleaseDC(w.hdc)
w.hdc = 0
}
w.invMu.Lock()
// The system destroys the HWND for us.
w.hwnd = 0
w.invMu.Unlock()
windows.PostQuitMessage(0)
case windows.WM_NCCALCSIZE:
if w.config.Decorated {
@@ -328,7 +337,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
// Adjust window position to avoid the extra padding in maximized
// state. See https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543.
// Note that trying to do the adjustment in WM_GETMINMAXINFO is ignored by Windows.
szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(uintptr(lParam)))
szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(lParam))
mi := windows.GetMonitorInfo(w.hwnd)
szp.Rgrc[0] = mi.WorkArea
return 0
@@ -350,7 +359,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
w.setStage(StageRunning)
}
case windows.WM_GETMINMAXINFO:
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam))
var bw, bh int32
if w.config.Decorated {
r := windows.GetWindowRect(w.hwnd)
@@ -378,7 +387,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
return windows.TRUE
}
case _WM_WAKEUP:
w.w.Event(wakeupEvent{})
w.loop.Wakeup()
w.loop.FlushEvents()
case windows.WM_IME_STARTCOMPOSITION:
imc := windows.ImmGetContext(w.hwnd)
if imc == 0 {
@@ -518,7 +528,7 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
}
x, y := coordsFromlParam(lParam)
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Position: p,
@@ -553,7 +563,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
sp.Y = -dist
}
}
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Position: p,
@@ -565,7 +575,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
}
// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/
func (w *window) loop() error {
func (w *window) runLoop() {
msg := new(windows.Msg)
loop:
for {
@@ -576,7 +586,7 @@ loop:
}
switch ret := windows.GetMessage(msg, 0, 0, 0); ret {
case -1:
return errors.New("GetMessage failed")
panic(errors.New("GetMessage failed"))
case 0:
// WM_QUIT received.
break loop
@@ -584,7 +594,6 @@ loop:
windows.TranslateMessage(msg)
windows.DispatchMessage(msg)
}
return nil
}
func (w *window) EditorStateChanged(old, new editorState) {
@@ -602,7 +611,35 @@ func (w *window) SetAnimating(anim bool) {
w.animating = anim
}
func (w *window) Wakeup() {
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
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 {
panic(err)
}
@@ -611,7 +648,7 @@ func (w *window) Wakeup() {
func (w *window) setStage(s Stage) {
if s != w.stage {
w.stage = s
w.w.Event(StageEvent{Stage: s})
w.ProcessEvent(StageEvent{Stage: s})
}
}
@@ -621,7 +658,7 @@ func (w *window) draw(sync bool) {
}
dpi := windows.GetWindowDPI(w.hwnd)
cfg := configForDPI(dpi)
w.w.Event(frameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
@@ -668,7 +705,7 @@ func (w *window) readClipboard() error {
}
defer windows.GlobalUnlock(mem)
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
w.w.Event(transfer.DataEvent{
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
+113 -76
View File
@@ -38,10 +38,12 @@ import (
"unsafe"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
syscall "golang.org/x/sys/unix"
@@ -98,7 +100,6 @@ type x11Window struct {
notify struct {
read, write int
}
dead bool
animating bool
@@ -111,6 +112,11 @@ type x11Window struct {
config Config
wakeups chan struct{}
handler x11EventHandler
buf [100]byte
// invMy avoids the race between destroy and Invalidate.
invMu sync.Mutex
}
var (
@@ -234,7 +240,7 @@ func (w *x11Window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *x11Window) setTitle(prev, cnf Config) {
@@ -377,11 +383,47 @@ func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
var x11OneByte = make([]byte, 1)
func (w *x11Window) Wakeup() {
func (w *x11Window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
}
func (w *x11Window) shutdown(err error) {
w.ProcessEvent(X11ViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
}
func (w *x11Window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if !ok {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
}
return evt
}
}
func (w *x11Window) Run(f func()) {
f()
}
func (w *x11Window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
}
func (w *x11Window) Invalidate() {
select {
case w.wakeups <- struct{}{}:
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 {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
@@ -400,11 +442,23 @@ func (w *x11Window) setStage(s Stage) {
return
}
w.stage = s
w.w.Event(StageEvent{Stage: s})
w.ProcessEvent(StageEvent{Stage: s})
}
func (w *x11Window) loop() {
h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
func (w *x11Window) dispatch() {
if w.x == nil {
// Only Invalidate can wake us up.
<-w.wakeups
w.w.Invalidate()
return
}
select {
case <-w.wakeups:
w.w.Invalidate()
default:
}
xfd := C.XConnectionNumber(w.x)
// Poll for events and notifications.
@@ -414,64 +468,52 @@ func (w *x11Window) loop() {
}
xEvents := &pollfds[0].Revents
// Plenty of room for a backlog of notifications.
buf := make([]byte, 100)
loop:
for !w.dead {
var syn, anim bool
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll.
if syn = h.handleEvents(); !syn {
anim = w.animating
if !anim {
// Clear poll events.
*xEvents = 0
// Wait for X event or gio notification.
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
}
switch {
case *xEvents&syscall.POLLIN != 0:
syn = h.handleEvents()
if w.dead {
break loop
}
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
break loop
}
var syn, anim bool
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll.
if syn = w.handler.handleEvents(); !syn {
anim = w.animating
if !anim {
// Clear poll events.
*xEvents = 0
// Wait for X event or gio notification.
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
}
switch {
case *xEvents&syscall.POLLIN != 0:
syn = w.handler.handleEvents()
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
}
}
// Clear notifications.
for {
_, err := syscall.Read(w.notify.read, buf)
if err == syscall.EAGAIN {
break
}
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
}
// Clear notifications.
for {
_, err := syscall.Read(w.notify.read, w.buf[:])
if err == syscall.EAGAIN {
break
}
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
default:
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.w.Event(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
}
}
func (w *x11Window) destroy() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.notify.write != 0 {
syscall.Close(w.notify.write)
w.notify.write = 0
@@ -486,6 +528,7 @@ func (w *x11Window) destroy() {
}
C.XDestroyWindow(w.x, w.xw)
C.XCloseDisplay(w.x)
w.x = nil
}
// atom is a wrapper around XInternAtom. Callers should cache the result
@@ -543,7 +586,7 @@ func (h *x11EventHandler) handleEvents() bool {
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.w.Event(e)
w.ProcessEvent(e)
}
}
case C.ButtonPress, C.ButtonRelease:
@@ -605,10 +648,10 @@ func (h *x11EventHandler) handleEvents() bool {
w.pointerBtns &^= btn
}
ev.Buttons = w.pointerBtns
w.w.Event(ev)
w.ProcessEvent(ev)
case C.MotionNotify:
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
w.w.Event(pointer.Event{
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
@@ -623,14 +666,14 @@ func (h *x11EventHandler) handleEvents() bool {
// redraw only on the last expose event
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
case C.FocusIn:
w.w.Event(key.FocusEvent{Focus: true})
w.ProcessEvent(key.FocusEvent{Focus: true})
case C.FocusOut:
w.w.Event(key.FocusEvent{Focus: false})
w.ProcessEvent(key.FocusEvent{Focus: false})
case C.ConfigureNotify: // window configuration change
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
w.config.Size = sz
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
// redraw will be done by a later expose event
case C.SelectionNotify:
@@ -652,7 +695,7 @@ func (h *x11EventHandler) handleEvents() bool {
break
}
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
w.w.Event(transfer.DataEvent{
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(str))
@@ -711,7 +754,7 @@ func (h *x11EventHandler) handleEvents() bool {
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
case C.long(w.atoms.evDelWindow):
w.dead = true
w.shutdown(nil)
return false
}
}
@@ -793,8 +836,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
wakeups: make(chan struct{}, 1),
config: Config{Size: cnf.Size},
}
w.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
w.notify.read = pipe[0]
w.notify.write = pipe[1]
w.w.SetDriver(w)
if err := w.updateXkbKeymap(); err != nil {
w.destroy()
@@ -830,19 +875,11 @@ func newX11Window(gioWin *callbacks, options []Option) error {
// extensions
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
go func() {
w.w.SetDriver(w)
// make the window visible on the screen
C.XMapWindow(dpy, win)
w.Configure(options)
w.w.Event(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
w.setStage(StageRunning)
w.loop()
w.w.Event(X11ViewEvent{})
w.w.Event(DestroyEvent{Err: nil})
w.destroy()
}()
// make the window visible on the screen
C.XMapWindow(dpy, win)
w.Configure(options)
w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
w.setStage(StageRunning)
return nil
}
+228 -337
View File
@@ -8,6 +8,7 @@ import (
"image"
"image/color"
"runtime"
"sync"
"time"
"unicode/utf8"
@@ -38,32 +39,13 @@ type Option func(unit.Metric, *Config)
type Window struct {
ctx context
gpu gpu.GPU
// driverFuncs is a channel of functions to run when
// the Window has a valid driver.
driverFuncs chan func(d driver)
// wakeups wakes up the native event loop to send a
// WakeupEvent that flushes driverFuncs.
wakeups chan struct{}
// wakeupFuncs is sent wakeup functions when the driver changes.
wakeupFuncs chan func()
// redraws is notified when a redraw is requested by the client.
redraws chan struct{}
// immediateRedraws is like redraw but doesn't need a wakeup.
immediateRedraws chan struct{}
// scheduledRedraws is sent the most recent delayed redraw time.
scheduledRedraws chan time.Time
// options are the options waiting to be applied.
options chan []Option
// actions are the actions waiting to be performed.
actions chan system.Action
// out is where the platform backend delivers events bound for the
// user program.
out chan event.Event
frames chan *op.Ops
frameAck chan struct{}
destroy chan struct{}
// timer tracks the delayed invalidate goroutine.
timer struct {
// quit is shuts down the goroutine.
quit chan struct{}
// update the invalidate time.
update chan time.Time
}
stage Stage
animating bool
@@ -72,8 +54,7 @@ type Window struct {
// viewport is the latest frame size with insets applied.
viewport image.Rectangle
// metric is the metric from the most recent frame.
metric unit.Metric
metric unit.Metric
queue input.Router
cursor pointer.Cursor
decorations struct {
@@ -89,11 +70,8 @@ type Window struct {
*material.Theme
*widget.Decorations
}
callbacks callbacks
nocontext bool
// semantic data, lazily evaluated if requested by a backend to speed up
// the cases where semantic data is not needed.
semantic struct {
@@ -104,25 +82,35 @@ type Window struct {
tree []input.SemanticNode
ids map[input.SemanticID]input.SemanticNode
}
imeState editorState
// event stores the state required for processing and delivering events
// from NextEvent. If we had support for range over func, this would
// be the iterator state.
eventState struct {
created bool
initialOpts []Option
wakeup func()
timer *time.Timer
driver driver
// basic is the driver interface that is needed even after the window is gone.
basic basicDriver
once sync.Once
initialOpts []Option
// coalesced tracks the most recent events waiting to be delivered
// to the client.
coalesced eventSummary
// frame tracks the most recently frame event.
lastFrame struct {
sync bool
size image.Point
off image.Point
deco op.CallOp
}
}
type eventSummary struct {
wakeup bool
cfg *ConfigEvent
view *ViewEvent
frame *frameEvent
stage *StageEvent
destroy *DestroyEvent
}
type callbacks struct {
w *Window
d driver
busy bool
waitEvents []event.Event
w *Window
}
// NewWindow creates a new window for a set of window
@@ -162,19 +150,7 @@ func NewWindow(options ...Option) *Window {
cnf.apply(unit.Metric{}, options)
w := &Window{
out: make(chan event.Event),
immediateRedraws: make(chan struct{}),
redraws: make(chan struct{}, 1),
scheduledRedraws: make(chan time.Time, 1),
frames: make(chan *op.Ops),
frameAck: make(chan struct{}),
driverFuncs: make(chan func(d driver), 1),
wakeups: make(chan struct{}, 1),
wakeupFuncs: make(chan func()),
destroy: make(chan struct{}),
options: make(chan []Option, 1),
actions: make(chan system.Action, 1),
nocontext: cnf.CustomRenderer,
nocontext: cnf.CustomRenderer,
}
w.decorations.Theme = theme
w.decorations.Decorations = deco
@@ -183,7 +159,7 @@ func NewWindow(options ...Option) *Window {
w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
w.callbacks.w = w
w.eventState.initialOpts = options
w.initialOpts = options
return w
}
@@ -193,15 +169,7 @@ func decoHeightOpt(h unit.Dp) Option {
}
}
// update the window contents, input operations declare input handlers,
// and so on. The supplied operations list completely replaces the window state
// from previous calls.
func (w *Window) update(frame *op.Ops) {
w.frames <- frame
<-w.frameAck
}
func (w *Window) validateAndProcess(d driver, size image.Point, sync bool, frame *op.Ops, sigChan chan<- struct{}) error {
func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops, sigChan chan<- struct{}) error {
signal := func() {
if sigChan != nil {
// We're done with frame, let the client continue.
@@ -215,7 +183,7 @@ func (w *Window) validateAndProcess(d driver, size image.Point, sync bool, frame
if w.gpu == nil && !w.nocontext {
var err error
if w.ctx == nil {
w.ctx, err = d.NewContext()
w.ctx, err = w.driver.NewContext()
if err != nil {
return err
}
@@ -294,7 +262,22 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
return w.gpu.Frame(frame, target, viewport)
}
func (w *Window) processFrame(d driver) {
func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
wrapper := &w.decorations.Ops
off := op.Offset(w.lastFrame.off).Push(wrapper)
ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
off.Pop()
w.lastFrame.deco.Add(wrapper)
if err := w.validateAndProcess(w.lastFrame.size, w.lastFrame.sync, wrapper, ack); err != nil {
w.destroyGPU()
w.driver.ProcessEvent(DestroyEvent{Err: err})
return
}
w.updateState()
w.updateCursor()
}
func (w *Window) updateState() {
for k := range w.semantic.ids {
delete(w.semantic.ids, k)
}
@@ -302,34 +285,34 @@ func (w *Window) processFrame(d driver) {
q := &w.queue
switch q.TextInputState() {
case input.TextInputOpen:
d.ShowTextInput(true)
w.driver.ShowTextInput(true)
case input.TextInputClose:
d.ShowTextInput(false)
w.driver.ShowTextInput(false)
}
if hint, ok := q.TextInputHint(); ok {
d.SetInputHint(hint)
w.driver.SetInputHint(hint)
}
if mime, txt, ok := q.WriteClipboard(); ok {
d.WriteClipboard(mime, txt)
w.driver.WriteClipboard(mime, txt)
}
if q.ClipboardRequested() {
d.ReadClipboard()
w.driver.ReadClipboard()
}
oldState := w.imeState
newState := oldState
newState.EditorState = q.EditorState()
if newState != oldState {
w.imeState = newState
d.EditorStateChanged(oldState, newState)
w.driver.EditorStateChanged(oldState, newState)
}
if t, ok := q.WakeupTime(); ok {
w.setNextFrame(t)
}
w.updateAnimation(d)
w.updateAnimation()
}
// Invalidate the window such that a [FrameEvent] will be generated immediately.
// If the window is inactive, the event is sent when the window becomes active.
// If the window is inactive, an unspecified event is sent instead.
//
// Note that Invalidate is intended for externally triggered updates, such as a
// response from a network request. The [op.InvalidateCmd] command is more efficient
@@ -337,16 +320,8 @@ func (w *Window) processFrame(d driver) {
//
// Invalidate is safe for concurrent use.
func (w *Window) Invalidate() {
select {
case w.immediateRedraws <- struct{}{}:
return
default:
}
select {
case w.redraws <- struct{}{}:
w.wakeup()
default:
}
w.init()
w.basic.Invalidate()
}
// Option applies the options to the window.
@@ -354,15 +329,21 @@ func (w *Window) Option(opts ...Option) {
if len(opts) == 0 {
return
}
for {
select {
case old := <-w.options:
opts = append(old, opts...)
case w.options <- opts:
w.wakeup()
return
w.Run(func() {
cnf := Config{Decorated: w.decorations.enabled}
for _, opt := range opts {
opt(w.metric, &cnf)
}
}
w.decorations.enabled = cnf.Decorated
decoHeight := w.decorations.height
if !w.decorations.enabled {
decoHeight = 0
}
opts = append(opts, decoHeightOpt(decoHeight))
w.driver.Configure(opts)
w.setNextFrame(time.Time{})
w.updateAnimation()
})
}
// Run f in the same thread as the native window event loop, and wait for f to
@@ -374,52 +355,64 @@ func (w *Window) Option(opts ...Option) {
// Note that most programs should not call Run; configuring a Window with
// [CustomRenderer] is a notable exception.
func (w *Window) Run(f func()) {
w.init()
if w.driver == nil {
return
}
done := make(chan struct{})
w.driverDefer(func(d driver) {
w.driver.Run(func() {
defer close(done)
f()
})
select {
case <-done:
case <-w.destroy:
}
<-done
}
// driverDefer is like Run but can be run from any context. It doesn't wait
// for f to return.
func (w *Window) driverDefer(f func(d driver)) {
select {
case w.driverFuncs <- f:
w.wakeup()
case <-w.destroy:
func (w *Window) updateAnimation() {
if w.driver == nil {
return
}
}
func (w *Window) updateAnimation(d driver) {
animate := false
if w.stage >= StageInactive && w.hasNextFrame {
if dt := time.Until(w.nextFrame); dt <= 0 {
animate = true
} else {
// Schedule redraw.
select {
case <-w.scheduledRedraws:
default:
}
w.scheduledRedraws <- w.nextFrame
w.scheduleInvalidate(w.nextFrame)
}
}
if animate != w.animating {
w.animating = animate
d.SetAnimating(animate)
w.driver.SetAnimating(animate)
}
}
func (w *Window) wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
func (w *Window) scheduleInvalidate(t time.Time) {
if w.timer.quit == nil {
w.timer.quit = make(chan struct{})
w.timer.update = make(chan time.Time)
go func() {
var timer *time.Timer
for {
var timeC <-chan time.Time
if timer != nil {
timeC = timer.C
}
select {
case <-w.timer.quit:
w.timer.quit <- struct{}{}
return
case t := <-w.timer.update:
if timer != nil {
timer.Stop()
}
timer = time.NewTimer(time.Until(t))
case <-timeC:
w.Invalidate()
}
}
}()
}
w.timer.update <- t
}
func (w *Window) setNextFrame(at time.Time) {
@@ -429,61 +422,19 @@ func (w *Window) setNextFrame(at time.Time) {
}
}
func (c *callbacks) SetDriver(d driver) {
c.d = d
var wakeup func()
if d != nil {
wakeup = d.Wakeup
func (c *callbacks) SetDriver(d basicDriver) {
c.w.basic = d
if d, ok := d.(driver); ok {
c.w.driver = d
}
c.w.wakeupFuncs <- wakeup
}
func (c *callbacks) Event(e event.Event) bool {
if c.d == nil {
panic("event while no driver active")
}
c.waitEvents = append(c.waitEvents, e)
if c.busy {
return true
}
c.busy = true
var handled bool
for len(c.waitEvents) > 0 {
e := c.waitEvents[0]
copy(c.waitEvents, c.waitEvents[1:])
c.waitEvents = c.waitEvents[:len(c.waitEvents)-1]
handled = c.w.processEvent(c.d, e)
}
c.busy = false
select {
case <-c.w.destroy:
return handled
default:
}
c.w.updateState(c.d)
if _, ok := e.(wakeupEvent); ok {
select {
case opts := <-c.w.options:
cnf := Config{Decorated: c.w.decorations.enabled}
for _, opt := range opts {
opt(c.w.metric, &cnf)
}
c.w.decorations.enabled = cnf.Decorated
decoHeight := c.w.decorations.height
if !c.w.decorations.enabled {
decoHeight = 0
}
opts = append(opts, decoHeightOpt(decoHeight))
c.d.Configure(opts)
default:
}
select {
case acts := <-c.w.actions:
c.d.Perform(acts)
default:
}
}
return handled
func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) {
c.w.processFrame(frame, ack)
}
func (c *callbacks) ProcessEvent(e event.Event) bool {
return c.w.processEvent(e)
}
// SemanticRoot returns the ID of the semantic root.
@@ -534,13 +485,13 @@ func (c *callbacks) EditorInsert(text string) {
func (c *callbacks) EditorReplace(r key.Range, text string) {
c.w.imeState.Replace(r, text)
c.Event(key.EditEvent{Range: r, Text: text})
c.Event(key.SnippetEvent(c.w.imeState.Snippet.Range))
c.w.driver.ProcessEvent(key.EditEvent{Range: r, Text: text})
c.w.driver.ProcessEvent(key.SnippetEvent(c.w.imeState.Snippet.Range))
}
func (c *callbacks) SetEditorSelection(r key.Range) {
c.w.imeState.Selection.Range = r
c.Event(key.SelectionEvent(r))
c.w.driver.ProcessEvent(key.SelectionEvent(r))
}
func (c *callbacks) SetEditorSnippet(r key.Range) {
@@ -548,7 +499,7 @@ func (c *callbacks) SetEditorSnippet(r key.Range) {
// No need to expand.
return
}
c.Event(key.SnippetEvent(r))
c.w.driver.ProcessEvent(key.SnippetEvent(r))
}
func (w *Window) moveFocus(dir key.FocusDirection) {
@@ -578,29 +529,13 @@ func (w *Window) moveFocus(dir key.FocusDirection) {
func (c *callbacks) ClickFocus() {
c.w.queue.ClickFocus()
c.w.setNextFrame(time.Time{})
c.w.updateAnimation(c.d)
c.w.updateAnimation()
}
func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) {
return c.w.queue.ActionAt(p)
}
func (w *Window) waitAck(d driver) {
for {
select {
case f := <-w.driverFuncs:
f(d)
case w.out <- theFlushEvent:
// A dummy event went through, so we know the application has processed the previous event.
return
case <-w.immediateRedraws:
// Invalidate was called during frame processing.
w.setNextFrame(time.Time{})
w.updateAnimation(d)
}
}
}
func (w *Window) destroyGPU() {
if w.gpu != nil {
w.ctx.Lock()
@@ -614,27 +549,6 @@ func (w *Window) destroyGPU() {
}
}
// waitFrame waits for the client to either call [FrameEvent.Frame]
// or to continue event handling.
func (w *Window) waitFrame(d driver) *op.Ops {
for {
select {
case f := <-w.driverFuncs:
f(d)
case frame := <-w.frames:
// The client called FrameEvent.Frame.
return frame
case w.out <- theFlushEvent:
// The client ignored FrameEvent and continued processing
// events.
return nil
case <-w.immediateRedraws:
// Invalidate was called during frame processing.
w.setNextFrame(time.Time{})
}
}
}
// updateSemantics refreshes the semantics tree, the id to node map and the ids of
// updated nodes.
func (w *Window) updateSemantics() {
@@ -671,26 +585,46 @@ func (w *Window) collectSemanticDiffs(diffs *[]input.SemanticID, n input.Semanti
}
}
func (w *Window) updateState(d driver) {
for {
select {
case f := <-w.driverFuncs:
f(d)
case <-w.redraws:
w.setNextFrame(time.Time{})
w.updateAnimation(d)
default:
return
}
}
func (c *callbacks) Invalidate() {
c.w.setNextFrame(time.Time{})
c.w.updateAnimation()
// Guarantee a wakeup, even when not animating.
c.w.processEvent(wakeupEvent{})
}
func (w *Window) processEvent(d driver, e event.Event) bool {
select {
case <-w.destroy:
return false
default:
func (c *callbacks) nextEvent() (event.Event, bool) {
s := &c.w.coalesced
// Every event counts as a wakeup.
defer func() { s.wakeup = false }()
switch {
case s.view != nil:
e := *s.view
s.view = nil
return e, true
case s.destroy != nil:
e := *s.destroy
// Clear pending events after DestroyEvent is delivered.
*s = eventSummary{}
return e, true
case s.cfg != nil:
e := *s.cfg
s.cfg = nil
return e, true
case s.stage != nil:
e := *s.stage
s.stage = nil
return e, true
case s.frame != nil:
e := *s.frame
s.frame = nil
return e.FrameEvent, true
case s.wakeup:
return wakeupEvent{}, true
}
return nil, false
}
func (w *Window) processEvent(e event.Event) bool {
switch e2 := e.(type) {
case StageEvent:
if e2.Stage < StageInactive {
@@ -702,9 +636,10 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
}
}
w.stage = e2.Stage
w.updateAnimation(d)
w.out <- e
w.waitAck(d)
w.updateAnimation()
w.coalesced.stage = &e2
case wakeupEvent:
w.coalesced.wakeup = true
case frameEvent:
if e2.Size == (image.Point{}) {
panic(errors.New("internal error: zero-sized Draw"))
@@ -715,12 +650,9 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
}
w.metric = e2.Metric
w.hasNextFrame = false
e2.Frame = w.update
e2.Frame = w.driver.Frame
e2.Source = w.queue.Source()
// Prepare the decorations and update the frame insets.
wrapper := &w.decorations.Ops
wrapper.Reset()
viewport := image.Rectangle{
Min: image.Point{
X: e2.Metric.Dp(e2.Insets.Left),
@@ -736,41 +668,30 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
w.queue.RevealFocus(viewport)
}
w.viewport = viewport
viewSize := e2.Size
wrapper := &w.decorations.Ops
wrapper.Reset()
m := op.Record(wrapper)
size, offset := w.decorate(d, e2.FrameEvent, wrapper)
e2.FrameEvent.Size = size
deco := m.Stop()
w.out <- e2.FrameEvent
frame := w.waitFrame(d)
var signal chan<- struct{}
if frame != nil {
signal = w.frameAck
off := op.Offset(offset).Push(wrapper)
ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
off.Pop()
}
deco.Add(wrapper)
if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil {
w.destroyGPU()
w.out <- DestroyEvent{Err: err}
close(w.destroy)
break
}
w.processFrame(d)
w.updateCursor(d)
offset := w.decorate(e2.FrameEvent, wrapper)
w.lastFrame.deco = m.Stop()
w.lastFrame.size = e2.Size
w.lastFrame.sync = e2.Sync
w.lastFrame.off = offset
e2.Size = e2.Size.Sub(offset)
w.coalesced.frame = &e2
case DestroyEvent:
w.destroyGPU()
w.out <- e2
close(w.destroy)
w.driver = nil
if q := w.timer.quit; q != nil {
q <- struct{}{}
<-q
}
w.coalesced.destroy = &e2
case ViewEvent:
w.out <- e2
w.waitAck(d)
w.coalesced.view = &e2
case ConfigEvent:
w.decorations.Config = e2.Config
e2.Config = w.effectiveConfig()
w.out <- e2
case wakeupEvent:
w.coalesced.cfg = &e2
case event.Event:
focusDir := key.FocusDirection(-1)
if e, ok := e2.(key.Event); ok && e.State == key.Press {
@@ -800,10 +721,10 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
w.moveFocus(focusDir)
t, handled = w.queue.WakeupTime()
}
w.updateCursor(d)
w.updateCursor()
if handled {
w.setNextFrame(t)
w.updateAnimation(d)
w.updateAnimation()
}
return handled
}
@@ -811,58 +732,22 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
}
// NextEvent blocks until an event is received from the window, such as
// [FrameEvent]. It blocks forever if called after [DestroyEvent]
// has been returned.
// [FrameEvent], or until [Invalidate] is called.
func (w *Window) NextEvent() event.Event {
state := &w.eventState
if !state.created {
state.created = true
if err := newWindow(&w.callbacks, state.initialOpts); err != nil {
close(w.destroy)
return DestroyEvent{Err: err}
}
}
for {
var (
wakeups <-chan struct{}
timeC <-chan time.Time
)
if state.wakeup != nil {
wakeups = w.wakeups
if state.timer != nil {
timeC = state.timer.C
}
}
select {
case t := <-w.scheduledRedraws:
if state.timer != nil {
state.timer.Stop()
}
state.timer = time.NewTimer(time.Until(t))
case e := <-w.out:
// Receiving a flushEvent indicates to the platform backend that
// all previous events have been processed by the user program.
if _, ok := e.(flushEvent); ok {
break
}
return e
case <-timeC:
select {
case w.redraws <- struct{}{}:
state.wakeup()
default:
}
case <-wakeups:
state.wakeup()
case state.wakeup = <-w.wakeupFuncs:
}
}
w.init()
return w.basic.Event()
}
func (w *Window) updateCursor(d driver) {
func (w *Window) init() {
w.once.Do(func() {
newWindow(&w.callbacks, w.initialOpts)
})
}
func (w *Window) updateCursor() {
if c := w.queue.Cursor(); c != w.cursor {
w.cursor = c
d.SetCursor(c)
w.driver.SetCursor(c)
}
}
@@ -872,9 +757,9 @@ func (w *Window) fallbackDecorate() bool {
}
// decorate the window if enabled and returns the corresponding Insets.
func (w *Window) decorate(d driver, e FrameEvent, o *op.Ops) (size, offset image.Point) {
func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point {
if !w.fallbackDecorate() {
return e.Size, image.Pt(0, 0)
return image.Pt(0, 0)
}
deco := w.decorations.Decorations
allActions := system.ActionMinimize | system.ActionMaximize | system.ActionUnmaximize |
@@ -903,16 +788,17 @@ func (w *Window) decorate(d driver, e FrameEvent, o *op.Ops) (size, offset image
Constraints: layout.Exact(e.Size),
}
// Update the window based on the actions on the decorations.
w.Perform(deco.Update(gtx))
opts, acts := splitActions(deco.Update(gtx))
w.driver.Configure(opts)
w.driver.Perform(acts)
style.Layout(gtx)
// Offset to place the frame content below the decorations.
decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
if w.decorations.currentHeight != decoHeight {
w.decorations.currentHeight = decoHeight
w.out <- ConfigEvent{Config: w.effectiveConfig()}
w.coalesced.cfg = &ConfigEvent{Config: w.effectiveConfig()}
}
e.Size.Y -= w.decorations.currentHeight
return e.Size, image.Pt(0, decoHeight)
return image.Pt(0, decoHeight)
}
func (w *Window) effectiveConfig() Config {
@@ -922,33 +808,38 @@ func (w *Window) effectiveConfig() Config {
return cnf
}
// Perform the actions on the window.
func (w *Window) Perform(actions system.Action) {
// splitActions splits options from actions and return them and the remaining
// actions.
func splitActions(actions system.Action) ([]Option, system.Action) {
var opts []Option
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionMinimize:
w.Option(Minimized.Option())
opts = append(opts, Minimized.Option())
case system.ActionMaximize:
w.Option(Maximized.Option())
opts = append(opts, Maximized.Option())
case system.ActionUnmaximize:
w.Option(Windowed.Option())
opts = append(opts, Windowed.Option())
case system.ActionFullscreen:
opts = append(opts, Fullscreen.Option())
default:
return
}
actions &^= action
})
if actions == 0 {
return opts, actions
}
// Perform the actions on the window.
func (w *Window) Perform(actions system.Action) {
opts, acts := splitActions(actions)
w.Option(opts...)
if acts == 0 {
return
}
for {
select {
case old := <-w.actions:
actions |= old
case w.actions <- actions:
w.wakeup()
return
}
}
w.Run(func() {
w.driver.Perform(actions)
})
}
// Title sets the title of the window.