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