Files
gio/app/os_macos.go
T
Pierre Curto 5ce1e98282 app: use material.Decorations on undecorated platforms
This patch implements a mechanism for customizing window
decorations.
If a window is configured with app.Decorated(true), then
the widget/material.Decorations are applied. On Wayland,
the option is automatically set when the server does not
provide window decorations.

Server side decorations are no longer requested.
The Decorated flag is set according to the
server's requests.

Wayland is now the default driver for UNIX platforms.

References: https://todo.sr.ht/~eliasnaur/gio/318
Signed-off-by: Pierre Curto <pierre.curto@gmail.com>
2022-01-27 15:32:42 +01:00

713 lines
16 KiB
Go

// 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 <AppKit/AppKit.h>
#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 void hideWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window miniaturize:window];
}
static void unhideWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window deminiaturize:window];
}
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))
// Decorations are never disabled.
cnf.Decorated = true
switch cnf.Mode {
case Fullscreen:
switch prev.Mode {
case Fullscreen:
case Minimized:
C.unhideWindow(w.window)
fallthrough
default:
w.config.Mode = Fullscreen
C.toggleFullScreen(w.window)
}
case Minimized:
switch prev.Mode {
case Minimized, Fullscreen:
default:
w.config.Mode = Minimized
C.hideWindow(w.window)
}
case Maximized:
switch prev.Mode {
case Fullscreen:
case Minimized:
C.unhideWindow(w.window)
fallthrough
default:
w.config.Mode = Maximized
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)
w.config.Size = image.Pt(int(r.size.width), int(r.size.height))
w.setTitle(prev, cnf)
}
case Windowed:
switch prev.Mode {
case Fullscreen:
w.config.Mode = Windowed
C.toggleFullScreen(w.window)
case Minimized:
w.config.Mode = Windowed
C.unhideWindow(w.window)
case Maximized:
w.config.Mode = Windowed
}
w.setTitle(prev, cnf)
if 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 cnf.center {
r := C.getScreenFrame(w.window) // the screen size of the window
sz := w.config.Size
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))
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
if w.config != prev {
w.w.Event(ConfigEvent{Config: w.config})
}
}
func (w *window) setTitle(prev, cnf Config) {
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)
}
}
func (w *window) Perform(system.Action) {}
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)
}
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,
}
done := make(chan struct{})
setNeedsDisplay := func() {
C.setNeedsDisplay(w.view)
done <- struct{}{}
}
dl, err := NewDisplayLink(func() {
w.runOnMain(setNeedsDisplay)
<-done
})
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 = 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 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() {}