// 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 #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(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 client = (__bridge id)clientRef; NSTextInputContext *ctx = [[NSTextInputContext alloc] initWithClient:client]; return CFBridgingRetain(ctx); } } static void discardMarkedText(CFTypeRef viewRef) { @autoreleasepool { id view = (__bridge id)viewRef; NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; if (view == [ctx client]) { [ctx discardMarkedText]; } } } static void invalidateCharacterCoordinates(CFTypeRef viewRef) { @autoreleasepool { id view = (__bridge id)viewRef; NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; if (view == [ctx client]) { [ctx invalidateCharacterCoordinates]; } } } static void interpretKeyEvents(CFTypeRef viewRef, CFTypeRef eventRef) { @autoreleasepool { NSView *view = (__bridge NSView *)viewRef; NSEvent *event = (__bridge NSEvent *)eventRef; [view interpretKeyEvents:[NSArray arrayWithObject:event]]; } } static int isMiniaturized(CFTypeRef windowRef) { @autoreleasepool { NSWindow *window = (__bridge NSWindow *)windowRef; return window.miniaturized ? 1 : 0; } } */ 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 keysDown map[key.Name]struct{} // cmdKeys is for storing the current key event while // waiting for a doCommandBySelector. cmdKeys cmdKeys } type cmdKeys struct { eventStr string eventMods key.Modifiers } // 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 && C.isMiniaturized(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, event C.CFTypeRef, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) { w := windowFor(h) if w.keysDown == nil { w.keysDown = make(map[key.Name]struct{}) } str := nsstringToString(cstr) kmods := convertMods(mods) ks := key.Release if keyDown { ks = key.Press w.cmdKeys.eventStr = str w.cmdKeys.eventMods = kmods C.interpretKeyEvents(w.view, event) } for _, k := range str { if n, ok := convertKey(k); ok { ke := key.Event{ Name: n, Modifiers: kmods, State: ks, } if keyDown { w.keysDown[ke.Name] = struct{}{} if _, isCmd := convertCommandKey(k); isCmd || kmods.Contain(key.ModCommand) { // doCommandBySelector already processed the event. return } } else { if _, pressed := w.keysDown[n]; !pressed { continue } delete(w.keysDown, n) } w.ProcessEvent(ke) } } } //export gio_onCommandBySelector func gio_onCommandBySelector(h C.uintptr_t) C.bool { w := windowFor(h) ev := w.cmdKeys w.cmdKeys = cmdKeys{} handled := false for _, k := range ev.eventStr { n, ok := convertCommandKey(k) if !ok && ev.eventMods.Contain(key.ModCommand) { n, ok = convertKey(k) } if !ok { continue } ke := key.Event{ Name: n, Modifiers: ev.eventMods, State: key.Press, } handled = w.processEvent(ke) || handled } return C.bool(handled) } //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) { w := windowFor(h) str := nsstringToString(cstr) // macOS IME in some cases calls insertText for command keys such as backspace // instead of doCommandBySelector. for _, r := range str { if _, ok := convertCommandKey(r); ok { 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.processEvent(e) } func (w *window) processEvent(e event.Event) bool { handled := w.w.ProcessEvent(e) w.loop.FlushEvents() return handled } 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 '\x1b': // ASCII 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 '\r': n = key.NameReturn case '\x03': n = key.NameEnter case C.NSHomeFunctionKey: n = key.NameHome case C.NSEndFunctionKey: n = key.NameEnd case '\x7f', '\b': n = key.NameDeleteBackward case C.NSDeleteFunctionKey: n = key.NameDeleteForward case '\t', 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) { if n, ok := convertCommandKey(k); ok { return n, true } 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{}) }