// SPDX-License-Identifier: Unlicense OR MIT //go:build darwin && !ios // +build darwin,!ios package app import ( "errors" "image" "runtime" "time" "unicode" "unicode/utf16" "unsafe" "gioui.org/f32" "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/unit" _ "gioui.org/internal/cocoainit" ) /* #cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c #include #define MOUSE_MOVE 1 #define MOUSE_UP 2 #define MOUSE_DOWN 3 #define MOUSE_SCROLL 4 __attribute__ ((visibility ("hidden"))) void gio_main(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); static void writeClipboard(unichar *chars, NSUInteger length) { @autoreleasepool { NSString *s = [NSString string]; if (length > 0) { s = [NSString stringWithCharacters:chars length:length]; } 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) { NSView *view = (__bridge NSView *)viewRef; return [view bounds].size.height; } static CGFloat viewWidth(CFTypeRef viewRef) { NSView *view = (__bridge NSView *)viewRef; return [view bounds].size.width; } static CGFloat getScreenBackingScale(void) { return [NSScreen.mainScreen backingScaleFactor]; } static CGFloat getViewBackingScale(CFTypeRef viewRef) { NSView *view = (__bridge NSView *)viewRef; return [view.window backingScaleFactor]; } static void setNeedsDisplay(CFTypeRef viewRef) { NSView *view = (__bridge NSView *)viewRef; [view setNeedsDisplay:YES]; } static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) { NSWindow *window = (__bridge NSWindow *)windowRef; return [window cascadeTopLeftFromPoint:topLeft]; } static void makeKeyAndOrderFront(CFTypeRef windowRef) { NSWindow *window = (__bridge NSWindow *)windowRef; [window makeKeyAndOrderFront:nil]; } static void toggleFullScreen(CFTypeRef windowRef) { NSWindow *window = (__bridge NSWindow *)windowRef; [window toggleFullScreen:nil]; } static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) { NSWindow *window = (__bridge NSWindow *)windowRef; return [window styleMask]; } static void closeWindow(CFTypeRef windowRef) { NSWindow* window = (__bridge NSWindow *)windowRef; [window performClose:nil]; } static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { NSWindow* window = (__bridge NSWindow *)windowRef; NSSize size = NSMakeSize(width, height); [window setContentSize:size]; } static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { NSWindow* window = (__bridge NSWindow *)windowRef; window.contentMinSize = NSMakeSize(width, height); } static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { NSWindow* window = (__bridge NSWindow *)windowRef; window.contentMaxSize = NSMakeSize(width, height); } static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) { NSWindow* window = (__bridge NSWindow *)windowRef; NSRect r = NSMakeRect(x, y, w, h); [window setFrame:r display:YES]; } static NSRect getScreenFrame(CFTypeRef windowRef) { NSWindow* window = (__bridge NSWindow *)windowRef; return [[window screen] frame]; } static void setTitle(CFTypeRef windowRef, const char *title) { NSWindow* window = (__bridge NSWindow *)windowRef; window.title = [NSString stringWithUTF8String: title]; } static CFTypeRef layerForView(CFTypeRef viewRef) { NSView *view = (__bridge NSView *)viewRef; return (__bridge CFTypeRef)view.layer; } static void raiseWindow(CFTypeRef windowRef) { NSWindow* window = (__bridge NSWindow *)windowRef; [window makeKeyAndOrderFront:nil]; } */ import "C" func init() { // Darwin requires that UI operations happen on the main thread only. runtime.LockOSThread() } // ViewEvent notified the client of changes to the window AppKit handles. // The handles are retained until another ViewEvent is sent. type ViewEvent 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 window C.CFTypeRef w *callbacks stage system.Stage displayLink *displayLink cursor pointer.CursorName scale float32 config Config } // viewMap is the mapping from Cocoa NSViews to Go windows. var viewMap = make(map[C.CFTypeRef]*window) // 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 // mustView is like lookupView, except that it panics // if the view isn't mapped. func mustView(view C.CFTypeRef) *window { w, ok := lookupView(view) if !ok { panic("no window for view") } return w } func lookupView(view C.CFTypeRef) (*window, bool) { w, exists := viewMap[view] if !exists { return nil, false } return w, true } func deleteView(view C.CFTypeRef) { delete(viewMap, view) } func insertView(view C.CFTypeRef, w *window) { viewMap[view] = w } func (w *window) contextView() C.CFTypeRef { return w.view } func (w *window) ReadClipboard() { content := nsstringToString(C.readClipboard()) w.w.Event(clipboard.Event{Text: content}) } func (w *window) WriteClipboard(s string) { u16 := utf16.Encode([]rune(s)) var chars *C.unichar if len(u16) > 0 { chars = (*C.unichar)(unsafe.Pointer(&u16[0])) } C.writeClipboard(chars, C.NSUInteger(len(u16))) } func (w *window) updateWindowMode() { style := int(C.getWindowStyleMask(w.window)) if style&C.NSWindowStyleMaskFullScreen > 0 { w.config.Mode = Fullscreen } else { w.config.Mode = Windowed } } func (w *window) Configure(options []Option) { screenScale := float32(C.getScreenBackingScale()) cfg := configFor(screenScale) prev := w.config w.updateWindowMode() cnf := w.config cnf.apply(cfg, options) cnf.Size = cnf.Size.Div(int(screenScale)) cnf.MinSize = cnf.MinSize.Div(int(screenScale)) cnf.MaxSize = cnf.MaxSize.Div(int(screenScale)) if cnf.Mode != Fullscreen && prev.Size != cnf.Size { w.config.Size = cnf.Size C.setSize(w.window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y)) } if prev.MinSize != cnf.MinSize { w.config.MinSize = cnf.MinSize C.setMinSize(w.window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y)) } if prev.MaxSize != cnf.MaxSize { w.config.MaxSize = cnf.MaxSize C.setMaxSize(w.window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y)) } if prev.Title != cnf.Title { w.config.Title = cnf.Title title := C.CString(cnf.Title) defer C.free(unsafe.Pointer(title)) C.setTitle(w.window, title) } if prev.Mode != cnf.Mode { switch cnf.Mode { case Windowed, Fullscreen: w.config.Mode = cnf.Mode C.toggleFullScreen(w.window) } } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } } func (w *window) SetCursor(name pointer.CursorName) { w.cursor = windowSetCursor(w.cursor, name) } func (w *window) ShowTextInput(show bool) {} func (w *window) SetInputHint(_ key.InputHint) {} func (w *window) SetAnimating(anim bool) { if anim { w.displayLink.Start() } else { w.displayLink.Stop() } } func (w *window) Raise() { C.raiseWindow(w.window) } 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() } }) } func (w *window) Close() { C.closeWindow(w.window) } // Maximize the window. func (w *window) Maximize() { r := C.getScreenFrame(w.window) // the screen size of the window C.setScreenFrame(w.window, C.CGFloat(0), C.CGFloat(0), r.size.width, r.size.height) } // Center the window. func (w *window) Center() { r := C.getScreenFrame(w.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(w.window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y)) } func (w *window) setStage(stage system.Stage) { if stage == w.stage { return } w.stage = stage w.w.Event(system.StageEvent{Stage: stage}) } //export gio_onKeys func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger, keyDown C.bool) { str := C.GoString(cstr) kmods := convertMods(mods) ks := key.Release if keyDown { ks = key.Press } w := mustView(view) for _, k := range str { if n, ok := convertKey(k); ok { w.w.Event(key.Event{ Name: n, Modifiers: kmods, State: ks, }) } } } //export gio_onText func gio_onText(view C.CFTypeRef, cstr *C.char) { str := C.GoString(cstr) w := mustView(view) w.w.Event(key.EditEvent{Text: str}) } //export gio_onMouse func gio_onMouse(view C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) { var typ pointer.Type switch cdir { case C.MOUSE_MOVE: typ = pointer.Move case C.MOUSE_UP: typ = pointer.Release case C.MOUSE_DOWN: typ = pointer.Press case C.MOUSE_SCROLL: typ = pointer.Scroll default: panic("invalid direction") } var btns pointer.Buttons if cbtns&(1<<0) != 0 { btns |= pointer.ButtonPrimary } if cbtns&(1<<1) != 0 { btns |= pointer.ButtonSecondary } if cbtns&(1<<2) != 0 { btns |= pointer.ButtonTertiary } t := time.Duration(float64(ti)*float64(time.Second) + .5) w := mustView(view) xf, yf := float32(x)*w.scale, float32(y)*w.scale dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale w.w.Event(pointer.Event{ Type: typ, Source: pointer.Mouse, Time: t, Buttons: btns, Position: f32.Point{X: xf, Y: yf}, Scroll: f32.Point{X: dxf, Y: dyf}, Modifiers: convertMods(mods), }) } //export gio_onDraw func gio_onDraw(view C.CFTypeRef) { w := mustView(view) w.draw() } //export gio_onFocus func gio_onFocus(view C.CFTypeRef, focus C.int) { w := mustView(view) w.w.Event(key.FocusEvent{Focus: focus == 1}) w.SetCursor(w.cursor) } //export gio_onChangeScreen func gio_onChangeScreen(view C.CFTypeRef, did uint64) { w := mustView(view) w.displayLink.SetDisplayID(did) } func (w *window) draw() { w.scale = float32(C.getViewBackingScale(w.view)) wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view)) sz := image.Point{ X: int(wf*w.scale + .5), Y: int(hf*w.scale + .5), } if sz != w.config.Size { w.config.Size = sz w.w.Event(ConfigEvent{Config: w.config}) } if sz.X == 0 || sz.Y == 0 { return } cfg := configFor(w.scale) w.setStage(system.StageRunning) w.w.Event(frameEvent{ FrameEvent: system.FrameEvent{ Now: time.Now(), Size: w.config.Size, Metric: cfg, }, Sync: true, }) } func configFor(scale float32) unit.Metric { return unit.Metric{ PxPerDp: scale, PxPerSp: scale, } } //export gio_onClose func gio_onClose(view C.CFTypeRef) { w := mustView(view) w.w.Event(ViewEvent{}) deleteView(view) w.w.Event(system.DestroyEvent{}) w.displayLink.Close() C.CFRelease(w.view) C.CFRelease(w.window) w.view = 0 w.window = 0 w.displayLink = nil } //export gio_onHide func gio_onHide(view C.CFTypeRef) { w := mustView(view) w.setStage(system.StagePaused) } //export gio_onShow func gio_onShow(view C.CFTypeRef) { w := mustView(view) w.setStage(system.StageRunning) } //export gio_onFullscreen func gio_onFullscreen(view C.CFTypeRef) { w := mustView(view) w.config.Mode = Fullscreen w.w.Event(ConfigEvent{Config: w.config}) } //export gio_onWindowed func gio_onWindowed(view C.CFTypeRef) { w := mustView(view) w.config.Mode = Windowed w.w.Event(ConfigEvent{Config: w.config}) } //export gio_onAppHide func gio_onAppHide() { for _, w := range viewMap { w.setStage(system.StagePaused) } } //export gio_onAppShow func gio_onAppShow() { for _, w := range viewMap { w.setStage(system.StageRunning) } } //export gio_onFinishLaunching func gio_onFinishLaunching() { close(launched) } func newWindow(win *callbacks, options []Option) error { <-launched errch := make(chan error) runOnMain(func() { w, err := newOSWindow() if err != nil { errch <- err return } errch <- nil w.w = win w.window = C.gio_createWindow(w.view, nil, 0, 0, 0, 0, 0, 0) win.SetDriver(w) 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(w.window, nextTopLeft) } nextTopLeft = C.cascadeTopLeftFromPoint(w.window, nextTopLeft) C.makeKeyAndOrderFront(w.window) layer := C.layerForView(w.view) w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)}) }) return <-errch } func newOSWindow() (*window, error) { view := C.gio_createView() if view == 0 { return nil, errors.New("CreateWindow: failed to create view") } scale := float32(C.getViewBackingScale(view)) w := &window{ view: view, scale: scale, } dl, err := NewDisplayLink(func() { w.runOnMain(func() { C.setNeedsDisplay(w.view) }) }) w.displayLink = dl if err != nil { C.CFRelease(view) return nil, err } insertView(view, w) return w, nil } func osMain() { C.gio_main() } func convertKey(k rune) (string, bool) { var n string switch k { case 0x1b: 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 0xd: n = key.NameReturn case 0x3: n = key.NameEnter case C.NSHomeFunctionKey: n = key.NameHome case C.NSEndFunctionKey: n = key.NameEnd case 0x7f: n = key.NameDeleteBackward case C.NSDeleteFunctionKey: n = key.NameDeleteForward case C.NSPageUpFunctionKey: n = key.NamePageUp case C.NSPageDownFunctionKey: n = key.NamePageDown case C.NSF1FunctionKey: n = "F1" case C.NSF2FunctionKey: n = "F2" case C.NSF3FunctionKey: n = "F3" case C.NSF4FunctionKey: n = "F4" case C.NSF5FunctionKey: n = "F5" case C.NSF6FunctionKey: n = "F6" case C.NSF7FunctionKey: n = "F7" case C.NSF8FunctionKey: n = "F8" case C.NSF9FunctionKey: n = "F9" case C.NSF10FunctionKey: n = "F10" case C.NSF11FunctionKey: n = "F11" case C.NSF12FunctionKey: n = "F12" case 0x09, 0x19: n = key.NameTab case 0x20: n = key.NameSpace default: k = unicode.ToUpper(k) if !unicode.IsPrint(k) { return "", false } n = string(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 (_ ViewEvent) ImplementsEvent() {}