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
30namespace {
31using taichi::mac::call;
32using taichi::mac::cast_call;
33using taichi::mac::clscall;
34
35std::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
42std::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
61std::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
80std::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
119constexpr int NSApplicationActivationPolicyRegular = 0;
120constexpr int NSEventTypeKeyDown = 10;
121constexpr int NSEventTypeKeyUp = 11;
122constexpr int NSEventTypeFlagsChanged = 12;
123constexpr int NSEventTypeScrollWheel = 22;
124
125struct 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.
164constexpr char kTaichiViewClassName[] = "TaichiGuiView";
165
166} // namespace
167
168extern id NSApp;
169extern id const NSDefaultRunLoopMode;
170
171typedef struct AppDel {
172 Class isa;
173 id window;
174} AppDelegate;
175
176class 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
185std::map<id, taichi::GUI *, IdComparator> gui_from_id;
186
187enum {
188 NSBorderlessWindowMask = 0,
189 NSTitledWindowMask = 1 << 0,
190 NSClosableWindowMask = 1 << 1,
191 NSMiniaturizableWindowMask = 1 << 2,
192 NSResizableWindowMask = 1 << 3,
193};
194
195void 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
238BOOL 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
244Class ViewClass;
245Class 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
274namespace taichi {
275
276void 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
324void 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
416void 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
422void GUI::redraw() {
423 call(view, "setNeedsDisplay:", YES);
424}
425
426GUI::~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