1 | #include "taichi/ui/gui/gui.h" |
2 | |
3 | #include "taichi/common/task.h" |
4 | #include "taichi/util/bit.h" |
5 | |
6 | #if defined(TI_GUI_COCOA) |
7 | |
8 | #include <algorithm> |
9 | #include <optional> |
10 | #include <string> |
11 | #include <thread> |
12 | #include <unordered_map> |
13 | |
14 | #include "taichi/platform/mac/objc_api.h" |
15 | |
16 | // https://stackoverflow.com/questions/4356441/mac-os-cocoa-draw-a-simple-pixel-on-a-canvas |
17 | // http://cocoadevcentral.com/d/intro_to_quartz/ |
18 | // Modified based on |
19 | // https://github.com/CodaFi/C-Macs |
20 | |
21 | // Obj-c runtime doc: |
22 | // https://developer.apple.com/documentation/objectivec/objective-c_runtime?language=objc |
23 | |
24 | #include <ApplicationServices/ApplicationServices.h> |
25 | #include <Carbon/Carbon.h> |
26 | #include <CoreGraphics/CGBase.h> |
27 | #include <CoreGraphics/CGGeometry.h> |
28 | #include <objc/NSObjCRuntime.h> |
29 | |
30 | namespace { |
31 | using taichi::mac::call; |
32 | using taichi::mac::cast_call; |
33 | using taichi::mac::clscall; |
34 | |
35 | std::string str_tolower(std::string s) { |
36 | // https://en.cppreference.com/w/cpp/string/byte/tolower |
37 | std::transform(s.begin(), s.end(), s.begin(), |
38 | [](unsigned char c) { return std::tolower(c); }); |
39 | return s; |
40 | } |
41 | |
42 | std::optional<std::string> try_get_alnum(ushort keycode) { |
43 | // Can someone tell me the reason why Apple didn't make these consecutive... |
44 | #define CASE(i) \ |
45 | { kVK_ANSI_##i, str_tolower(#i) } |
46 | static const std::unordered_map<ushort, std::string> key2str = { |
47 | CASE(0), CASE(1), CASE(2), CASE(3), CASE(4), CASE(5), CASE(6), CASE(7), |
48 | CASE(8), CASE(9), CASE(A), CASE(B), CASE(C), CASE(D), CASE(E), CASE(F), |
49 | CASE(G), CASE(H), CASE(I), CASE(J), CASE(K), CASE(L), CASE(M), CASE(N), |
50 | CASE(O), CASE(P), CASE(Q), CASE(R), CASE(S), CASE(T), CASE(U), CASE(V), |
51 | CASE(W), CASE(X), CASE(Y), CASE(Z), |
52 | }; |
53 | #undef CASE |
54 | const auto iter = key2str.find(keycode); |
55 | if (iter == key2str.end()) { |
56 | return std::nullopt; |
57 | } |
58 | return iter->second; |
59 | } |
60 | |
61 | std::optional<std::string> try_get_fnkey(ushort keycode) { |
62 | // Or these... |
63 | #define STRINGIFY(x) #x |
64 | #define CASE(i) \ |
65 | { kVK_F##i, STRINGIFY(F##i) } |
66 | static const std::unordered_map<ushort, std::string> key2str = { |
67 | CASE(1), CASE(2), CASE(3), CASE(4), CASE(5), CASE(6), |
68 | CASE(7), CASE(8), CASE(9), CASE(10), CASE(11), CASE(12), |
69 | CASE(13), CASE(14), CASE(15), CASE(16), |
70 | }; |
71 | #undef CASE |
72 | #undef STRINGIFY |
73 | const auto iter = key2str.find(keycode); |
74 | if (iter == key2str.end()) { |
75 | return std::nullopt; |
76 | } |
77 | return iter->second; |
78 | } |
79 | |
80 | std::string lookup_keysym(ushort keycode) { |
81 | // Full enum definition: |
82 | // https://github.com/phracker/MacOSX-SDKs/blob/ef9fe35d5691b6dd383c8c46d867a499817a01b6/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L198-L315 |
83 | switch (keycode) { |
84 | case kVK_LeftArrow: |
85 | return "Left" ; |
86 | case kVK_RightArrow: |
87 | return "Right" ; |
88 | case kVK_UpArrow: |
89 | return "Up" ; |
90 | case kVK_DownArrow: |
91 | return "Down" ; |
92 | case kVK_Tab: |
93 | return "Tab" ; |
94 | case kVK_Return: |
95 | return "Return" ; |
96 | // Mac Delete = Backspace on other platforms |
97 | // Mac ForwardDelete (Fn + Delete) = Delete on other platforms |
98 | case kVK_Delete: |
99 | return "BackSpace" ; |
100 | case kVK_Escape: |
101 | return "Escape" ; |
102 | case kVK_Space: |
103 | return " " ; |
104 | default: |
105 | break; |
106 | } |
107 | auto val_opt = try_get_alnum(keycode); |
108 | if (val_opt.has_value()) { |
109 | return *val_opt; |
110 | } |
111 | val_opt = try_get_fnkey(keycode); |
112 | if (val_opt.has_value()) { |
113 | return *val_opt; |
114 | } |
115 | return "Vk" + std::to_string((int)keycode); |
116 | } |
117 | |
118 | // TODO(k-ye): Define all the magic numbers for Obj-C enums here |
119 | constexpr int NSApplicationActivationPolicyRegular = 0; |
120 | constexpr int NSEventTypeKeyDown = 10; |
121 | constexpr int NSEventTypeKeyUp = 11; |
122 | constexpr int NSEventTypeFlagsChanged = 12; |
123 | constexpr int NSEventTypeScrollWheel = 22; |
124 | |
125 | struct ModifierFlagsHandler { |
126 | struct Result { |
127 | std::vector<std::string> released; |
128 | }; |
129 | |
130 | static Result handle(unsigned int flag, |
131 | std::unordered_map<std::string, bool> *active_flags) { |
132 | constexpr int NSEventModifierFlagCapsLock = 1 << 16; |
133 | constexpr int NSEventModifierFlagShift = 1 << 17; |
134 | constexpr int NSEventModifierFlagControl = 1 << 18; |
135 | constexpr int NSEventModifierFlagOption = 1 << 19; |
136 | constexpr int NSEventModifierFlagCommand = 1 << 20; |
137 | const static std::unordered_map<int, std::string> flag_mask_to_name = { |
138 | {NSEventModifierFlagCapsLock, "Caps_Lock" }, |
139 | {NSEventModifierFlagShift, "Shift" }, |
140 | {NSEventModifierFlagControl, "Control" }, |
141 | // Mac Option = Alt on other platforms |
142 | {NSEventModifierFlagOption, "Alt" }, |
143 | {NSEventModifierFlagCommand, "Command" }, |
144 | }; |
145 | Result result; |
146 | for (const auto &kv : flag_mask_to_name) { |
147 | bool &cur = (*active_flags)[kv.second]; |
148 | if (flag & kv.first) { |
149 | cur = true; |
150 | } else { |
151 | if (cur) { |
152 | // If previously pressed, trigger a release event |
153 | result.released.push_back(kv.second); |
154 | } |
155 | cur = false; |
156 | } |
157 | } |
158 | return result; |
159 | } |
160 | }; |
161 | |
162 | // We need to give the View class a somewhat unique name, so that it won't |
163 | // conflict with other modules (e.g. matplotlib). See issue#998. |
164 | constexpr char kTaichiViewClassName[] = "TaichiGuiView" ; |
165 | |
166 | } // namespace |
167 | |
168 | extern id NSApp; |
169 | extern id const NSDefaultRunLoopMode; |
170 | |
171 | typedef struct AppDel { |
172 | Class isa; |
173 | id window; |
174 | } AppDelegate; |
175 | |
176 | class IdComparator { |
177 | public: |
178 | bool operator()(id a, id b) const { |
179 | TI_STATIC_ASSERT(sizeof(a) == sizeof(taichi::int64)); |
180 | return taichi::bit::reinterpret_bits<taichi::int64>(a) < |
181 | taichi::bit::reinterpret_bits<taichi::int64>(b); |
182 | } |
183 | }; |
184 | |
185 | std::map<id, taichi::GUI *, IdComparator> gui_from_id; |
186 | |
187 | enum { |
188 | NSBorderlessWindowMask = 0, |
189 | NSTitledWindowMask = 1 << 0, |
190 | NSClosableWindowMask = 1 << 1, |
191 | NSMiniaturizableWindowMask = 1 << 2, |
192 | NSResizableWindowMask = 1 << 3, |
193 | }; |
194 | |
195 | void updateLayer(id self, SEL _) { |
196 | using namespace taichi; |
197 | auto *gui = gui_from_id[self]; |
198 | auto width = gui->width, height = gui->height; |
199 | uint8_t *data_ptr = nullptr; |
200 | if (gui->fast_gui) { |
201 | data_ptr = reinterpret_cast<uint8_t *>(gui->fast_buf); |
202 | } else { |
203 | auto &img = gui->canvas->img; |
204 | auto &data = gui->img_data; |
205 | data_ptr = data.data(); |
206 | for (int j = 0; j < height; j++) { |
207 | for (int i = 0; i < width; i++) { |
208 | int index = 4 * (i + j * width); |
209 | auto pixel = img[i][height - j - 1]; |
210 | data[index++] = uint8(clamp(int(pixel[0] * 255.0_f), 0, 255)); |
211 | data[index++] = uint8(clamp(int(pixel[1] * 255.0_f), 0, 255)); |
212 | data[index++] = uint8(clamp(int(pixel[2] * 255.0_f), 0, 255)); |
213 | data[index++] = 255; // alpha |
214 | } |
215 | } |
216 | } |
217 | |
218 | CGDataProviderRef provider = CGDataProviderCreateWithData( |
219 | nullptr, data_ptr, gui->img_data_length, nullptr); |
220 | CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); |
221 | CGImageRef image = |
222 | CGImageCreate(width, height, 8, 32, width * 4, colorspace, |
223 | kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast, |
224 | provider, nullptr, true, kCGRenderingIntentDefault); |
225 | // Profiling showed that CGContextDrawImage can be rather slow (~50ms per |
226 | // frame!), so we instead set the image as the content of the view's layer. |
227 | // See also: |
228 | // * slow CGContextDrawImage: https://stackoverflow.com/a/7599794/12003165 |
229 | // * CALayer + CGImage: https://stackoverflow.com/a/48310419/12003165 |
230 | // * profiling: |
231 | // https://github.com/taichi-dev/taichi/issues/489#issuecomment-589955458 |
232 | call(call(gui->view, "layer" ), "setContents:" , image); |
233 | CGImageRelease(image); |
234 | CGDataProviderRelease(provider); |
235 | CGColorSpaceRelease(colorspace); |
236 | } |
237 | |
238 | BOOL windowShouldClose(id self, SEL _, id sender) { |
239 | auto *gui = gui_from_id[sender]; |
240 | gui->window_received_close.store(true); |
241 | return true; |
242 | } |
243 | |
244 | Class ViewClass; |
245 | Class AppDelClass; |
246 | |
247 | __attribute__((constructor)) static void initView() { |
248 | ViewClass = objc_allocateClassPair((Class)objc_getClass("NSView" ), |
249 | kTaichiViewClassName, 0); |
250 | // There are two ways to update NSView's content, either via "drawRect:" or |
251 | // "updateLayer". Updating via layer can be a lot faster, so we use this |
252 | // method. See also: |
253 | // https://developer.apple.com/documentation/appkit/nsview/1483461-wantsupdatelayer?language=objc |
254 | // https://stackoverflow.com/a/51899686/12003165 |
255 | // |
256 | // Also, it should be noted that if NSView's layer is enabled (via |
257 | // [view setWantsLyaer:YES]), but "drawRect:" is used, then the content is |
258 | // drawn and cleared rapidly, causing a flickering screen. It seems that the |
259 | // view itself and the underlying layer were overwriting each other's content. |
260 | // https://stackoverflow.com/a/11321521/12003165 |
261 | class_addMethod(ViewClass, sel_getUid("updateLayer" /* no colon */), |
262 | (IMP)updateLayer, "v@:" ); |
263 | objc_registerClassPair(ViewClass); |
264 | |
265 | AppDelClass = objc_allocateClassPair((Class)objc_getClass("NSObject" ), |
266 | "AppDelegate" , 0); |
267 | Protocol *WinDelProtocol = objc_getProtocol("NSWindowDelegate" ); |
268 | class_addMethod(AppDelClass, sel_getUid("windowShouldClose:" ), |
269 | (IMP)windowShouldClose, "c@:@" ); |
270 | class_addProtocol(AppDelClass, WinDelProtocol); |
271 | objc_registerClassPair(AppDelClass); |
272 | } |
273 | |
274 | namespace taichi { |
275 | |
276 | void GUI::create_window() { |
277 | clscall("NSApplication" , "sharedApplication" ); |
278 | if (NSApp == nullptr) { |
279 | fprintf(stderr, "Failed to initialized NSApplication.\nterminating.\n" ); |
280 | return; |
281 | } |
282 | // I finally found how to bring the NSWindow to the front and to handle |
283 | // keyboard events in these posts: |
284 | // https://stackoverflow.com/a/11010614/12003165 |
285 | // http://www.cocoawithlove.com/2010/09/minimalist-cocoa-programming.html |
286 | // |
287 | // The problem was that, a Cocoa app without NIB files (app bundle, |
288 | // info.plist, whatever the meta files are) by default has a policy of |
289 | // NSApplicationActivationPolicyProhibited. |
290 | // (https://developer.apple.com/documentation/appkit/nsapplicationactivationpolicy/nsapplicationactivationpolicyprohibited?language=objc) |
291 | call(NSApp, "setActivationPolicy:" , NSApplicationActivationPolicyRegular); |
292 | // This doesn't seem necessary, but in case there's some weird bug causing the |
293 | // Window not to be brought to the front, try enable this. |
294 | // https://stackoverflow.com/a/7460187/12003165 |
295 | // call(NSApp, "activateIgnoringOtherApps:", YES); |
296 | img_data_length = width * height * 4; |
297 | img_data.resize(img_data_length); |
298 | auto appDelObj = clscall("AppDelegate" , "alloc" ); |
299 | appDelObj = call(appDelObj, "init" ); |
300 | call(NSApp, "setDelegate:" , appDelObj); |
301 | window = clscall("NSWindow" , "alloc" ); |
302 | auto rect = (CGRect){{0, 0}, {CGFloat(width), CGFloat(height)}}; |
303 | call(window, "initWithContentRect:styleMask:backing:defer:" , rect, |
304 | (NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | |
305 | NSMiniaturizableWindowMask), |
306 | 0, false); |
307 | view = call(clscall(kTaichiViewClassName, "alloc" ), "initWithFrame:" , rect); |
308 | gui_from_id[view] = this; |
309 | // Needed by NSWindowDelegate |
310 | gui_from_id[window] = this; |
311 | // Use layer to speed up the draw |
312 | // https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc |
313 | call(view, "setWantsLayer:" , YES); |
314 | call(window, "setDelegate:" , appDelObj); |
315 | call(window, "setContentView:" , view); |
316 | call(window, "becomeFirstResponder" ); |
317 | call(window, "setAcceptsMouseMovedEvents:" , YES); |
318 | call(window, "makeKeyAndOrderFront:" , window); |
319 | if (fullscreen) { |
320 | call(window, "toggleFullScreen:" ); |
321 | } |
322 | } |
323 | |
324 | void GUI::process_event() { |
325 | call(clscall("NSRunLoop" , "currentRunLoop" ), |
326 | "runMode:beforeDate:" , NSDefaultRunLoopMode, |
327 | clscall("NSDate" , "distantPast" )); |
328 | while (1) { |
329 | auto event = call( |
330 | NSApp, "nextEventMatchingMask:untilDate:inMode:dequeue:" , NSUIntegerMax, |
331 | clscall("NSDate" , "distantPast" ), NSDefaultRunLoopMode, YES); |
332 | if (event != nullptr) { |
333 | auto event_type = cast_call<NSInteger>(event, "type" ); |
334 | call(NSApp, "sendEvent:" , event); |
335 | call(NSApp, "updateWindows" ); |
336 | auto p = cast_call<CGPoint>(event, "locationInWindow" ); |
337 | ushort keycode = 0; |
338 | std::string keysym; |
339 | switch (event_type) { |
340 | case 1: // NSLeftMouseDown |
341 | set_mouse_pos(p.x, p.y); |
342 | mouse_event(MouseEvent{MouseEvent::Type::press, cursor_pos}); |
343 | key_events.push_back( |
344 | GUI::KeyEvent{GUI::KeyEvent::Type::press, "LMB" , cursor_pos}); |
345 | break; |
346 | case 2: // NSLeftMouseUp |
347 | set_mouse_pos(p.x, p.y); |
348 | mouse_event(MouseEvent{MouseEvent::Type::release, cursor_pos}); |
349 | key_events.push_back( |
350 | GUI::KeyEvent{GUI::KeyEvent::Type::release, "LMB" , cursor_pos}); |
351 | break; |
352 | case 3: // NSEventTypeRightMouseDown |
353 | key_events.push_back( |
354 | GUI::KeyEvent{GUI::KeyEvent::Type::press, "RMB" , cursor_pos}); |
355 | break; |
356 | case 4: // NSEventTypeRightMouseUp |
357 | key_events.push_back( |
358 | GUI::KeyEvent{GUI::KeyEvent::Type::release, "RMB" , cursor_pos}); |
359 | break; |
360 | case 5: // NSMouseMoved |
361 | case 6: // NSLeftMouseDragged |
362 | case 7: // NSRightMouseDragged |
363 | case 27: // NSNSOtherMouseDragged |
364 | set_mouse_pos(p.x, p.y); |
365 | key_events.push_back( |
366 | GUI::KeyEvent{GUI::KeyEvent::Type::move, "Motion" , cursor_pos}); |
367 | mouse_event(MouseEvent{MouseEvent::Type::move, Vector2i(p.x, p.y)}); |
368 | break; |
369 | case NSEventTypeKeyDown: |
370 | case NSEventTypeKeyUp: { |
371 | keycode = cast_call<ushort>(event, "keyCode" ); |
372 | keysym = lookup_keysym(keycode); |
373 | auto kev_type = (event_type == NSEventTypeKeyDown) |
374 | ? KeyEvent::Type::press |
375 | : KeyEvent::Type::release; |
376 | key_events.push_back(KeyEvent{kev_type, keysym, cursor_pos}); |
377 | break; |
378 | } |
379 | case NSEventTypeFlagsChanged: { |
380 | const auto modflag = cast_call<unsigned long>(event, "modifierFlags" ); |
381 | const auto r = |
382 | ModifierFlagsHandler::handle(modflag, &active_modifier_flags); |
383 | for (const auto &key : r.released) { |
384 | key_events.push_back( |
385 | KeyEvent{KeyEvent::Type::release, key, cursor_pos}); |
386 | } |
387 | break; |
388 | } |
389 | case NSEventTypeScrollWheel: { |
390 | set_mouse_pos(p.x, p.y); |
391 | const auto dx = (int)cast_call<CGFloat>(event, "scrollingDeltaX" ); |
392 | // Mac trackpad's vertical scroll is reversed. |
393 | const auto dy = -(int)cast_call<CGFloat>(event, "scrollingDeltaY" ); |
394 | key_events.push_back(KeyEvent{KeyEvent::Type::move, "Wheel" , |
395 | cursor_pos, Vector2i{dx, dy}}); |
396 | break; |
397 | } |
398 | } |
399 | } else { |
400 | break; |
401 | } |
402 | } |
403 | |
404 | for (const auto &kv : active_modifier_flags) { |
405 | if (kv.second) { |
406 | key_events.push_back( |
407 | KeyEvent{KeyEvent::Type::press, kv.first, cursor_pos}); |
408 | } |
409 | } |
410 | if (window_received_close.load()) { |
411 | send_window_close_message(); |
412 | window_received_close.store(false); |
413 | } |
414 | } |
415 | |
416 | void GUI::set_title(std::string title) { |
417 | auto str = clscall("NSString" , "stringWithUTF8String:" , title.c_str()); |
418 | call(window, "setTitle:" , str); |
419 | call(str, "release" ); |
420 | } |
421 | |
422 | void GUI::redraw() { |
423 | call(view, "setNeedsDisplay:" , YES); |
424 | } |
425 | |
426 | GUI::~GUI() { |
427 | if (show_gui) { |
428 | call(window, "close" ); |
429 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
430 | process_event(); |
431 | } |
432 | } |
433 | |
434 | } // namespace taichi |
435 | |
436 | #endif |
437 | |