mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
7337c06daf
This change generates keypress and release events for modifier keys in macOS. Specifically the Control, Alt, Shift and Command keys. Signed-off-by: Jeff Williams <kanobe@gmail.com>
1135 lines
28 KiB
Go
1135 lines
28 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
//go:build darwin && !ios
|
|
// +build darwin,!ios
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"io"
|
|
"runtime"
|
|
"runtime/cgo"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"gioui.org/internal/f32"
|
|
"gioui.org/io/event"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/io/system"
|
|
"gioui.org/io/transfer"
|
|
"gioui.org/op"
|
|
"gioui.org/unit"
|
|
|
|
_ "gioui.org/internal/cocoainit"
|
|
)
|
|
|
|
/*
|
|
#cgo CFLAGS: -Werror -Wno-deprecated-declarations -fobjc-arc -x objective-c
|
|
#cgo LDFLAGS: -framework AppKit -framework QuartzCore
|
|
|
|
#include <AppKit/AppKit.h>
|
|
|
|
#define MOUSE_MOVE 1
|
|
#define MOUSE_UP 2
|
|
#define MOUSE_DOWN 3
|
|
#define MOUSE_SCROLL 4
|
|
|
|
#define KEY_DELETE_BACKWARD 0x7f
|
|
#define KEY_RETURN 0xd
|
|
#define KEY_ENTER 0x3
|
|
#define KEY_ESCAPE 0x1b
|
|
#define KEY_TAB 0x09
|
|
|
|
__attribute__ ((visibility ("hidden"))) void gio_main(void);
|
|
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans);
|
|
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
|
|
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
|
|
|
|
static void writeClipboard(CFTypeRef str) {
|
|
@autoreleasepool {
|
|
NSString *s = (__bridge NSString *)str;
|
|
NSPasteboard *p = NSPasteboard.generalPasteboard;
|
|
[p declareTypes:@[NSPasteboardTypeString] owner:nil];
|
|
[p setString:s forType:NSPasteboardTypeString];
|
|
}
|
|
}
|
|
|
|
static CFTypeRef readClipboard(void) {
|
|
@autoreleasepool {
|
|
NSPasteboard *p = NSPasteboard.generalPasteboard;
|
|
NSString *content = [p stringForType:NSPasteboardTypeString];
|
|
return (__bridge_retained CFTypeRef)content;
|
|
}
|
|
}
|
|
|
|
static CGFloat viewHeight(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
NSView *view = (__bridge NSView *)viewRef;
|
|
return [view bounds].size.height;
|
|
}
|
|
}
|
|
|
|
static CGFloat viewWidth(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
NSView *view = (__bridge NSView *)viewRef;
|
|
return [view bounds].size.width;
|
|
}
|
|
}
|
|
|
|
static CGFloat getScreenBackingScale(void) {
|
|
@autoreleasepool {
|
|
return [NSScreen.mainScreen backingScaleFactor];
|
|
}
|
|
}
|
|
|
|
static CGFloat getViewBackingScale(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
NSView *view = (__bridge NSView *)viewRef;
|
|
return [view.window backingScaleFactor];
|
|
}
|
|
}
|
|
|
|
static void setNeedsDisplay(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
NSView *view = (__bridge NSView *)viewRef;
|
|
[view setNeedsDisplay:YES];
|
|
}
|
|
}
|
|
|
|
static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
return [window cascadeTopLeftFromPoint:topLeft];
|
|
}
|
|
}
|
|
|
|
static void makeKeyAndOrderFront(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
[window makeKeyAndOrderFront:nil];
|
|
}
|
|
}
|
|
|
|
static void makeFirstResponder(CFTypeRef windowRef, CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
NSView *view = (__bridge NSView *)viewRef;
|
|
[window makeFirstResponder:view];
|
|
}
|
|
}
|
|
|
|
static void toggleFullScreen(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
[window toggleFullScreen:nil];
|
|
}
|
|
}
|
|
|
|
static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
return [window styleMask];
|
|
}
|
|
}
|
|
|
|
static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
window.styleMask = mask;
|
|
}
|
|
}
|
|
|
|
static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
window.titleVisibility = state;
|
|
}
|
|
}
|
|
|
|
static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
window.titlebarAppearsTransparent = (BOOL)transparent;
|
|
}
|
|
}
|
|
|
|
static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
[window standardWindowButton:btn].hidden = (BOOL)hide;
|
|
}
|
|
}
|
|
|
|
static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
|
|
}
|
|
}
|
|
|
|
static void closeWindow(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
[window performClose:nil];
|
|
}
|
|
}
|
|
|
|
static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
|
@autoreleasepool {
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
NSSize size = NSMakeSize(width, height);
|
|
[window setContentSize:size];
|
|
}
|
|
}
|
|
|
|
static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
|
@autoreleasepool {
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
window.contentMinSize = NSMakeSize(width, height);
|
|
}
|
|
}
|
|
|
|
static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
|
@autoreleasepool {
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
window.contentMaxSize = NSMakeSize(width, height);
|
|
}
|
|
}
|
|
|
|
static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
|
|
@autoreleasepool {
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
NSRect r = NSMakeRect(x, y, w, h);
|
|
[window setFrame:r display:YES];
|
|
}
|
|
}
|
|
|
|
static void resetLayerFrame(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
NSView* view = (__bridge NSView *)viewRef;
|
|
NSRect r = view.frame;
|
|
view.layer.frame = r;
|
|
}
|
|
}
|
|
|
|
static void hideWindow(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
[window miniaturize:window];
|
|
}
|
|
}
|
|
|
|
static void unhideWindow(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
[window deminiaturize:window];
|
|
}
|
|
}
|
|
|
|
static NSRect getScreenFrame(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
return [[window screen] frame];
|
|
}
|
|
}
|
|
|
|
static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
window.title = (__bridge NSString *)titleRef;
|
|
}
|
|
}
|
|
|
|
static int isWindowZoomed(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
return window.zoomed ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
static int isWindowMiniaturized(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
return window.miniaturized ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
static void zoomWindow(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
|
[window zoom:nil];
|
|
}
|
|
}
|
|
|
|
static CFTypeRef layerForView(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
NSView *view = (__bridge NSView *)viewRef;
|
|
return (__bridge CFTypeRef)view.layer;
|
|
}
|
|
}
|
|
|
|
static CFTypeRef windowForView(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
NSView *view = (__bridge NSView *)viewRef;
|
|
return (__bridge CFTypeRef)view.window;
|
|
}
|
|
}
|
|
|
|
static void raiseWindow(CFTypeRef windowRef) {
|
|
@autoreleasepool {
|
|
NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
|
|
if (![currentApp isActive]) {
|
|
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
|
|
}
|
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
|
[window makeKeyAndOrderFront:nil];
|
|
}
|
|
}
|
|
|
|
static CFTypeRef createInputContext(CFTypeRef clientRef) {
|
|
@autoreleasepool {
|
|
id<NSTextInputClient> client = (__bridge id<NSTextInputClient>)clientRef;
|
|
NSTextInputContext *ctx = [[NSTextInputContext alloc] initWithClient:client];
|
|
return CFBridgingRetain(ctx);
|
|
}
|
|
}
|
|
|
|
static void discardMarkedText(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
|
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
|
if (view == [ctx client]) {
|
|
[ctx discardMarkedText];
|
|
}
|
|
}
|
|
}
|
|
|
|
static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
|
|
@autoreleasepool {
|
|
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
|
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
|
if (view == [ctx client]) {
|
|
[ctx invalidateCharacterCoordinates];
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
import "C"
|
|
|
|
func init() {
|
|
// Darwin requires that UI operations happen on the main thread only.
|
|
runtime.LockOSThread()
|
|
}
|
|
|
|
// AppKitViewEvent notifies the client of changes to the window AppKit handles.
|
|
// The handles are retained until another AppKitViewEvent is sent.
|
|
type AppKitViewEvent struct {
|
|
// View is a CFTypeRef for the NSView for the window.
|
|
View uintptr
|
|
// Layer is a CFTypeRef of the CALayer of View.
|
|
Layer uintptr
|
|
}
|
|
|
|
type window struct {
|
|
view C.CFTypeRef
|
|
w *callbacks
|
|
anim bool
|
|
displayLink *displayLink
|
|
// redraw is a single entry channel for making sure only one
|
|
// display link redraw request is in flight.
|
|
redraw chan struct{}
|
|
cursor pointer.Cursor
|
|
pointerBtns pointer.Buttons
|
|
loop *eventLoop
|
|
lastMods C.NSUInteger
|
|
|
|
scale float32
|
|
config Config
|
|
|
|
cmdKeysDown map[key.Name]struct{}
|
|
}
|
|
|
|
// launched is closed when applicationDidFinishLaunching is called.
|
|
var launched = make(chan struct{})
|
|
|
|
// nextTopLeft is the offset to use for the next window's call to
|
|
// cascadeTopLeftFromPoint.
|
|
var nextTopLeft C.NSPoint
|
|
|
|
func windowFor(h C.uintptr_t) *window {
|
|
return cgo.Handle(h).Value().(*window)
|
|
}
|
|
|
|
func (w *window) contextView() C.CFTypeRef {
|
|
return w.view
|
|
}
|
|
|
|
func (w *window) ReadClipboard() {
|
|
cstr := C.readClipboard()
|
|
if cstr != 0 {
|
|
defer C.CFRelease(cstr)
|
|
}
|
|
content := nsstringToString(cstr)
|
|
w.ProcessEvent(transfer.DataEvent{
|
|
Type: "application/text",
|
|
Open: func() io.ReadCloser {
|
|
return io.NopCloser(strings.NewReader(content))
|
|
},
|
|
})
|
|
}
|
|
|
|
func (w *window) WriteClipboard(mime string, s []byte) {
|
|
cstr := stringToNSString(string(s))
|
|
defer C.CFRelease(cstr)
|
|
C.writeClipboard(cstr)
|
|
}
|
|
|
|
func (w *window) updateWindowMode() {
|
|
w.scale = float32(C.getViewBackingScale(w.view))
|
|
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
|
|
w.config.Size = image.Point{
|
|
X: int(wf*w.scale + .5),
|
|
Y: int(hf*w.scale + .5),
|
|
}
|
|
w.config.Mode = Windowed
|
|
window := C.windowForView(w.view)
|
|
if window == 0 {
|
|
return
|
|
}
|
|
style := int(C.getWindowStyleMask(C.windowForView(w.view)))
|
|
switch {
|
|
case style&C.NSWindowStyleMaskFullScreen != 0:
|
|
w.config.Mode = Fullscreen
|
|
case C.isWindowZoomed(window) != 0:
|
|
w.config.Mode = Maximized
|
|
}
|
|
w.config.Decorated = style&C.NSWindowStyleMaskFullSizeContentView == 0
|
|
}
|
|
|
|
func (w *window) Configure(options []Option) {
|
|
screenScale := float32(C.getScreenBackingScale())
|
|
cfg := configFor(screenScale)
|
|
cnf := w.config
|
|
cnf.apply(cfg, options)
|
|
window := C.windowForView(w.view)
|
|
|
|
mask := C.getWindowStyleMask(window)
|
|
fullscreen := mask&C.NSWindowStyleMaskFullScreen != 0
|
|
switch cnf.Mode {
|
|
case Fullscreen:
|
|
if C.isWindowMiniaturized(window) != 0 {
|
|
C.unhideWindow(window)
|
|
}
|
|
if !fullscreen {
|
|
C.toggleFullScreen(window)
|
|
}
|
|
case Minimized:
|
|
C.hideWindow(window)
|
|
case Maximized:
|
|
if C.isWindowMiniaturized(window) != 0 {
|
|
C.unhideWindow(window)
|
|
}
|
|
if fullscreen {
|
|
C.toggleFullScreen(window)
|
|
}
|
|
w.setTitle(cnf.Title)
|
|
if C.isWindowZoomed(window) == 0 {
|
|
C.zoomWindow(window)
|
|
}
|
|
case Windowed:
|
|
if C.isWindowMiniaturized(window) != 0 {
|
|
C.unhideWindow(window)
|
|
}
|
|
if fullscreen {
|
|
C.toggleFullScreen(window)
|
|
}
|
|
w.setTitle(cnf.Title)
|
|
w.config.Size = cnf.Size
|
|
cnf.Size = cnf.Size.Div(int(screenScale))
|
|
C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y))
|
|
w.config.MinSize = cnf.MinSize
|
|
cnf.MinSize = cnf.MinSize.Div(int(screenScale))
|
|
C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y))
|
|
w.config.MaxSize = cnf.MaxSize
|
|
cnf.MaxSize = cnf.MaxSize.Div(int(screenScale))
|
|
if cnf.MaxSize != (image.Point{}) {
|
|
C.setMaxSize(window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y))
|
|
}
|
|
if C.isWindowZoomed(window) != 0 {
|
|
C.zoomWindow(window)
|
|
}
|
|
}
|
|
style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable)
|
|
style = C.NSWindowStyleMaskFullSizeContentView
|
|
mask &^= style
|
|
barTrans := C.int(C.NO)
|
|
titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible)
|
|
if !cnf.Decorated {
|
|
mask |= style
|
|
barTrans = C.YES
|
|
titleVis = C.NSWindowTitleHidden
|
|
}
|
|
C.setWindowTitlebarAppearsTransparent(window, barTrans)
|
|
C.setWindowTitleVisibility(window, titleVis)
|
|
C.setWindowStyleMask(window, mask)
|
|
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
|
|
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
|
|
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
|
|
// When toggling the titlebar, the layer doesn't update its frame
|
|
// until the next resize. Force it.
|
|
C.resetLayerFrame(w.view)
|
|
}
|
|
|
|
func (w *window) setTitle(title string) {
|
|
w.config.Title = title
|
|
titleC := stringToNSString(title)
|
|
defer C.CFRelease(titleC)
|
|
C.setTitle(C.windowForView(w.view), titleC)
|
|
}
|
|
|
|
func (w *window) Perform(acts system.Action) {
|
|
window := C.windowForView(w.view)
|
|
walkActions(acts, func(a system.Action) {
|
|
switch a {
|
|
case system.ActionCenter:
|
|
r := C.getScreenFrame(window) // the screen size of the window
|
|
screenScale := float32(C.getScreenBackingScale())
|
|
sz := w.config.Size.Div(int(screenScale))
|
|
x := (int(r.size.width) - sz.X) / 2
|
|
y := (int(r.size.height) - sz.Y) / 2
|
|
C.setScreenFrame(window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y))
|
|
case system.ActionRaise:
|
|
C.raiseWindow(window)
|
|
}
|
|
})
|
|
if acts&system.ActionClose != 0 {
|
|
C.closeWindow(window)
|
|
}
|
|
}
|
|
|
|
func (w *window) SetCursor(cursor pointer.Cursor) {
|
|
w.cursor = windowSetCursor(w.cursor, cursor)
|
|
}
|
|
|
|
func (w *window) EditorStateChanged(old, new editorState) {
|
|
if old.Selection.Range != new.Selection.Range || old.Snippet != new.Snippet {
|
|
C.discardMarkedText(w.view)
|
|
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
|
}
|
|
if old.Selection.Caret != new.Selection.Caret || old.Selection.Transform != new.Selection.Transform {
|
|
C.invalidateCharacterCoordinates(w.view)
|
|
}
|
|
}
|
|
|
|
func (w *window) ShowTextInput(show bool) {}
|
|
|
|
func (w *window) SetInputHint(_ key.InputHint) {}
|
|
|
|
func (w *window) SetAnimating(anim bool) {
|
|
w.anim = anim
|
|
window := C.windowForView(w.view)
|
|
if w.anim && window != 0 {
|
|
w.displayLink.Start()
|
|
} else {
|
|
w.displayLink.Stop()
|
|
}
|
|
}
|
|
|
|
func (w *window) runOnMain(f func()) {
|
|
runOnMain(func() {
|
|
// Make sure the view is still valid. The window might've been closed
|
|
// during the switch to the main thread.
|
|
if w.view != 0 {
|
|
f()
|
|
}
|
|
})
|
|
}
|
|
|
|
//export gio_onKeys
|
|
func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
|
|
str := nsstringToString(cstr)
|
|
kmods := convertMods(mods)
|
|
ks := key.Release
|
|
if keyDown {
|
|
ks = key.Press
|
|
}
|
|
w := windowFor(h)
|
|
for _, k := range str {
|
|
n, ok := convertKey(k)
|
|
if !ok && ks == key.Release {
|
|
// Key release events are not reported by onCommandBySelector.
|
|
n, ok = convertCommandKey(k)
|
|
if ok {
|
|
_, ok = w.cmdKeysDown[n]
|
|
delete(w.cmdKeysDown, n)
|
|
}
|
|
}
|
|
if ok {
|
|
w.ProcessEvent(key.Event{
|
|
Name: n,
|
|
Modifiers: kmods,
|
|
State: ks,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
//export gio_onCommandBySelector
|
|
func gio_onCommandBySelector(h C.uintptr_t, k C.int, ti C.double, mods C.NSUInteger) {
|
|
w := windowFor(h)
|
|
w.commandKey(rune(k), ti, mods)
|
|
}
|
|
|
|
func (w *window) commandKey(k rune, ti C.double, mods C.NSUInteger) bool {
|
|
n, ok := convertCommandKey(k)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if w.cmdKeysDown == nil {
|
|
w.cmdKeysDown = make(map[key.Name]struct{})
|
|
}
|
|
w.cmdKeysDown[n] = struct{}{}
|
|
w.ProcessEvent(key.Event{
|
|
Modifiers: convertMods(mods),
|
|
Name: n,
|
|
State: key.Press,
|
|
})
|
|
return true
|
|
}
|
|
|
|
//export gio_onFlagsChanged
|
|
func gio_onFlagsChanged(h C.uintptr_t, curMods C.NSUInteger) {
|
|
w := windowFor(h)
|
|
|
|
mods := []C.NSUInteger{C.NSControlKeyMask, C.NSAlternateKeyMask, C.NSShiftKeyMask, C.NSCommandKeyMask}
|
|
keys := []key.Name{key.NameCtrl, key.NameAlt, key.NameShift, key.NameCommand}
|
|
|
|
for i, mod := range mods {
|
|
wasPressed := w.lastMods&mod != 0
|
|
isPressed := curMods&mod != 0
|
|
|
|
if wasPressed != isPressed {
|
|
st := key.Release
|
|
if isPressed {
|
|
st = key.Press
|
|
}
|
|
w.ProcessEvent(key.Event{
|
|
Name: keys[i],
|
|
State: st,
|
|
})
|
|
}
|
|
}
|
|
|
|
w.lastMods = curMods
|
|
}
|
|
|
|
//export gio_onText
|
|
func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) {
|
|
str := nsstringToString(cstr)
|
|
w := windowFor(h)
|
|
w.w.EditorInsert(str)
|
|
}
|
|
|
|
//export gio_onMouse
|
|
func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
|
|
w := windowFor(h)
|
|
t := time.Duration(float64(ti)*float64(time.Second) + .5)
|
|
xf, yf := float32(x)*w.scale, float32(y)*w.scale
|
|
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
|
|
pos := f32.Point{X: xf, Y: yf}
|
|
var btn pointer.Buttons
|
|
switch cbtn {
|
|
case 0:
|
|
btn = pointer.ButtonPrimary
|
|
case 1:
|
|
btn = pointer.ButtonSecondary
|
|
case 2:
|
|
btn = pointer.ButtonTertiary
|
|
}
|
|
var typ pointer.Kind
|
|
switch cdir {
|
|
case C.MOUSE_MOVE:
|
|
typ = pointer.Move
|
|
case C.MOUSE_UP:
|
|
typ = pointer.Release
|
|
w.pointerBtns &^= btn
|
|
case C.MOUSE_DOWN:
|
|
typ = pointer.Press
|
|
w.pointerBtns |= btn
|
|
act, ok := w.w.ActionAt(pos)
|
|
if ok && w.config.Mode != Fullscreen {
|
|
switch act {
|
|
case system.ActionMove:
|
|
C.performWindowDragWithEvent(C.windowForView(w.view), evt)
|
|
return
|
|
}
|
|
}
|
|
case C.MOUSE_SCROLL:
|
|
typ = pointer.Scroll
|
|
default:
|
|
panic("invalid direction")
|
|
}
|
|
w.ProcessEvent(pointer.Event{
|
|
Kind: typ,
|
|
Source: pointer.Mouse,
|
|
Time: t,
|
|
Buttons: w.pointerBtns,
|
|
Position: pos,
|
|
Scroll: f32.Point{X: dxf, Y: dyf},
|
|
Modifiers: convertMods(mods),
|
|
})
|
|
}
|
|
|
|
//export gio_onDraw
|
|
func gio_onDraw(h C.uintptr_t) {
|
|
w := windowFor(h)
|
|
w.draw()
|
|
}
|
|
|
|
//export gio_onFocus
|
|
func gio_onFocus(h C.uintptr_t, focus C.int) {
|
|
w := windowFor(h)
|
|
w.SetCursor(w.cursor)
|
|
w.config.Focused = focus == 1
|
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
|
}
|
|
|
|
//export gio_onChangeScreen
|
|
func gio_onChangeScreen(h C.uintptr_t, did uint64) {
|
|
w := windowFor(h)
|
|
w.displayLink.SetDisplayID(did)
|
|
C.setNeedsDisplay(w.view)
|
|
}
|
|
|
|
//export gio_hasMarkedText
|
|
func gio_hasMarkedText(h C.uintptr_t) C.int {
|
|
w := windowFor(h)
|
|
state := w.w.EditorState()
|
|
if state.compose.Start != -1 {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
//export gio_markedRange
|
|
func gio_markedRange(h C.uintptr_t) C.NSRange {
|
|
w := windowFor(h)
|
|
state := w.w.EditorState()
|
|
rng := state.compose
|
|
start, end := rng.Start, rng.End
|
|
if start == -1 {
|
|
return C.NSMakeRange(C.NSNotFound, 0)
|
|
}
|
|
u16start := state.UTF16Index(start)
|
|
return C.NSMakeRange(
|
|
C.NSUInteger(u16start),
|
|
C.NSUInteger(state.UTF16Index(end)-u16start),
|
|
)
|
|
}
|
|
|
|
//export gio_selectedRange
|
|
func gio_selectedRange(h C.uintptr_t) C.NSRange {
|
|
w := windowFor(h)
|
|
state := w.w.EditorState()
|
|
rng := state.Selection
|
|
start, end := rng.Start, rng.End
|
|
if start > end {
|
|
start, end = end, start
|
|
}
|
|
u16start := state.UTF16Index(start)
|
|
return C.NSMakeRange(
|
|
C.NSUInteger(u16start),
|
|
C.NSUInteger(state.UTF16Index(end)-u16start),
|
|
)
|
|
}
|
|
|
|
//export gio_unmarkText
|
|
func gio_unmarkText(h C.uintptr_t) {
|
|
w := windowFor(h)
|
|
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
|
}
|
|
|
|
//export gio_setMarkedText
|
|
func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
|
|
w := windowFor(h)
|
|
str := nsstringToString(cstr)
|
|
state := w.w.EditorState()
|
|
rng := state.compose
|
|
if rng.Start == -1 {
|
|
rng = state.Selection.Range
|
|
}
|
|
if replaceRange.location != C.NSNotFound {
|
|
// replaceRange is relative to marked (or selected) text.
|
|
offset := state.UTF16Index(rng.Start)
|
|
start := state.RunesIndex(int(replaceRange.location) + offset)
|
|
end := state.RunesIndex(int(replaceRange.location+replaceRange.length) + offset)
|
|
rng = key.Range{
|
|
Start: start,
|
|
End: end,
|
|
}
|
|
}
|
|
w.w.EditorReplace(rng, str)
|
|
comp := key.Range{
|
|
Start: rng.Start,
|
|
End: rng.Start + utf8.RuneCountInString(str),
|
|
}
|
|
w.w.SetComposingRegion(comp)
|
|
|
|
sel := key.Range{Start: comp.End, End: comp.End}
|
|
if selRange.location != C.NSNotFound {
|
|
// selRange is relative to inserted text.
|
|
offset := state.UTF16Index(rng.Start)
|
|
start := state.RunesIndex(int(selRange.location) + offset)
|
|
end := state.RunesIndex(int(selRange.location+selRange.length) + offset)
|
|
sel = key.Range{
|
|
Start: start,
|
|
End: end,
|
|
}
|
|
}
|
|
w.w.SetEditorSelection(sel)
|
|
}
|
|
|
|
//export gio_substringForProposedRange
|
|
func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
|
|
w := windowFor(h)
|
|
state := w.w.EditorState()
|
|
start, end := state.Snippet.Start, state.Snippet.End
|
|
if start > end {
|
|
start, end = end, start
|
|
}
|
|
rng := key.Range{
|
|
Start: state.RunesIndex(int(crng.location)),
|
|
End: state.RunesIndex(int(crng.location + crng.length)),
|
|
}
|
|
if rng.Start < start || end < rng.End {
|
|
w.w.SetEditorSnippet(rng)
|
|
}
|
|
u16start := state.UTF16Index(start)
|
|
actual.location = C.NSUInteger(u16start)
|
|
actual.length = C.NSUInteger(state.UTF16Index(end) - u16start)
|
|
return stringToNSString(state.Snippet.Text)
|
|
}
|
|
|
|
//export gio_insertText
|
|
func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange, ti C.double, mods C.NSUInteger) {
|
|
w := windowFor(h)
|
|
str := nsstringToString(cstr)
|
|
// macOS IME in some cases calls insertText for command keys such as backspace
|
|
// instead of doCommandBySelector.
|
|
hadCommands := false
|
|
for _, r := range str {
|
|
hadCommands = w.commandKey(r, ti, mods) || hadCommands
|
|
}
|
|
if hadCommands {
|
|
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
|
return
|
|
}
|
|
state := w.w.EditorState()
|
|
rng := state.compose
|
|
if rng.Start == -1 {
|
|
rng = state.Selection.Range
|
|
}
|
|
if crng.location != C.NSNotFound {
|
|
rng = key.Range{
|
|
Start: state.RunesIndex(int(crng.location)),
|
|
End: state.RunesIndex(int(crng.location + crng.length)),
|
|
}
|
|
}
|
|
w.w.EditorReplace(rng, str)
|
|
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
|
start := rng.Start
|
|
if rng.End < start {
|
|
start = rng.End
|
|
}
|
|
pos := start + utf8.RuneCountInString(str)
|
|
w.w.SetEditorSelection(key.Range{Start: pos, End: pos})
|
|
}
|
|
|
|
//export gio_characterIndexForPoint
|
|
func gio_characterIndexForPoint(h C.uintptr_t, p C.NSPoint) C.NSUInteger {
|
|
return C.NSNotFound
|
|
}
|
|
|
|
//export gio_firstRectForCharacterRange
|
|
func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
|
|
w := windowFor(h)
|
|
state := w.w.EditorState()
|
|
sel := state.Selection
|
|
u16start := state.UTF16Index(sel.Start)
|
|
actual.location = C.NSUInteger(u16start)
|
|
actual.length = 0
|
|
// Transform to NSView local coordinates (lower left origin, undo backing scale).
|
|
scale := 1. / float32(C.getViewBackingScale(w.view))
|
|
height := float32(C.viewHeight(w.view))
|
|
local := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(scale, -scale)).Offset(f32.Pt(0, height))
|
|
t := local.Mul(sel.Transform)
|
|
bounds := f32.Rectangle{
|
|
Min: t.Transform(sel.Pos.Sub(f32.Pt(0, sel.Ascent))),
|
|
Max: t.Transform(sel.Pos.Add(f32.Pt(0, sel.Descent))),
|
|
}.Canon()
|
|
sz := bounds.Size()
|
|
return C.NSMakeRect(
|
|
C.CGFloat(bounds.Min.X), C.CGFloat(bounds.Min.Y),
|
|
C.CGFloat(sz.X), C.CGFloat(sz.Y),
|
|
)
|
|
}
|
|
|
|
func (w *window) draw() {
|
|
cnf := w.config
|
|
w.updateWindowMode()
|
|
if w.config != cnf {
|
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
|
}
|
|
select {
|
|
case <-w.redraw:
|
|
default:
|
|
}
|
|
if w.anim {
|
|
w.SetAnimating(w.anim)
|
|
}
|
|
sz := w.config.Size
|
|
if sz.X == 0 || sz.Y == 0 {
|
|
return
|
|
}
|
|
cfg := configFor(w.scale)
|
|
w.ProcessEvent(frameEvent{
|
|
FrameEvent: FrameEvent{
|
|
Now: time.Now(),
|
|
Size: sz,
|
|
Metric: cfg,
|
|
},
|
|
Sync: true,
|
|
})
|
|
}
|
|
|
|
func (w *window) ProcessEvent(e event.Event) {
|
|
w.w.ProcessEvent(e)
|
|
w.loop.FlushEvents()
|
|
}
|
|
|
|
func (w *window) Event() event.Event {
|
|
return w.loop.Event()
|
|
}
|
|
|
|
func (w *window) Invalidate() {
|
|
w.loop.Invalidate()
|
|
}
|
|
|
|
func (w *window) Run(f func()) {
|
|
w.loop.Run(f)
|
|
}
|
|
|
|
func (w *window) Frame(frame *op.Ops) {
|
|
w.loop.Frame(frame)
|
|
}
|
|
|
|
func configFor(scale float32) unit.Metric {
|
|
return unit.Metric{
|
|
PxPerDp: scale,
|
|
PxPerSp: scale,
|
|
}
|
|
}
|
|
|
|
//export gio_onAttached
|
|
func gio_onAttached(h C.uintptr_t, attached C.int) {
|
|
w := windowFor(h)
|
|
if attached != 0 {
|
|
layer := C.layerForView(w.view)
|
|
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
|
|
} else {
|
|
w.ProcessEvent(AppKitViewEvent{})
|
|
w.SetAnimating(w.anim)
|
|
}
|
|
}
|
|
|
|
//export gio_onDestroy
|
|
func gio_onDestroy(h C.uintptr_t) {
|
|
w := windowFor(h)
|
|
w.ProcessEvent(DestroyEvent{})
|
|
w.displayLink.Close()
|
|
w.displayLink = nil
|
|
cgo.Handle(h).Delete()
|
|
w.view = 0
|
|
}
|
|
|
|
//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.loop = newEventLoop(w.w, w.wakeup)
|
|
win.SetDriver(w)
|
|
res <- struct{}{}
|
|
var cnf Config
|
|
cnf.apply(unit.Metric{}, options)
|
|
if err := w.init(cnf.CustomRenderer); err != nil {
|
|
w.ProcessEvent(DestroyEvent{Err: err})
|
|
return
|
|
}
|
|
window := C.gio_createWindow(w.view, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y), 0, 0, 0, 0)
|
|
// Release our reference now that the NSWindow has it.
|
|
C.CFRelease(w.view)
|
|
w.Configure(options)
|
|
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
|
|
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
|
|
// and just returns the offset we need for the first window.
|
|
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
|
|
}
|
|
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
|
|
C.makeFirstResponder(window, w.view)
|
|
// makeKeyAndOrderFront assumes ownership of our window reference.
|
|
C.makeKeyAndOrderFront(window)
|
|
})
|
|
<-res
|
|
}
|
|
|
|
func (w *window) init(customRenderer bool) error {
|
|
presentWithTrans := 1
|
|
if customRenderer {
|
|
presentWithTrans = 0
|
|
}
|
|
view := C.gio_createView(C.int(presentWithTrans))
|
|
if view == 0 {
|
|
return errors.New("newOSWindow: failed to create view")
|
|
}
|
|
scale := float32(C.getViewBackingScale(view))
|
|
w.scale = scale
|
|
dl, err := newDisplayLink(func() {
|
|
select {
|
|
case w.redraw <- struct{}{}:
|
|
default:
|
|
return
|
|
}
|
|
w.runOnMain(func() {
|
|
C.setNeedsDisplay(w.view)
|
|
})
|
|
})
|
|
w.displayLink = dl
|
|
if err != nil {
|
|
C.CFRelease(view)
|
|
return err
|
|
}
|
|
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
|
|
w.view = view
|
|
return nil
|
|
}
|
|
|
|
func osMain() {
|
|
if !isMainThread() {
|
|
panic("app.Main must run on the main goroutine")
|
|
}
|
|
C.gio_main()
|
|
}
|
|
|
|
func convertCommandKey(k rune) (key.Name, bool) {
|
|
var n key.Name
|
|
switch k {
|
|
case C.KEY_ESCAPE:
|
|
n = key.NameEscape
|
|
case C.NSLeftArrowFunctionKey:
|
|
n = key.NameLeftArrow
|
|
case C.NSRightArrowFunctionKey:
|
|
n = key.NameRightArrow
|
|
case C.NSUpArrowFunctionKey:
|
|
n = key.NameUpArrow
|
|
case C.NSDownArrowFunctionKey:
|
|
n = key.NameDownArrow
|
|
case C.KEY_RETURN:
|
|
n = key.NameReturn
|
|
case C.KEY_ENTER:
|
|
n = key.NameEnter
|
|
case C.NSHomeFunctionKey:
|
|
n = key.NameHome
|
|
case C.NSEndFunctionKey:
|
|
n = key.NameEnd
|
|
case C.KEY_DELETE_BACKWARD, 0x08:
|
|
n = key.NameDeleteBackward
|
|
case C.NSDeleteFunctionKey:
|
|
n = key.NameDeleteForward
|
|
case C.KEY_TAB, 0x19:
|
|
n = key.NameTab
|
|
case C.NSPageUpFunctionKey:
|
|
n = key.NamePageUp
|
|
case C.NSPageDownFunctionKey:
|
|
n = key.NamePageDown
|
|
default:
|
|
return "", false
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
func convertKey(k rune) (key.Name, bool) {
|
|
var n key.Name
|
|
switch k {
|
|
case C.NSF1FunctionKey:
|
|
n = key.NameF1
|
|
case C.NSF2FunctionKey:
|
|
n = key.NameF2
|
|
case C.NSF3FunctionKey:
|
|
n = key.NameF3
|
|
case C.NSF4FunctionKey:
|
|
n = key.NameF4
|
|
case C.NSF5FunctionKey:
|
|
n = key.NameF5
|
|
case C.NSF6FunctionKey:
|
|
n = key.NameF6
|
|
case C.NSF7FunctionKey:
|
|
n = key.NameF7
|
|
case C.NSF8FunctionKey:
|
|
n = key.NameF8
|
|
case C.NSF9FunctionKey:
|
|
n = key.NameF9
|
|
case C.NSF10FunctionKey:
|
|
n = key.NameF10
|
|
case C.NSF11FunctionKey:
|
|
n = key.NameF11
|
|
case C.NSF12FunctionKey:
|
|
n = key.NameF12
|
|
case 0x20:
|
|
n = key.NameSpace
|
|
default:
|
|
k = unicode.ToUpper(k)
|
|
if !unicode.IsPrint(k) {
|
|
return "", false
|
|
}
|
|
n = key.Name(k)
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
func convertMods(mods C.NSUInteger) key.Modifiers {
|
|
var kmods key.Modifiers
|
|
if mods&C.NSAlternateKeyMask != 0 {
|
|
kmods |= key.ModAlt
|
|
}
|
|
if mods&C.NSControlKeyMask != 0 {
|
|
kmods |= key.ModCtrl
|
|
}
|
|
if mods&C.NSCommandKeyMask != 0 {
|
|
kmods |= key.ModCommand
|
|
}
|
|
if mods&C.NSShiftKeyMask != 0 {
|
|
kmods |= key.ModShift
|
|
}
|
|
return kmods
|
|
}
|
|
|
|
func (AppKitViewEvent) implementsViewEvent() {}
|
|
func (AppKitViewEvent) ImplementsEvent() {}
|
|
func (a AppKitViewEvent) Valid() bool {
|
|
return a != (AppKitViewEvent{})
|
|
}
|