~eliasnaur/gio

b1f84da679190e7d5e889f77e6e737c710fbfd4e — Elias Naur 5 months ago 43024fc
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>
6 files changed, 129 insertions(+), 42 deletions(-)

M app/os_darwin.go
D app/os_darwin.m
M app/os_ios.go
M app/os_ios.m
M app/os_macos.go
M app/os_macos.m
M app/os_darwin.go => app/os_darwin.go +7 -10
@@ 15,8 15,8 @@ __attribute__ ((visibility ("hidden"))) void gio_hideCursor();
__attribute__ ((visibility ("hidden"))) void gio_showCursor();
__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);

static bool isMainThread() {
	return [NSThread isMainThread];
static int isMainThread() {
	return [NSThread isMainThread] ? 1 : 0;
}

static NSUInteger nsstringLength(CFTypeRef cstr) {


@@ 77,7 77,7 @@ var mainFuncs = make(chan func(), 1)

// runOnMain runs the function on the main thread.
func runOnMain(f func()) {
	if C.isMainThread() {
	if isMainThread() {
		f()
		return
	}


@@ 87,6 87,10 @@ func runOnMain(f func()) {
	}()
}

func isMainThread() bool {
	return C.isMainThread() != 0
}

//export gio_dispatchMainFuncs
func gio_dispatchMainFuncs() {
	for {


@@ 259,10 263,3 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
	C.gio_setCursor(C.NSUInteger(macosCursorID[to]))
	return to
}

func (w *window) wakeup() {
	runOnMain(func() {
		w.loop.Wakeup()
		w.loop.FlushEvents()
	})
}

D app/os_darwin.m => app/os_darwin.m +0 -11
@@ 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();
	});
}

M app/os_ios.go => app/os_ios.go +7 -0
@@ 396,5 396,12 @@ func gio_runMain() {
	runMain()
}

func (w *window) wakeup() {
	runOnMain(func() {
		w.loop.Wakeup()
		w.loop.FlushEvents()
	})
}

func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent()     {}

M app/os_ios.m => app/os_ios.m +6 -0
@@ 276,3 276,9 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
	GioView *v = (__bridge GioView *)viewRef;
	v.handle = handle;
}

void gio_wakeupMainThread(void) {
	dispatch_async(dispatch_get_main_queue(), ^{
		gio_dispatchMainFuncs();
	});
}

M app/os_macos.go => app/os_macos.go +80 -18
@@ 39,7 39,7 @@ import (
#define MOUSE_DOWN 3
#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_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);


@@ 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"



@@ 323,13 333,16 @@ type window struct {
	config Config
}

// launched is closed when applicationDidFinishLaunching is called.
// launched is closed after gio_initApp returns.
var launched = make(chan struct{})

// nextTopLeft is the offset to use for the next window's call to
// cascadeTopLeftFromPoint.
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 {
	return cgo.Handle(h).Value().(*window)
}


@@ 822,23 835,51 @@ func (w *window) draw() {

func (w *window) ProcessEvent(e event.Event) {
	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 {
	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() {
	if isMainThread() {
		mainThreadWindow = w
		defer func() { mainThreadWindow = nil }()
	}
	w.loop.Invalidate()
}

func (w *window) Run(f func()) {
	if isMainThread() {
		mainThreadWindow = w
		defer func() { mainThreadWindow = nil }()
	}
	w.loop.Run(f)
}

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 {


@@ 898,20 939,27 @@ func gio_onWindowed(h C.uintptr_t) {
	w.ProcessEvent(ConfigEvent{Config: w.config})
}

//export gio_onFinishLaunching
func gio_onFinishLaunching() {
	close(launched)
}

func newWindow(win *callbacks, options []Option) {
	<-launched
	res := make(chan struct{})
	runOnMain(func() {
		w := &window{
			redraw: make(chan struct{}, 1),
			w:      win,
	w := &window{
		redraw: make(chan struct{}, 1),
		w:      win,
	}
	w.loop = newEventLoop(w.w, w.wakeup)
	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)
		res <- struct{}{}
		if err := w.init(); err != nil {


@@ 965,7 1013,12 @@ func (w *window) init() error {
}

func osMain() {
	C.gio_main()
	C.gio_initApp()
	close(launched)
	for {
		C.dispatchEvent()
		gio_dispatchMainFuncs()
	}
}

func convertKey(k rune) (key.Name, bool) {


@@ 1052,5 1105,14 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
	return kmods
}

func (w *window) wakeup() {
	runOnMain(func() {
		w.loop.Wakeup()
		if w != mainThreadWindow {
			w.loop.FlushEvents()
		}
	})
}

func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent()     {}

M app/os_macos.m => app/os_macos.m +29 -3
@@ 348,7 348,6 @@ static BOOL isEqualNSCursor(NSCursor *c1, SEL name2) {
	// Don't pass commands up the responder chain.
	// They will end up in a beep.
}

- (BOOL)hasMarkedText {
	int res = gio_hasMarkedText(self.handle);
	return res ? YES : NO;


@@ 613,11 612,22 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
	[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
	[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

void gio_main() {
void gio_initApp() {
	@autoreleasepool {
		[GioApplication sharedApplication];
		GioAppDelegate *del = [[GioAppDelegate alloc] init];


@@ 641,6 651,22 @@ void gio_main() {

		globalWindowDel = [[GioWindowDelegate alloc] init];

		// Runs until stopped by applicationDidFinishLaunching.
		[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];
	}
}