mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-20 23:59:55 +00:00
170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
//go:build darwin && !ios
|
|
|
|
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/maphash"
|
|
"image"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"github.com/ebitengine/purego"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
darwinCursorOnce sync.Once
|
|
cgsCreateCursor func() uintptr
|
|
darwinCursorErr error
|
|
)
|
|
|
|
// initDarwinCursor binds a private symbol that returns the current
|
|
// system cursor image. The classic CGSCreateCurrentCursorImage moved
|
|
// from CoreGraphics to SkyLight around macOS 13 and is gone entirely
|
|
// in Sequoia; we probe both frameworks for any of the historical
|
|
// names so this keeps working on whichever release the binding still
|
|
// exists. Without a hit the remote-cursor compositing path becomes a
|
|
// no-op and we log the candidates we tried.
|
|
func initDarwinCursor() {
|
|
darwinCursorOnce.Do(func() {
|
|
libs := []string{
|
|
"/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight",
|
|
"/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics",
|
|
}
|
|
names := []string{
|
|
"CGSCreateCurrentCursorImage",
|
|
"CGSCopyCurrentCursorImage",
|
|
"CGSCurrentCursorImage",
|
|
"CGSHardwareCursorActiveImage",
|
|
}
|
|
var tried []string
|
|
for _, path := range libs {
|
|
h, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
|
if err != nil {
|
|
tried = append(tried, fmt.Sprintf("dlopen %s: %v", path, err))
|
|
continue
|
|
}
|
|
for _, name := range names {
|
|
sym, err := purego.Dlsym(h, name)
|
|
if err != nil {
|
|
tried = append(tried, fmt.Sprintf("%s!%s missing", path, name))
|
|
continue
|
|
}
|
|
purego.RegisterFunc(&cgsCreateCursor, sym)
|
|
log.Infof("macOS cursor: bound %s from %s", name, path)
|
|
return
|
|
}
|
|
}
|
|
darwinCursorErr = fmt.Errorf("no cursor image symbol available; tried: %v", tried)
|
|
})
|
|
}
|
|
|
|
// cgCursor holds the cached macOS cursor sprite and bumps a serial when
|
|
// the bytes change. Hotspot is left at (0, 0): the public Cocoa hot-spot
|
|
// query lives on NSCursor which is process-local and not reachable from
|
|
// our purego-based bindings; the visual cost is a small misalignment for
|
|
// non-arrow cursors (I-beam, crosshair, etc.).
|
|
type cgCursor struct {
|
|
mu sync.Mutex
|
|
hashSeed maphash.Seed
|
|
lastSum uint64
|
|
cached *image.RGBA
|
|
serial uint64
|
|
}
|
|
|
|
func newCGCursor() *cgCursor {
|
|
initDarwinCursor()
|
|
return &cgCursor{hashSeed: maphash.MakeSeed()}
|
|
}
|
|
|
|
// Cursor returns the current cursor sprite as RGBA. Errors that come from
|
|
// missing private symbols are sticky; transient empty-image responses are
|
|
// reported as such so the encoder skips this cycle.
|
|
func (c *cgCursor) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if darwinCursorErr != nil {
|
|
return nil, 0, 0, 0, darwinCursorErr
|
|
}
|
|
if cgsCreateCursor == nil {
|
|
return nil, 0, 0, 0, fmt.Errorf("CGSCreateCurrentCursorImage unavailable")
|
|
}
|
|
cgImage := cgsCreateCursor()
|
|
if cgImage == 0 {
|
|
return nil, 0, 0, 0, fmt.Errorf("no cursor image available")
|
|
}
|
|
defer cgImageRelease(cgImage)
|
|
|
|
w := int(cgImageGetWidth(cgImage))
|
|
h := int(cgImageGetHeight(cgImage))
|
|
if w <= 0 || h <= 0 {
|
|
return nil, 0, 0, 0, fmt.Errorf("cursor has zero extent")
|
|
}
|
|
bytesPerRow := int(cgImageGetBytesPerRow(cgImage))
|
|
bpp := int(cgImageGetBitsPerPixel(cgImage))
|
|
if bpp != 32 {
|
|
return nil, 0, 0, 0, fmt.Errorf("unsupported cursor bpp: %d", bpp)
|
|
}
|
|
provider := cgImageGetDataProvider(cgImage)
|
|
if provider == 0 {
|
|
return nil, 0, 0, 0, fmt.Errorf("cursor data provider missing")
|
|
}
|
|
cfData := cgDataProviderCopyData(provider)
|
|
if cfData == 0 {
|
|
return nil, 0, 0, 0, fmt.Errorf("cursor data copy failed")
|
|
}
|
|
defer cfRelease(cfData)
|
|
dataLen := int(cfDataGetLength(cfData))
|
|
dataPtr := cfDataGetBytePtr(cfData)
|
|
if dataPtr == 0 || dataLen == 0 {
|
|
return nil, 0, 0, 0, fmt.Errorf("cursor data empty")
|
|
}
|
|
src := unsafe.Slice((*byte)(unsafe.Pointer(dataPtr)), dataLen)
|
|
|
|
sum := maphash.Bytes(c.hashSeed, src)
|
|
if c.cached != nil && sum == c.lastSum {
|
|
return c.cached, 0, 0, c.serial, nil
|
|
}
|
|
|
|
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
|
for y := 0; y < h; y++ {
|
|
srcOff := y * bytesPerRow
|
|
dstOff := y * w * 4
|
|
for x := 0; x < w; x++ {
|
|
si := srcOff + x*4
|
|
di := dstOff + x*4
|
|
img.Pix[di+0] = src[si+2]
|
|
img.Pix[di+1] = src[si+1]
|
|
img.Pix[di+2] = src[si+0]
|
|
img.Pix[di+3] = src[si+3]
|
|
}
|
|
}
|
|
|
|
c.lastSum = sum
|
|
c.cached = img
|
|
c.serial++
|
|
return img, 0, 0, c.serial, nil
|
|
}
|
|
|
|
// Cursor on CGCapturer satisfies cursorSource. The cgCursor wrapper is
|
|
// allocated lazily so a build that never asks for the cursor pays no cost.
|
|
func (c *CGCapturer) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|
c.cursorOnce.Do(func() {
|
|
c.cursor = newCGCursor()
|
|
})
|
|
return c.cursor.Cursor()
|
|
}
|
|
|
|
// Cursor on MacPoller forwards to the lazy CGCapturer. ensureCapturerLocked
|
|
// returns an error when Screen Recording permission has not been granted;
|
|
// in that case there is no usable cursor source either.
|
|
func (p *MacPoller) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if err := p.ensureCapturerLocked(); err != nil {
|
|
return nil, 0, 0, 0, err
|
|
}
|
|
return p.capturer.Cursor()
|
|
}
|