1/*
2* Copyright 2019 Google Inc.
3*
4* Use of this source code is governed by a BSD-style license that can be
5* found in the LICENSE file.
6*/
7
8#include <Carbon/Carbon.h>
9
10#include "src/core/SkUtils.h"
11#include "tools/sk_app/mac/WindowContextFactory_mac.h"
12#include "tools/sk_app/mac/Window_mac.h"
13#include "tools/skui/ModifierKey.h"
14
15@interface WindowDelegate : NSObject<NSWindowDelegate>
16
17- (WindowDelegate*)initWithWindow:(sk_app::Window_mac*)initWindow;
18
19@end
20
21@interface MainView : NSView
22
23- (MainView*)initWithWindow:(sk_app::Window_mac*)initWindow;
24
25@end
26
27///////////////////////////////////////////////////////////////////////////////
28
29using sk_app::Window;
30
31namespace sk_app {
32
33SkTDynamicHash<Window_mac, NSInteger> Window_mac::gWindowMap;
34
35Window* Window::CreateNativeWindow(void*) {
36    Window_mac* window = new Window_mac();
37    if (!window->initWindow()) {
38        delete window;
39        return nullptr;
40    }
41
42    return window;
43}
44
45bool Window_mac::initWindow() {
46    // we already have a window
47    if (fWindow) {
48        return true;
49    }
50
51    // Create a delegate to track certain events
52    WindowDelegate* delegate = [[WindowDelegate alloc] initWithWindow:this];
53    if (nil == delegate) {
54        return false;
55    }
56
57    // Create Cocoa window
58    constexpr int initialWidth = 1280;
59    constexpr int initialHeight = 960;
60    NSRect windowRect = NSMakeRect(100, 100, initialWidth, initialHeight);
61
62    NSUInteger windowStyle = (NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask |
63                              NSMiniaturizableWindowMask);
64
65    fWindow = [[NSWindow alloc] initWithContentRect:windowRect styleMask:windowStyle
66                                backing:NSBackingStoreBuffered defer:NO];
67    if (nil == fWindow) {
68        [delegate release];
69        return false;
70    }
71
72    // create view
73    MainView* view = [[MainView alloc] initWithWindow:this];
74    if (nil == view) {
75        [fWindow release];
76        [delegate release];
77        return false;
78    }
79
80    [fWindow setContentView:view];
81    [fWindow makeFirstResponder:view];
82    [fWindow setDelegate:delegate];
83    [fWindow setAcceptsMouseMovedEvents:YES];
84    [fWindow setRestorable:NO];
85
86    // Should be retained by window now
87    [view release];
88
89    fWindowNumber = fWindow.windowNumber;
90    gWindowMap.add(this);
91
92    return true;
93}
94
95void Window_mac::closeWindow() {
96    if (nil != fWindow) {
97        gWindowMap.remove(fWindowNumber);
98        if (sk_app::Window_mac::gWindowMap.count() < 1) {
99            [NSApp terminate:fWindow];
100        }
101        [fWindow close];
102        fWindow = nil;
103    }
104}
105
106void Window_mac::setTitle(const char* title) {
107    if (NSString* titleStr = [NSString stringWithUTF8String:title]) {
108        [fWindow setTitle:titleStr];
109    }
110}
111
112void Window_mac::show() {
113    [fWindow orderFront:nil];
114
115    [NSApp activateIgnoringOtherApps:YES];
116    [fWindow makeKeyAndOrderFront:NSApp];
117}
118
119bool Window_mac::attach(BackendType attachType) {
120    this->initWindow();
121
122    window_context_factory::MacWindowInfo info;
123    info.fMainView = [fWindow contentView];
124    switch (attachType) {
125#ifdef SK_DAWN
126        case kDawn_BackendType:
127            fWindowContext = MakeDawnMTLForMac(info, fRequestedDisplayParams);
128            break;
129#endif
130#ifdef SK_VULKAN
131        case kVulkan_BackendType:
132            fWindowContext = MakeVulkanForMac(info, fRequestedDisplayParams);
133            break;
134#endif
135#ifdef SK_METAL
136        case kMetal_BackendType:
137            fWindowContext = MakeMetalForMac(info, fRequestedDisplayParams);
138            break;
139#endif
140#ifdef SK_GL
141        case kNativeGL_BackendType:
142        default:
143            fWindowContext = MakeGLForMac(info, fRequestedDisplayParams);
144            break;
145#else
146        default:
147#endif
148        case kRaster_BackendType:
149            fWindowContext = MakeRasterForMac(info, fRequestedDisplayParams);
150            break;
151    }
152    this->onBackendCreated();
153
154    return SkToBool(fWindowContext);
155}
156
157void Window_mac::PaintWindows() {
158    gWindowMap.foreach([&](Window_mac* window) {
159        if (window->fIsContentInvalidated) {
160            window->onPaint();
161        }
162    });
163}
164
165}   // namespace sk_app
166
167///////////////////////////////////////////////////////////////////////////////
168
169@implementation WindowDelegate {
170    sk_app::Window_mac* fWindow;
171}
172
173- (WindowDelegate*)initWithWindow:(sk_app::Window_mac *)initWindow {
174    fWindow = initWindow;
175
176    return self;
177}
178
179- (void)windowDidResize:(NSNotification *)notification {
180    const NSRect mainRect = [fWindow->window().contentView bounds];
181
182    fWindow->onResize(mainRect.size.width, mainRect.size.height);
183    fWindow->inval();
184}
185
186- (BOOL)windowShouldClose:(NSWindow*)sender {
187    fWindow->closeWindow();
188
189    return FALSE;
190}
191
192@end
193
194///////////////////////////////////////////////////////////////////////////////
195
196static skui::Key get_key(unsigned short vk) {
197    // This will work with an ANSI QWERTY keyboard.
198    // Something more robust would be needed to support alternate keyboards.
199    static const struct {
200        unsigned short fVK;
201        skui::Key      fKey;
202    } gPair[] = {
203        { kVK_Delete,        skui::Key::kBack },
204        { kVK_Return,        skui::Key::kOK },
205        { kVK_UpArrow,       skui::Key::kUp },
206        { kVK_DownArrow,     skui::Key::kDown },
207        { kVK_LeftArrow,     skui::Key::kLeft },
208        { kVK_RightArrow,    skui::Key::kRight },
209        { kVK_Tab,           skui::Key::kTab },
210        { kVK_PageUp,        skui::Key::kPageUp },
211        { kVK_PageDown,      skui::Key::kPageDown },
212        { kVK_Home,          skui::Key::kHome },
213        { kVK_End,           skui::Key::kEnd },
214        { kVK_ForwardDelete, skui::Key::kDelete },
215        { kVK_Escape,        skui::Key::kEscape },
216        { kVK_Shift,         skui::Key::kShift },
217        { kVK_RightShift,    skui::Key::kShift },
218        { kVK_Control,       skui::Key::kCtrl },
219        { kVK_RightControl,  skui::Key::kCtrl },
220        { kVK_Option,        skui::Key::kOption },
221        { kVK_RightOption,   skui::Key::kOption },
222        { kVK_Command,       skui::Key::kSuper },
223        { kVK_RightCommand,  skui::Key::kSuper },
224        { kVK_ANSI_A,        skui::Key::kA },
225        { kVK_ANSI_C,        skui::Key::kC },
226        { kVK_ANSI_V,        skui::Key::kV },
227        { kVK_ANSI_X,        skui::Key::kX },
228        { kVK_ANSI_Y,        skui::Key::kY },
229        { kVK_ANSI_Z,        skui::Key::kZ },
230    };
231
232    for (size_t i = 0; i < SK_ARRAY_COUNT(gPair); i++) {
233        if (gPair[i].fVK == vk) {
234            return gPair[i].fKey;
235        }
236    }
237
238    return skui::Key::kNONE;
239}
240
241static skui::ModifierKey get_modifiers(const NSEvent* event) {
242    NSUInteger modifierFlags = [event modifierFlags];
243    skui::ModifierKey modifiers = skui::ModifierKey::kNone;
244
245    if (modifierFlags & NSEventModifierFlagCommand) {
246        modifiers |= skui::ModifierKey::kCommand;
247    }
248    if (modifierFlags & NSEventModifierFlagShift) {
249        modifiers |= skui::ModifierKey::kShift;
250    }
251    if (modifierFlags & NSEventModifierFlagControl) {
252        modifiers |= skui::ModifierKey::kControl;
253    }
254    if (modifierFlags & NSEventModifierFlagOption) {
255        modifiers |= skui::ModifierKey::kOption;
256    }
257
258    if ((NSKeyDown == [event type] || NSKeyUp == [event type]) && ![event isARepeat]) {
259        modifiers |= skui::ModifierKey::kFirstPress;
260    }
261
262    return modifiers;
263}
264
265@implementation MainView {
266    sk_app::Window_mac* fWindow;
267    // A TrackingArea prevents us from capturing events outside the view
268    NSTrackingArea* fTrackingArea;
269    // We keep track of the state of the modifier keys on each event in order to synthesize
270    // key-up/down events for each modifier.
271    skui::ModifierKey fLastModifiers;
272}
273
274- (MainView*)initWithWindow:(sk_app::Window_mac *)initWindow {
275    self = [super init];
276
277    fWindow = initWindow;
278    fTrackingArea = nil;
279
280    [self updateTrackingAreas];
281
282    return self;
283}
284
285- (void)dealloc
286{
287    [fTrackingArea release];
288    [super dealloc];
289}
290
291- (BOOL)isOpaque {
292    return YES;
293}
294
295- (BOOL)canBecomeKeyView {
296    return YES;
297}
298
299- (BOOL)acceptsFirstResponder {
300    return YES;
301}
302
303- (void)updateTrackingAreas {
304    if (fTrackingArea != nil) {
305        [self removeTrackingArea:fTrackingArea];
306        [fTrackingArea release];
307    }
308
309    const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
310                                          NSTrackingActiveInKeyWindow |
311                                          NSTrackingEnabledDuringMouseDrag |
312                                          NSTrackingCursorUpdate |
313                                          NSTrackingInVisibleRect |
314                                          NSTrackingAssumeInside;
315
316    fTrackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
317                                                 options:options
318                                                   owner:self
319                                                userInfo:nil];
320
321    [self addTrackingArea:fTrackingArea];
322    [super updateTrackingAreas];
323}
324
325- (skui::ModifierKey) updateModifierKeys:(NSEvent*) event {
326    using sknonstd::Any;
327
328    skui::ModifierKey modifiers = get_modifiers(event);
329    skui::ModifierKey changed = modifiers ^ fLastModifiers;
330    fLastModifiers = modifiers;
331
332    struct ModMap {
333        skui::ModifierKey modifier;
334        skui::Key         key;
335    };
336
337    // Map each modifier bit to the equivalent skui Key and send key-up/down events.
338    for (const ModMap& cur : {ModMap{skui::ModifierKey::kCommand, skui::Key::kSuper},
339                              ModMap{skui::ModifierKey::kShift,   skui::Key::kShift},
340                              ModMap{skui::ModifierKey::kControl, skui::Key::kCtrl},
341                              ModMap{skui::ModifierKey::kOption,  skui::Key::kOption}}) {
342        if (Any(changed & cur.modifier)) {
343            const skui::InputState state = Any(modifiers & cur.modifier) ? skui::InputState::kDown
344                                                                         : skui::InputState::kUp;
345            (void) fWindow->onKey(cur.key, state, modifiers);
346        }
347    }
348
349    return modifiers;
350}
351
352- (BOOL)performKeyEquivalent:(NSEvent *)event {
353    [self updateModifierKeys:event];
354
355    // By default, unhandled key equivalents send -keyDown events; unfortunately, they do not send
356    // a matching -keyUp. In other words, we can claim that we didn't handle the event and OS X will
357    // turn this event into a -keyDown automatically, but we need to synthesize a matching -keyUp on
358    // a later frame. Since we only read the modifiers and key code from the event, we can reuse
359    // this "key-equivalent" event as a "key up".
360    [self performSelector:@selector(keyUp:) withObject:event afterDelay:0.1];
361    return NO;
362}
363
364- (void)keyDown:(NSEvent *)event {
365    skui::ModifierKey modifiers = [self updateModifierKeys:event];
366
367    skui::Key key = get_key([event keyCode]);
368    if (key != skui::Key::kNONE) {
369        if (!fWindow->onKey(key, skui::InputState::kDown, modifiers)) {
370            if (skui::Key::kEscape == key) {
371                [NSApp terminate:fWindow->window()];
372            }
373        }
374    }
375
376    NSString* characters = [event charactersIgnoringModifiers];
377    NSUInteger len = [characters length];
378    if (len > 0) {
379        unichar* charBuffer = new unichar[len+1];
380        [characters getCharacters:charBuffer range:NSMakeRange(0, len)];
381        for (NSUInteger i = 0; i < len; ++i) {
382            (void) fWindow->onChar((SkUnichar) charBuffer[i], modifiers);
383        }
384        delete [] charBuffer;
385    }
386}
387
388- (void)keyUp:(NSEvent *)event {
389    skui::ModifierKey modifiers = [self updateModifierKeys:event];
390
391    skui::Key key = get_key([event keyCode]);
392    if (key != skui::Key::kNONE) {
393        (void) fWindow->onKey(key, skui::InputState::kUp, modifiers);
394    }
395}
396
397-(void)flagsChanged:(NSEvent *)event {
398    [self updateModifierKeys:event];
399}
400
401- (void)mouseDown:(NSEvent *)event {
402    skui::ModifierKey modifiers = [self updateModifierKeys:event];
403
404    const NSPoint pos = [event locationInWindow];
405    const NSRect rect = [fWindow->window().contentView frame];
406    fWindow->onMouse(pos.x, rect.size.height - pos.y, skui::InputState::kDown, modifiers);
407}
408
409- (void)mouseUp:(NSEvent *)event {
410    skui::ModifierKey modifiers = [self updateModifierKeys:event];
411
412    const NSPoint pos = [event locationInWindow];
413    const NSRect rect = [fWindow->window().contentView frame];
414    fWindow->onMouse(pos.x, rect.size.height - pos.y, skui::InputState::kUp, modifiers);
415}
416
417- (void)mouseDragged:(NSEvent *)event {
418    [self updateModifierKeys:event];
419    [self mouseMoved:event];
420}
421
422- (void)mouseMoved:(NSEvent *)event {
423    skui::ModifierKey modifiers = [self updateModifierKeys:event];
424
425    const NSPoint pos = [event locationInWindow];
426    const NSRect rect = [fWindow->window().contentView frame];
427    fWindow->onMouse(pos.x, rect.size.height - pos.y, skui::InputState::kMove, modifiers);
428}
429
430- (void)scrollWheel:(NSEvent *)event {
431    skui::ModifierKey modifiers = [self updateModifierKeys:event];
432
433    // TODO: support hasPreciseScrollingDeltas?
434    fWindow->onMouseWheel([event scrollingDeltaY], modifiers);
435}
436
437- (void)drawRect:(NSRect)rect {
438    fWindow->onPaint();
439}
440
441@end
442