| /* |
| * Copyright 2019 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include <Carbon/Carbon.h> |
| |
| #include "src/core/SkUtils.h" |
| #include "tools/sk_app/mac/WindowContextFactory_mac.h" |
| #include "tools/sk_app/mac/Window_mac.h" |
| #include "tools/skui/ModifierKey.h" |
| |
| @interface WindowDelegate : NSObject<NSWindowDelegate> |
| |
| - (WindowDelegate*)initWithWindow:(sk_app::Window_mac*)initWindow; |
| |
| @end |
| |
| @interface MainView : NSView |
| |
| - (MainView*)initWithWindow:(sk_app::Window_mac*)initWindow; |
| |
| @end |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| using sk_app::Window; |
| |
| namespace sk_app { |
| |
| SkTDynamicHash<Window_mac, NSInteger> Window_mac::gWindowMap; |
| |
| Window* Window::CreateNativeWindow(void*) { |
| Window_mac* window = new Window_mac(); |
| if (!window->initWindow()) { |
| delete window; |
| return nullptr; |
| } |
| |
| return window; |
| } |
| |
| bool Window_mac::initWindow() { |
| // we already have a window |
| if (fWindow) { |
| return true; |
| } |
| |
| // Create a delegate to track certain events |
| WindowDelegate* delegate = [[WindowDelegate alloc] initWithWindow:this]; |
| if (nil == delegate) { |
| return false; |
| } |
| |
| // Create Cocoa window |
| constexpr int initialWidth = 1280; |
| constexpr int initialHeight = 960; |
| NSRect windowRect = NSMakeRect(100, 100, initialWidth, initialHeight); |
| |
| NSUInteger windowStyle = (NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | |
| NSMiniaturizableWindowMask); |
| |
| fWindow = [[NSWindow alloc] initWithContentRect:windowRect styleMask:windowStyle |
| backing:NSBackingStoreBuffered defer:NO]; |
| if (nil == fWindow) { |
| [delegate release]; |
| return false; |
| } |
| |
| // create view |
| MainView* view = [[MainView alloc] initWithWindow:this]; |
| if (nil == view) { |
| [fWindow release]; |
| [delegate release]; |
| return false; |
| } |
| |
| [fWindow setContentView:view]; |
| [fWindow makeFirstResponder:view]; |
| [fWindow setDelegate:delegate]; |
| [fWindow setAcceptsMouseMovedEvents:YES]; |
| [fWindow setRestorable:NO]; |
| |
| // Should be retained by window now |
| [view release]; |
| |
| fWindowNumber = fWindow.windowNumber; |
| gWindowMap.add(this); |
| |
| return true; |
| } |
| |
| void Window_mac::closeWindow() { |
| if (nil != fWindow) { |
| gWindowMap.remove(fWindowNumber); |
| if (sk_app::Window_mac::gWindowMap.count() < 1) { |
| [NSApp terminate:fWindow]; |
| } |
| [fWindow close]; |
| fWindow = nil; |
| } |
| } |
| |
| void Window_mac::setTitle(const char* title) { |
| if (NSString* titleStr = [NSString stringWithUTF8String:title]) { |
| [fWindow setTitle:titleStr]; |
| } |
| } |
| |
| void Window_mac::show() { |
| [fWindow orderFront:nil]; |
| |
| [NSApp activateIgnoringOtherApps:YES]; |
| [fWindow makeKeyAndOrderFront:NSApp]; |
| } |
| |
| bool Window_mac::attach(BackendType attachType) { |
| this->initWindow(); |
| |
| window_context_factory::MacWindowInfo info; |
| info.fMainView = [fWindow contentView]; |
| switch (attachType) { |
| #ifdef SK_DAWN |
| case kDawn_BackendType: |
| fWindowContext = MakeDawnMTLForMac(info, fRequestedDisplayParams); |
| break; |
| #endif |
| #ifdef SK_VULKAN |
| case kVulkan_BackendType: |
| fWindowContext = MakeVulkanForMac(info, fRequestedDisplayParams); |
| break; |
| #endif |
| #ifdef SK_METAL |
| case kMetal_BackendType: |
| fWindowContext = MakeMetalForMac(info, fRequestedDisplayParams); |
| break; |
| #endif |
| #ifdef SK_GL |
| case kNativeGL_BackendType: |
| default: |
| fWindowContext = MakeGLForMac(info, fRequestedDisplayParams); |
| break; |
| #else |
| default: |
| #endif |
| case kRaster_BackendType: |
| fWindowContext = MakeRasterForMac(info, fRequestedDisplayParams); |
| break; |
| } |
| this->onBackendCreated(); |
| |
| return SkToBool(fWindowContext); |
| } |
| |
| float Window_mac::scaleFactor() const { |
| return sk_app::GetBackingScaleFactor(fWindow.contentView); |
| } |
| |
| void Window_mac::PaintWindows() { |
| gWindowMap.foreach([&](Window_mac* window) { |
| if (window->fIsContentInvalidated) { |
| window->onPaint(); |
| } |
| }); |
| } |
| |
| } // namespace sk_app |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| @implementation WindowDelegate { |
| sk_app::Window_mac* fWindow; |
| } |
| |
| - (WindowDelegate*)initWithWindow:(sk_app::Window_mac *)initWindow { |
| fWindow = initWindow; |
| |
| return self; |
| } |
| |
| - (void)windowDidResize:(NSNotification *)notification { |
| NSView* view = fWindow->window().contentView; |
| CGFloat scale = sk_app::GetBackingScaleFactor(view); |
| fWindow->onResize(view.bounds.size.width * scale, view.bounds.size.height * scale); |
| fWindow->inval(); |
| } |
| |
| - (BOOL)windowShouldClose:(NSWindow*)sender { |
| fWindow->closeWindow(); |
| |
| return FALSE; |
| } |
| |
| @end |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| static skui::Key get_key(unsigned short vk) { |
| // This will work with an ANSI QWERTY keyboard. |
| // Something more robust would be needed to support alternate keyboards. |
| static const struct { |
| unsigned short fVK; |
| skui::Key fKey; |
| } gPair[] = { |
| { kVK_Delete, skui::Key::kBack }, |
| { kVK_Return, skui::Key::kOK }, |
| { kVK_UpArrow, skui::Key::kUp }, |
| { kVK_DownArrow, skui::Key::kDown }, |
| { kVK_LeftArrow, skui::Key::kLeft }, |
| { kVK_RightArrow, skui::Key::kRight }, |
| { kVK_Tab, skui::Key::kTab }, |
| { kVK_PageUp, skui::Key::kPageUp }, |
| { kVK_PageDown, skui::Key::kPageDown }, |
| { kVK_Home, skui::Key::kHome }, |
| { kVK_End, skui::Key::kEnd }, |
| { kVK_ForwardDelete, skui::Key::kDelete }, |
| { kVK_Escape, skui::Key::kEscape }, |
| { kVK_Shift, skui::Key::kShift }, |
| { kVK_RightShift, skui::Key::kShift }, |
| { kVK_Control, skui::Key::kCtrl }, |
| { kVK_RightControl, skui::Key::kCtrl }, |
| { kVK_Option, skui::Key::kOption }, |
| { kVK_RightOption, skui::Key::kOption }, |
| { kVK_Command, skui::Key::kSuper }, |
| { kVK_RightCommand, skui::Key::kSuper }, |
| { kVK_ANSI_A, skui::Key::kA }, |
| { kVK_ANSI_C, skui::Key::kC }, |
| { kVK_ANSI_V, skui::Key::kV }, |
| { kVK_ANSI_X, skui::Key::kX }, |
| { kVK_ANSI_Y, skui::Key::kY }, |
| { kVK_ANSI_Z, skui::Key::kZ }, |
| }; |
| |
| for (size_t i = 0; i < SK_ARRAY_COUNT(gPair); i++) { |
| if (gPair[i].fVK == vk) { |
| return gPair[i].fKey; |
| } |
| } |
| |
| return skui::Key::kNONE; |
| } |
| |
| static skui::ModifierKey get_modifiers(const NSEvent* event) { |
| NSUInteger modifierFlags = [event modifierFlags]; |
| skui::ModifierKey modifiers = skui::ModifierKey::kNone; |
| |
| if (modifierFlags & NSEventModifierFlagCommand) { |
| modifiers |= skui::ModifierKey::kCommand; |
| } |
| if (modifierFlags & NSEventModifierFlagShift) { |
| modifiers |= skui::ModifierKey::kShift; |
| } |
| if (modifierFlags & NSEventModifierFlagControl) { |
| modifiers |= skui::ModifierKey::kControl; |
| } |
| if (modifierFlags & NSEventModifierFlagOption) { |
| modifiers |= skui::ModifierKey::kOption; |
| } |
| |
| if ((NSKeyDown == [event type] || NSKeyUp == [event type]) && ![event isARepeat]) { |
| modifiers |= skui::ModifierKey::kFirstPress; |
| } |
| |
| return modifiers; |
| } |
| |
| @implementation MainView { |
| sk_app::Window_mac* fWindow; |
| // A TrackingArea prevents us from capturing events outside the view |
| NSTrackingArea* fTrackingArea; |
| // We keep track of the state of the modifier keys on each event in order to synthesize |
| // key-up/down events for each modifier. |
| skui::ModifierKey fLastModifiers; |
| } |
| |
| - (MainView*)initWithWindow:(sk_app::Window_mac *)initWindow { |
| self = [super init]; |
| |
| fWindow = initWindow; |
| fTrackingArea = nil; |
| |
| [self updateTrackingAreas]; |
| |
| return self; |
| } |
| |
| - (void)dealloc |
| { |
| [fTrackingArea release]; |
| [super dealloc]; |
| } |
| |
| - (BOOL)isOpaque { |
| return YES; |
| } |
| |
| - (BOOL)canBecomeKeyView { |
| return YES; |
| } |
| |
| - (BOOL)acceptsFirstResponder { |
| return YES; |
| } |
| |
| - (void)updateTrackingAreas { |
| if (fTrackingArea != nil) { |
| [self removeTrackingArea:fTrackingArea]; |
| [fTrackingArea release]; |
| } |
| |
| const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | |
| NSTrackingActiveInKeyWindow | |
| NSTrackingEnabledDuringMouseDrag | |
| NSTrackingCursorUpdate | |
| NSTrackingInVisibleRect | |
| NSTrackingAssumeInside; |
| |
| fTrackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] |
| options:options |
| owner:self |
| userInfo:nil]; |
| |
| [self addTrackingArea:fTrackingArea]; |
| [super updateTrackingAreas]; |
| } |
| |
| - (skui::ModifierKey) updateModifierKeys:(NSEvent*) event { |
| using sknonstd::Any; |
| |
| skui::ModifierKey modifiers = get_modifiers(event); |
| skui::ModifierKey changed = modifiers ^ fLastModifiers; |
| fLastModifiers = modifiers; |
| |
| struct ModMap { |
| skui::ModifierKey modifier; |
| skui::Key key; |
| }; |
| |
| // Map each modifier bit to the equivalent skui Key and send key-up/down events. |
| for (const ModMap& cur : {ModMap{skui::ModifierKey::kCommand, skui::Key::kSuper}, |
| ModMap{skui::ModifierKey::kShift, skui::Key::kShift}, |
| ModMap{skui::ModifierKey::kControl, skui::Key::kCtrl}, |
| ModMap{skui::ModifierKey::kOption, skui::Key::kOption}}) { |
| if (Any(changed & cur.modifier)) { |
| const skui::InputState state = Any(modifiers & cur.modifier) ? skui::InputState::kDown |
| : skui::InputState::kUp; |
| (void) fWindow->onKey(cur.key, state, modifiers); |
| } |
| } |
| |
| return modifiers; |
| } |
| |
| - (BOOL)performKeyEquivalent:(NSEvent *)event { |
| [self updateModifierKeys:event]; |
| |
| // By default, unhandled key equivalents send -keyDown events; unfortunately, they do not send |
| // a matching -keyUp. In other words, we can claim that we didn't handle the event and OS X will |
| // turn this event into a -keyDown automatically, but we need to synthesize a matching -keyUp on |
| // a later frame. Since we only read the modifiers and key code from the event, we can reuse |
| // this "key-equivalent" event as a "key up". |
| [self performSelector:@selector(keyUp:) withObject:event afterDelay:0.1]; |
| return NO; |
| } |
| |
| - (void)keyDown:(NSEvent *)event { |
| skui::ModifierKey modifiers = [self updateModifierKeys:event]; |
| |
| skui::Key key = get_key([event keyCode]); |
| if (key != skui::Key::kNONE) { |
| if (!fWindow->onKey(key, skui::InputState::kDown, modifiers)) { |
| if (skui::Key::kEscape == key) { |
| [NSApp terminate:fWindow->window()]; |
| } |
| } |
| } |
| |
| NSString* characters = [event charactersIgnoringModifiers]; |
| NSUInteger len = [characters length]; |
| if (len > 0) { |
| unichar* charBuffer = new unichar[len+1]; |
| [characters getCharacters:charBuffer range:NSMakeRange(0, len)]; |
| for (NSUInteger i = 0; i < len; ++i) { |
| (void) fWindow->onChar((SkUnichar) charBuffer[i], modifiers); |
| } |
| delete [] charBuffer; |
| } |
| } |
| |
| - (void)keyUp:(NSEvent *)event { |
| skui::ModifierKey modifiers = [self updateModifierKeys:event]; |
| |
| skui::Key key = get_key([event keyCode]); |
| if (key != skui::Key::kNONE) { |
| (void) fWindow->onKey(key, skui::InputState::kUp, modifiers); |
| } |
| } |
| |
| -(void)flagsChanged:(NSEvent *)event { |
| [self updateModifierKeys:event]; |
| } |
| |
| - (void)mouseDown:(NSEvent *)event { |
| NSView* view = fWindow->window().contentView; |
| CGFloat backingScaleFactor = sk_app::GetBackingScaleFactor(view); |
| |
| skui::ModifierKey modifiers = [self updateModifierKeys:event]; |
| |
| const NSPoint pos = [event locationInWindow]; |
| const NSRect rect = [view frame]; |
| fWindow->onMouse(pos.x * backingScaleFactor, (rect.size.height - pos.y) * backingScaleFactor, |
| skui::InputState::kDown, modifiers); |
| } |
| |
| - (void)mouseUp:(NSEvent *)event { |
| NSView* view = fWindow->window().contentView; |
| CGFloat backingScaleFactor = sk_app::GetBackingScaleFactor(view); |
| |
| skui::ModifierKey modifiers = [self updateModifierKeys:event]; |
| |
| const NSPoint pos = [event locationInWindow]; |
| const NSRect rect = [view frame]; |
| fWindow->onMouse(pos.x * backingScaleFactor, (rect.size.height - pos.y) * backingScaleFactor, |
| skui::InputState::kUp, modifiers); |
| } |
| |
| - (void)mouseDragged:(NSEvent *)event { |
| [self updateModifierKeys:event]; |
| [self mouseMoved:event]; |
| } |
| |
| - (void)mouseMoved:(NSEvent *)event { |
| NSView* view = fWindow->window().contentView; |
| CGFloat backingScaleFactor = sk_app::GetBackingScaleFactor(view); |
| |
| skui::ModifierKey modifiers = [self updateModifierKeys:event]; |
| |
| const NSPoint pos = [event locationInWindow]; |
| const NSRect rect = [view frame]; |
| fWindow->onMouse(pos.x * backingScaleFactor, (rect.size.height - pos.y) * backingScaleFactor, |
| skui::InputState::kMove, modifiers); |
| } |
| |
| - (void)scrollWheel:(NSEvent *)event { |
| skui::ModifierKey modifiers = [self updateModifierKeys:event]; |
| |
| // TODO: support hasPreciseScrollingDeltas? |
| fWindow->onMouseWheel([event scrollingDeltaY], modifiers); |
| } |
| |
| - (void)drawRect:(NSRect)rect { |
| fWindow->onPaint(); |
| } |
| |
| @end |