os_darwin.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package wm
4
5 /*
6 #include <Foundation/Foundation.h>
7
8 __attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
9 __attribute__ ((visibility ("hidden"))) NSUInteger gio_nsstringLength(CFTypeRef str);
10 __attribute__ ((visibility ("hidden"))) void gio_nsstringGetCharacters(CFTypeRef str, unichar *chars, NSUInteger loc, NSUInteger length);
11 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void);
12 __attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
13 __attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
14 __attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
15 __attribute__ ((visibility ("hidden"))) void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did);
16 __attribute__ ((visibility ("hidden"))) void gio_hideCursor();
17 __attribute__ ((visibility ("hidden"))) void gio_showCursor();
18 __attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
19 __attribute__ ((visibility ("hidden"))) bool gio_isMainThread();
20 */
21 import "C"
22 import (
23 "errors"
24 "sync"
25 "sync/atomic"
26 "time"
27 "unicode/utf16"
28 "unsafe"
29
30 "github.com/p9c/p9/pkg/gel/gio/io/pointer"
31 )
32
33 // displayLink is the state for a display link (CVDisplayLinkRef on macOS,
34 // CADisplayLink on iOS). It runs a state-machine goroutine that keeps the
35 // display link running for a while after being stopped to avoid the thread
36 // start/stop overhead and because the CVDisplayLink sometimes fails to
37 // start, stop and start again within a short duration.
38 type displayLink struct {
39 callback func()
40 // states is for starting or stopping the display link.
41 states chan bool
42 // done is closed when the display link is destroyed.
43 done chan struct{}
44 // dids receives the display id when the callback owner is moved
45 // to a different screen.
46 dids chan uint64
47 // running tracks the desired state of the link. running is accessed
48 // with atomic.
49 running uint32
50 }
51
52 // displayLinks maps CFTypeRefs to *displayLinks.
53 var displayLinks sync.Map
54
55 var mainFuncs = make(chan func(), 1)
56
57 // runOnMain runs the function on the main thread.
58 func runOnMain(f func()) {
59 if C.gio_isMainThread() {
60 f()
61 return
62 }
63 go func() {
64 mainFuncs <- f
65 C.gio_wakeupMainThread()
66 }()
67 }
68
69 //export gio_dispatchMainFuncs
70 func gio_dispatchMainFuncs() {
71 for {
72 select {
73 case f := <-mainFuncs:
74 f()
75 default:
76 return
77 }
78 }
79 }
80
81 // nsstringToString converts a NSString to a Go string, and
82 // releases the original string.
83 func nsstringToString(str C.CFTypeRef) string {
84 if str == 0 {
85 return ""
86 }
87 defer C.CFRelease(str)
88 n := C.gio_nsstringLength(str)
89 if n == 0 {
90 return ""
91 }
92 chars := make([]uint16, n)
93 C.gio_nsstringGetCharacters(str, (*C.unichar)(unsafe.Pointer(&chars[0])), 0, n)
94 utf8 := utf16.Decode(chars)
95 return string(utf8)
96 }
97
98 func NewDisplayLink(callback func()) (*displayLink, error) {
99 d := &displayLink{
100 callback: callback,
101 done: make(chan struct{}),
102 states: make(chan bool),
103 dids: make(chan uint64),
104 }
105 dl := C.gio_createDisplayLink()
106 if dl == 0 {
107 return nil, errors.New("app: failed to create display link")
108 }
109 go d.run(dl)
110 return d, nil
111 }
112
113 func (d *displayLink) run(dl C.CFTypeRef) {
114 defer C.gio_releaseDisplayLink(dl)
115 displayLinks.Store(dl, d)
116 defer displayLinks.Delete(dl)
117 var stopTimer *time.Timer
118 var tchan <-chan time.Time
119 started := false
120 for {
121 select {
122 case <-tchan:
123 tchan = nil
124 started = false
125 C.gio_stopDisplayLink(dl)
126 case start := <-d.states:
127 switch {
128 case !start && tchan == nil:
129 // stopTimeout is the delay before stopping the display link to
130 // avoid the overhead of frequently starting and stopping the
131 // link thread.
132 const stopTimeout = 500 * time.Millisecond
133 if stopTimer == nil {
134 stopTimer = time.NewTimer(stopTimeout)
135 } else {
136 // stopTimer is always drained when tchan == nil.
137 stopTimer.Reset(stopTimeout)
138 }
139 tchan = stopTimer.C
140 atomic.StoreUint32(&d.running, 0)
141 case start:
142 if tchan != nil && !stopTimer.Stop() {
143 <-tchan
144 }
145 tchan = nil
146 atomic.StoreUint32(&d.running, 1)
147 if !started {
148 started = true
149 C.gio_startDisplayLink(dl)
150 }
151 }
152 case did := <-d.dids:
153 C.gio_setDisplayLinkDisplay(dl, C.uint64_t(did))
154 case <-d.done:
155 return
156 }
157 }
158 }
159
160 func (d *displayLink) Start() {
161 d.states <- true
162 }
163
164 func (d *displayLink) Stop() {
165 d.states <- false
166 }
167
168 func (d *displayLink) Close() {
169 close(d.done)
170 }
171
172 func (d *displayLink) SetDisplayID(did uint64) {
173 d.dids <- did
174 }
175
176 //export gio_onFrameCallback
177 func gio_onFrameCallback(dl C.CFTypeRef) {
178 if d, exists := displayLinks.Load(dl); exists {
179 d := d.(*displayLink)
180 if atomic.LoadUint32(&d.running) != 0 {
181 d.callback()
182 }
183 }
184 }
185
186 // windowSetCursor updates the cursor from the current one to a new one
187 // and returns the new one.
188 func windowSetCursor(from, to pointer.CursorName) pointer.CursorName {
189 if from == to {
190 return to
191 }
192 var curID int
193 switch to {
194 default:
195 to = pointer.CursorDefault
196 fallthrough
197 case pointer.CursorDefault:
198 curID = 1
199 case pointer.CursorText:
200 curID = 2
201 case pointer.CursorPointer:
202 curID = 3
203 case pointer.CursorCrossHair:
204 curID = 4
205 case pointer.CursorColResize:
206 curID = 5
207 case pointer.CursorRowResize:
208 curID = 6
209 case pointer.CursorGrab:
210 curID = 7
211 case pointer.CursorNone:
212 runOnMain(func() {
213 C.gio_hideCursor()
214 })
215 return to
216 }
217 runOnMain(func() {
218 if from == pointer.CursorNone {
219 C.gio_showCursor()
220 }
221 C.gio_setCursor(C.NSUInteger(curID))
222 })
223 return to
224 }
225