app: [macOS] implement custom event dispatching

To get rid of app.Main, we need to control the main thread. The macOS
[NSApp run] must be called on the main goroutine and never yields control.

Implement the escape hatch which is calling [NSApp stop] to force
[NSApp run] to return and allow us to fetch and dispatch events one at a
time.

This change is separate from the larger change removing app.Main to ease
bisecting.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-12-11 19:18:38 -06:00
parent 43024fcca2
commit b1f84da679
6 changed files with 129 additions and 42 deletions
+7 -10
View File
@@ -15,8 +15,8 @@ __attribute__ ((visibility ("hidden"))) void gio_hideCursor();
__attribute__ ((visibility ("hidden"))) void gio_showCursor(); __attribute__ ((visibility ("hidden"))) void gio_showCursor();
__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID); __attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
static bool isMainThread() { static int isMainThread() {
return [NSThread isMainThread]; return [NSThread isMainThread] ? 1 : 0;
} }
static NSUInteger nsstringLength(CFTypeRef cstr) { static NSUInteger nsstringLength(CFTypeRef cstr) {
@@ -77,7 +77,7 @@ var mainFuncs = make(chan func(), 1)
// runOnMain runs the function on the main thread. // runOnMain runs the function on the main thread.
func runOnMain(f func()) { func runOnMain(f func()) {
if C.isMainThread() { if isMainThread() {
f() f()
return return
} }
@@ -87,6 +87,10 @@ func runOnMain(f func()) {
}() }()
} }
func isMainThread() bool {
return C.isMainThread() != 0
}
//export gio_dispatchMainFuncs //export gio_dispatchMainFuncs
func gio_dispatchMainFuncs() { func gio_dispatchMainFuncs() {
for { for {
@@ -259,10 +263,3 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
C.gio_setCursor(C.NSUInteger(macosCursorID[to])) C.gio_setCursor(C.NSUInteger(macosCursorID[to]))
return to return to
} }
func (w *window) wakeup() {
runOnMain(func() {
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
-11
View File
@@ -1,11 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
#import <Foundation/Foundation.h>
#include "_cgo_export.h"
void gio_wakeupMainThread(void) {
dispatch_async(dispatch_get_main_queue(), ^{
gio_dispatchMainFuncs();
});
}
+7
View File
@@ -396,5 +396,12 @@ func gio_runMain() {
runMain() runMain()
} }
func (w *window) wakeup() {
runOnMain(func() {
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
func (UIKitViewEvent) implementsViewEvent() {} func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent() {} func (UIKitViewEvent) ImplementsEvent() {}
+6
View File
@@ -276,3 +276,9 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
GioView *v = (__bridge GioView *)viewRef; GioView *v = (__bridge GioView *)viewRef;
v.handle = handle; v.handle = handle;
} }
void gio_wakeupMainThread(void) {
dispatch_async(dispatch_get_main_queue(), ^{
gio_dispatchMainFuncs();
});
}
+80 -18
View File
@@ -39,7 +39,7 @@ import (
#define MOUSE_DOWN 3 #define MOUSE_DOWN 3
#define MOUSE_SCROLL 4 #define MOUSE_SCROLL 4
__attribute__ ((visibility ("hidden"))) void gio_main(void); __attribute__ ((visibility ("hidden"))) void gio_initApp(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); __attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
@@ -289,6 +289,16 @@ static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
} }
} }
} }
static void dispatchEvent(void) {
@autoreleasepool {
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantFuture]
inMode:NSDefaultRunLoopMode
dequeue:YES];
[NSApp sendEvent:event];
}
}
*/ */
import "C" import "C"
@@ -323,13 +333,16 @@ type window struct {
config Config config Config
} }
// launched is closed when applicationDidFinishLaunching is called. // launched is closed after gio_initApp returns.
var launched = make(chan struct{}) var launched = make(chan struct{})
// nextTopLeft is the offset to use for the next window's call to // nextTopLeft is the offset to use for the next window's call to
// cascadeTopLeftFromPoint. // cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint var nextTopLeft C.NSPoint
// mainThreadWindow is the window currently in control of the main thread.
var mainThreadWindow *window
func windowFor(h C.uintptr_t) *window { func windowFor(h C.uintptr_t) *window {
return cgo.Handle(h).Value().(*window) return cgo.Handle(h).Value().(*window)
} }
@@ -822,23 +835,51 @@ func (w *window) draw() {
func (w *window) ProcessEvent(e event.Event) { func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e) w.w.ProcessEvent(e)
w.loop.FlushEvents() // The main thread window deliver events in Event.
if w != mainThreadWindow {
w.loop.FlushEvents()
}
} }
func (w *window) Event() event.Event { func (w *window) Event() event.Event {
return w.loop.Event() if !isMainThread() {
return w.loop.Event()
}
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
for {
if evt, ok := w.loop.win.nextEvent(); ok {
return evt
}
C.dispatchEvent()
gio_dispatchMainFuncs()
}
} }
func (w *window) Invalidate() { func (w *window) Invalidate() {
if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
}
w.loop.Invalidate() w.loop.Invalidate()
} }
func (w *window) Run(f func()) { func (w *window) Run(f func()) {
if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
}
w.loop.Run(f) w.loop.Run(f)
} }
func (w *window) Frame(frame *op.Ops) { func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame) if !isMainThread() {
w.loop.Frame(frame)
return
}
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
w.loop.win.ProcessFrame(frame, nil)
} }
func configFor(scale float32) unit.Metric { func configFor(scale float32) unit.Metric {
@@ -898,20 +939,27 @@ func gio_onWindowed(h C.uintptr_t) {
w.ProcessEvent(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export gio_onFinishLaunching
func gio_onFinishLaunching() {
close(launched)
}
func newWindow(win *callbacks, options []Option) { func newWindow(win *callbacks, options []Option) {
<-launched w := &window{
res := make(chan struct{}) redraw: make(chan struct{}, 1),
runOnMain(func() { w: win,
w := &window{ }
redraw: make(chan struct{}, 1), w.loop = newEventLoop(w.w, w.wakeup)
w: win, if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
select {
case <-launched:
default:
// If we're the main thread, initialize the GUI.
C.gio_initApp()
close(launched)
} }
w.loop = newEventLoop(w.w, w.wakeup) } else {
<-launched
}
res := make(chan struct{}, 1)
runOnMain(func() {
win.SetDriver(w) win.SetDriver(w)
res <- struct{}{} res <- struct{}{}
if err := w.init(); err != nil { if err := w.init(); err != nil {
@@ -965,7 +1013,12 @@ func (w *window) init() error {
} }
func osMain() { func osMain() {
C.gio_main() C.gio_initApp()
close(launched)
for {
C.dispatchEvent()
gio_dispatchMainFuncs()
}
} }
func convertKey(k rune) (key.Name, bool) { func convertKey(k rune) (key.Name, bool) {
@@ -1052,5 +1105,14 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
return kmods return kmods
} }
func (w *window) wakeup() {
runOnMain(func() {
w.loop.Wakeup()
if w != mainThreadWindow {
w.loop.FlushEvents()
}
})
}
func (AppKitViewEvent) implementsViewEvent() {} func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent() {} func (AppKitViewEvent) ImplementsEvent() {}
+29 -3
View File
@@ -348,7 +348,6 @@ static BOOL isEqualNSCursor(NSCursor *c1, SEL name2) {
// Don't pass commands up the responder chain. // Don't pass commands up the responder chain.
// They will end up in a beep. // They will end up in a beep.
} }
- (BOOL)hasMarkedText { - (BOOL)hasMarkedText {
int res = gio_hasMarkedText(self.handle); int res = gio_hasMarkedText(self.handle);
return res ? YES : NO; return res ? YES : NO;
@@ -613,11 +612,22 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES]; [NSApp activateIgnoringOtherApps:YES];
gio_onFinishLaunching(); // Force the [NSApp run] call to return.
[NSApp stop:nil];
NSEvent *dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSZeroPoint
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:dummy atStart:YES];
} }
@end @end
void gio_main() { void gio_initApp() {
@autoreleasepool { @autoreleasepool {
[GioApplication sharedApplication]; [GioApplication sharedApplication];
GioAppDelegate *del = [[GioAppDelegate alloc] init]; GioAppDelegate *del = [[GioAppDelegate alloc] init];
@@ -641,6 +651,22 @@ void gio_main() {
globalWindowDel = [[GioWindowDelegate alloc] init]; globalWindowDel = [[GioWindowDelegate alloc] init];
// Runs until stopped by applicationDidFinishLaunching.
[NSApp run]; [NSApp run];
} }
} }
void gio_wakeupMainThread(void) {
@autoreleasepool {
NSEvent *dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSZeroPoint
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:dummy atStart:YES];
}
}