aboutsummaryrefslogtreecommitdiff
path: root/src/platform/gui/osx
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2022-05-15 14:08:06 +0800
committercrupest <crupest@outlook.com>2022-05-15 14:08:06 +0800
commit8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6 (patch)
tree77e41cc14264060517c0f7ed95837012afb8342e /src/platform/gui/osx
parent9e0c9d3499bc50c3534b4dc500d8b5d0b5f22752 (diff)
downloadcru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.tar.gz
cru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.tar.bz2
cru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.zip
...
Diffstat (limited to 'src/platform/gui/osx')
-rw-r--r--src/platform/gui/osx/CMakeLists.txt15
-rw-r--r--src/platform/gui/osx/Clipboard.mm46
-rw-r--r--src/platform/gui/osx/ClipboardPrivate.h27
-rw-r--r--src/platform/gui/osx/Cursor.mm93
-rw-r--r--src/platform/gui/osx/CursorPrivate.h29
-rw-r--r--src/platform/gui/osx/InputMethod.mm84
-rw-r--r--src/platform/gui/osx/InputMethodPrivate.h64
-rw-r--r--src/platform/gui/osx/Keyboard.mm283
-rw-r--r--src/platform/gui/osx/KeyboardPrivate.h9
-rw-r--r--src/platform/gui/osx/Menu.mm180
-rw-r--r--src/platform/gui/osx/MenuPrivate.h65
-rw-r--r--src/platform/gui/osx/Resource.cpp6
-rw-r--r--src/platform/gui/osx/UiApplication.mm260
-rw-r--r--src/platform/gui/osx/Window.mm800
-rw-r--r--src/platform/gui/osx/WindowPrivate.h118
15 files changed, 2079 insertions, 0 deletions
diff --git a/src/platform/gui/osx/CMakeLists.txt b/src/platform/gui/osx/CMakeLists.txt
new file mode 100644
index 00000000..5442ad15
--- /dev/null
+++ b/src/platform/gui/osx/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_library(CruPlatformGuiOsx SHARED
+ Clipboard.mm
+ Cursor.mm
+ InputMethod.mm
+ Keyboard.mm
+ Menu.mm
+ Resource.cpp
+ UiApplication.mm
+ Window.mm
+)
+
+find_library(APPKIT AppKit REQUIRED)
+find_library(UNIFORMTYPEIDENTIFIERS UniformTypeIdentifiers REQUIRED)
+
+target_link_libraries(CruPlatformGuiOsx PUBLIC CruPlatformGui CruPlatformGraphicsQuartz ${APPKIT} ${UNIFORMTYPEIDENTIFIERS})
diff --git a/src/platform/gui/osx/Clipboard.mm b/src/platform/gui/osx/Clipboard.mm
new file mode 100644
index 00000000..068771c8
--- /dev/null
+++ b/src/platform/gui/osx/Clipboard.mm
@@ -0,0 +1,46 @@
+#include "cru/platform/gui/osx/Clipboard.h"
+#include "ClipboardPrivate.h"
+
+#include "cru/common/log/Logger.h"
+#include "cru/platform/osx/Convert.h"
+
+#include <memory>
+
+namespace cru::platform::gui::osx {
+using cru::platform::osx::Convert;
+
+OsxClipboard::OsxClipboard(cru::platform::gui::IUiApplication* ui_application,
+ std::unique_ptr<details::OsxClipboardPrivate> p)
+ : OsxGuiResource(ui_application), p_(std::move(p)) {}
+
+OsxClipboard::~OsxClipboard() {}
+
+String OsxClipboard::GetText() { return p_->GetText(); }
+
+void OsxClipboard::SetText(String text) { p_->SetText(text); }
+
+namespace details {
+OsxClipboardPrivate::OsxClipboardPrivate(NSPasteboard* pasteboard) : pasteboard_(pasteboard) {}
+
+OsxClipboardPrivate::~OsxClipboardPrivate() {}
+
+String OsxClipboardPrivate::GetText() {
+ auto result = [pasteboard_ readObjectsForClasses:@[ NSString.class ] options:nil];
+ if (result == nil) {
+ CRU_LOG_WARN(u"Failed to get text from clipboard");
+ return u"";
+ } else {
+ if (result.count == 0) {
+ return u"";
+ } else {
+ return Convert((CFStringRef)result[0]);
+ }
+ }
+}
+
+void OsxClipboardPrivate::SetText(String text) {
+ [pasteboard_ clearContents];
+ [pasteboard_ writeObjects:@[ (NSString*)Convert(text) ]];
+}
+}
+} // namespace cru::platform::gui::osx
diff --git a/src/platform/gui/osx/ClipboardPrivate.h b/src/platform/gui/osx/ClipboardPrivate.h
new file mode 100644
index 00000000..e00c59dc
--- /dev/null
+++ b/src/platform/gui/osx/ClipboardPrivate.h
@@ -0,0 +1,27 @@
+#pragma once
+#include "cru/common/Base.h"
+#include "cru/platform/gui/osx/Clipboard.h"
+
+#include <AppKit/AppKit.h>
+
+namespace cru::platform::gui::osx {
+namespace details {
+class OsxClipboardPrivate : public Object {
+ CRU_DEFINE_CLASS_LOG_TAG(u"OsxClipboardPrivate")
+ public:
+ explicit OsxClipboardPrivate(NSPasteboard* pasteboard);
+
+ CRU_DELETE_COPY(OsxClipboardPrivate)
+ CRU_DELETE_MOVE(OsxClipboardPrivate)
+
+ ~OsxClipboardPrivate();
+
+ public:
+ String GetText();
+ void SetText(String text);
+
+ private:
+ NSPasteboard* pasteboard_;
+};
+} // namespace details
+} // namespace cru::platform::gui::osx
diff --git a/src/platform/gui/osx/Cursor.mm b/src/platform/gui/osx/Cursor.mm
new file mode 100644
index 00000000..fae1514c
--- /dev/null
+++ b/src/platform/gui/osx/Cursor.mm
@@ -0,0 +1,93 @@
+#include "cru/platform/gui/osx/Cursor.h"
+#include "CursorPrivate.h"
+
+#include "cru/platform/osx/Exception.h"
+#include "cru/platform/gui/osx/Resource.h"
+#include "cru/platform/gui/Cursor.h"
+#include "cru/platform/gui/UiApplication.h"
+
+#include <memory>
+
+namespace cru::platform::gui::osx {
+namespace details {
+OsxCursorPrivate::OsxCursorPrivate(OsxCursor* cursor, SystemCursorType cursor_type) {
+ cursor_ = cursor;
+
+ switch (cursor_type) {
+ case SystemCursorType::Arrow:
+ ns_cursor_ = [NSCursor arrowCursor];
+ break;
+ case SystemCursorType::Hand:
+ ns_cursor_ = [NSCursor pointingHandCursor];
+ break;
+ case SystemCursorType::IBeam:
+ ns_cursor_ = [NSCursor IBeamCursor];
+ break;
+ default:
+ throw Exception(u"Unknown system cursor type.");
+ }
+}
+
+OsxCursorPrivate::~OsxCursorPrivate() {}
+}
+
+OsxCursor::OsxCursor(IUiApplication* ui_application, SystemCursorType cursor_type)
+ : OsxGuiResource(ui_application) {
+ p_ = std::make_unique<details::OsxCursorPrivate>(this, cursor_type);
+}
+
+OsxCursor::~OsxCursor() {}
+
+namespace details {
+class OsxCursorManagerPrivate {
+ friend OsxCursorManager;
+
+ public:
+ explicit OsxCursorManagerPrivate(OsxCursorManager* cursor_manager);
+
+ CRU_DELETE_COPY(OsxCursorManagerPrivate)
+ CRU_DELETE_MOVE(OsxCursorManagerPrivate)
+
+ ~OsxCursorManagerPrivate();
+
+ private:
+ OsxCursorManager* cursor_manager_;
+
+ std::shared_ptr<OsxCursor> arrow_cursor_;
+ std::shared_ptr<OsxCursor> hand_cursor_;
+ std::shared_ptr<OsxCursor> ibeam_cursor_;
+};
+
+OsxCursorManagerPrivate::OsxCursorManagerPrivate(OsxCursorManager* cursor_manager) {
+ cursor_manager_ = cursor_manager;
+ arrow_cursor_ =
+ std::make_shared<OsxCursor>(cursor_manager->GetUiApplication(), SystemCursorType::Arrow);
+ hand_cursor_ =
+ std::make_shared<OsxCursor>(cursor_manager->GetUiApplication(), SystemCursorType::Hand);
+ ibeam_cursor_ =
+ std::make_shared<OsxCursor>(cursor_manager->GetUiApplication(), SystemCursorType::IBeam);
+}
+
+OsxCursorManagerPrivate::~OsxCursorManagerPrivate() {}
+}
+
+OsxCursorManager::OsxCursorManager(IUiApplication* ui_application)
+ : OsxGuiResource(ui_application) {
+ p_ = std::make_unique<details::OsxCursorManagerPrivate>(this);
+}
+
+OsxCursorManager::~OsxCursorManager() {}
+
+std::shared_ptr<ICursor> OsxCursorManager::GetSystemCursor(SystemCursorType type) {
+ switch (type) {
+ case SystemCursorType::Arrow:
+ return p_->arrow_cursor_;
+ case SystemCursorType::Hand:
+ return p_->hand_cursor_;
+ case SystemCursorType::IBeam:
+ return p_->ibeam_cursor_;
+ default:
+ throw Exception(u"Unknown system cursor type.");
+ }
+}
+}
diff --git a/src/platform/gui/osx/CursorPrivate.h b/src/platform/gui/osx/CursorPrivate.h
new file mode 100644
index 00000000..2dcfed8f
--- /dev/null
+++ b/src/platform/gui/osx/CursorPrivate.h
@@ -0,0 +1,29 @@
+#pragma once
+#include "cru/platform/gui/osx/Cursor.h"
+
+#import <AppKit/NSCursor.h>
+
+namespace cru::platform::gui::osx {
+class OsxWindow;
+
+namespace details {
+class OsxWindowPrivate;
+
+class OsxCursorPrivate {
+ friend OsxWindow;
+ friend OsxWindowPrivate;
+
+ public:
+ OsxCursorPrivate(OsxCursor* cursor, SystemCursorType cursor_type);
+
+ CRU_DELETE_COPY(OsxCursorPrivate)
+ CRU_DELETE_MOVE(OsxCursorPrivate)
+
+ ~OsxCursorPrivate();
+
+ private:
+ OsxCursor* cursor_;
+ NSCursor* ns_cursor_;
+};
+} // namespace details
+} // namespace cru::platform::gui::osx
diff --git a/src/platform/gui/osx/InputMethod.mm b/src/platform/gui/osx/InputMethod.mm
new file mode 100644
index 00000000..50ff80de
--- /dev/null
+++ b/src/platform/gui/osx/InputMethod.mm
@@ -0,0 +1,84 @@
+#include "cru/platform/gui/osx/InputMethod.h"
+
+#import <AppKit/AppKit.h>
+#include "InputMethodPrivate.h"
+#include "WindowPrivate.h"
+#include "cru/common/log/Logger.h"
+#include "cru/platform/osx/Convert.h"
+#include "cru/platform/gui/osx/Window.h"
+
+namespace cru::platform::gui::osx {
+namespace details {
+OsxInputMethodContextPrivate::OsxInputMethodContextPrivate(
+ OsxInputMethodContext* input_method_context, OsxWindow* window) {
+ input_method_context_ = input_method_context;
+ window_ = window;
+}
+
+OsxInputMethodContextPrivate::~OsxInputMethodContextPrivate() {}
+
+void OsxInputMethodContextPrivate::RaiseCompositionStartEvent() {
+ composition_start_event_.Raise(nullptr);
+}
+void OsxInputMethodContextPrivate::RaiseCompositionEndEvent() {
+ composition_end_event_.Raise(nullptr);
+}
+void OsxInputMethodContextPrivate::RaiseCompositionEvent() { composition_event_.Raise(nullptr); }
+
+void OsxInputMethodContextPrivate::RaiseTextEvent(StringView text) { text_event_.Raise(text); }
+
+void OsxInputMethodContextPrivate::PerformSel(SEL sel) {
+ // [window_->p_->GetNSWindow() performSelector:sel];
+}
+
+void OsxInputMethodContextPrivate::Activate() { is_enabled_ = true; }
+
+void OsxInputMethodContextPrivate::Deactivate() {
+ input_method_context_->CompleteComposition();
+ is_enabled_ = false;
+}
+}
+
+OsxInputMethodContext::OsxInputMethodContext(OsxWindow* window)
+ : OsxGuiResource(window->GetUiApplication()) {
+ p_ = std::make_unique<details::OsxInputMethodContextPrivate>(this, window);
+}
+
+OsxInputMethodContext::~OsxInputMethodContext() {}
+
+void OsxInputMethodContext::EnableIME() { p_->Activate(); }
+
+void OsxInputMethodContext::DisableIME() { p_->Deactivate(); }
+
+bool OsxInputMethodContext::ShouldManuallyDrawCompositionText() { return true; }
+
+void OsxInputMethodContext::CompleteComposition() {
+ // TODO: Implement this.
+}
+
+void OsxInputMethodContext::CancelComposition() {
+ [[NSTextInputContext currentInputContext] discardMarkedText];
+}
+
+CompositionText OsxInputMethodContext::GetCompositionText() { return p_->composition_text_; }
+
+void OsxInputMethodContext::SetCandidateWindowPosition(const Point& point) {
+ p_->SetCandidateWindowPosition(point);
+}
+
+IEvent<std::nullptr_t>* OsxInputMethodContext::CompositionStartEvent() {
+ return &p_->composition_start_event_;
+}
+
+IEvent<std::nullptr_t>* OsxInputMethodContext::CompositionEndEvent() {
+ return &p_->composition_end_event_;
+}
+
+IEvent<std::nullptr_t>* OsxInputMethodContext::CompositionEvent() {
+ return &p_->composition_event_;
+}
+
+IEvent<StringView>* OsxInputMethodContext::TextEvent() { return &p_->text_event_; }
+
+bool OsxInputMethodContext::IsEnabled() { return p_->is_enabled_; }
+}
diff --git a/src/platform/gui/osx/InputMethodPrivate.h b/src/platform/gui/osx/InputMethodPrivate.h
new file mode 100644
index 00000000..ac2d1bf4
--- /dev/null
+++ b/src/platform/gui/osx/InputMethodPrivate.h
@@ -0,0 +1,64 @@
+#pragma once
+#include "cru/platform/gui/osx/InputMethod.h"
+
+#include <AppKit/AppKit.h>
+
+namespace cru::platform::gui::osx {
+namespace details {
+class OsxInputMethodContextPrivate {
+ friend OsxInputMethodContext;
+
+ public:
+ OsxInputMethodContextPrivate(OsxInputMethodContext* input_method_context,
+ OsxWindow* window);
+
+ CRU_DELETE_COPY(OsxInputMethodContextPrivate)
+ CRU_DELETE_MOVE(OsxInputMethodContextPrivate)
+
+ ~OsxInputMethodContextPrivate();
+
+ void SetCompositionText(CompositionText composition_text) {
+ composition_text_ = std::move(composition_text);
+ }
+
+ void RaiseCompositionStartEvent();
+ void RaiseCompositionEndEvent();
+ void RaiseCompositionEvent();
+ void RaiseTextEvent(StringView text);
+
+ Point GetCandidateWindowPosition() const { return candidate_window_point_; }
+ void SetCandidateWindowPosition(const Point& p) {
+ candidate_window_point_ = p;
+ }
+
+ Range GetSelectionRange() const { return selection_range_; }
+ void SetSelectionRange(Range selection_range) {
+ selection_range_ = selection_range;
+ }
+
+ void PerformSel(SEL sel);
+
+ void Activate();
+ void Deactivate();
+
+ private:
+ OsxWindow* window_;
+
+ CompositionText composition_text_;
+
+ Range selection_range_;
+
+ OsxInputMethodContext* input_method_context_;
+
+ // On Osx, this is the text lefttop point on screen.
+ Point candidate_window_point_;
+
+ Event<std::nullptr_t> composition_start_event_;
+ Event<std::nullptr_t> composition_event_;
+ Event<std::nullptr_t> composition_end_event_;
+ Event<StringView> text_event_;
+
+ bool is_enabled_ = false;
+};
+} // namespace details
+} // namespace cru::platform::gui::osx
diff --git a/src/platform/gui/osx/Keyboard.mm b/src/platform/gui/osx/Keyboard.mm
new file mode 100644
index 00000000..d4489c96
--- /dev/null
+++ b/src/platform/gui/osx/Keyboard.mm
@@ -0,0 +1,283 @@
+#include "cru/platform/gui/osx/Keyboard.h"
+
+#import <AppKit/NSText.h>
+#import <Carbon/Carbon.h>
+#import "KeyboardPrivate.h"
+
+namespace cru::platform::gui::osx {
+KeyCode KeyCodeFromOsxToCru(unsigned short n) {
+ switch (n) {
+#define CRU_DEFINE_KEYCODE_MAP(osx, cru) \
+ case osx: \
+ return cru;
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_0, KeyCode::N0)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_1, KeyCode::N1)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_2, KeyCode::N2)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_3, KeyCode::N3)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_4, KeyCode::N4)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_5, KeyCode::N5)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_6, KeyCode::N6)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_7, KeyCode::N7)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_8, KeyCode::N8)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_9, KeyCode::N9)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_A, KeyCode::A)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_B, KeyCode::B)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_C, KeyCode::C)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_D, KeyCode::D)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_E, KeyCode::E)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_F, KeyCode::F)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_G, KeyCode::G)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_H, KeyCode::H)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_I, KeyCode::I)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_J, KeyCode::J)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_K, KeyCode::K)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_L, KeyCode::L)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_M, KeyCode::M)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_N, KeyCode::N)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_O, KeyCode::O)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_P, KeyCode::P)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Q, KeyCode::Q)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_R, KeyCode::R)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_S, KeyCode::S)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_T, KeyCode::T)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_U, KeyCode::U)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_V, KeyCode::V)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_W, KeyCode::W)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_X, KeyCode::X)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Y, KeyCode::Y)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Z, KeyCode::Z)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Comma, KeyCode::Comma)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Period, KeyCode::Period)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Slash, KeyCode::Slash)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Semicolon, KeyCode::Semicolon)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Quote, KeyCode::Quote)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_LeftBracket, KeyCode::LeftSquareBracket)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_RightBracket, KeyCode::RightSquareBracket)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Minus, KeyCode::Minus)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Equal, KeyCode::Equal)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Backslash, KeyCode::BackSlash)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Grave, KeyCode::GraveAccent)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Escape, KeyCode::Escape)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Tab, KeyCode::Tab)
+ CRU_DEFINE_KEYCODE_MAP(kVK_CapsLock, KeyCode::CapsLock)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Shift, KeyCode::LeftShift)
+ CRU_DEFINE_KEYCODE_MAP(kVK_RightShift, KeyCode::RightShift)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Control, KeyCode::LeftCtrl)
+ CRU_DEFINE_KEYCODE_MAP(kVK_RightControl, KeyCode::RightCtrl)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Option, KeyCode::LeftAlt)
+ CRU_DEFINE_KEYCODE_MAP(kVK_RightOption, KeyCode::RightAlt)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Command, KeyCode::LeftCommand)
+ CRU_DEFINE_KEYCODE_MAP(kVK_RightCommand, KeyCode::RightCommand)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Delete, KeyCode::Backspace)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Return, KeyCode::Return)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ForwardDelete, KeyCode::Delete)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Home, KeyCode::Home)
+ CRU_DEFINE_KEYCODE_MAP(kVK_End, KeyCode::End)
+ CRU_DEFINE_KEYCODE_MAP(kVK_PageUp, KeyCode::PageUp)
+ CRU_DEFINE_KEYCODE_MAP(kVK_PageDown, KeyCode::PageDown)
+ CRU_DEFINE_KEYCODE_MAP(kVK_LeftArrow, KeyCode::Left)
+ CRU_DEFINE_KEYCODE_MAP(kVK_RightArrow, KeyCode::Right)
+ CRU_DEFINE_KEYCODE_MAP(kVK_UpArrow, KeyCode::Up)
+ CRU_DEFINE_KEYCODE_MAP(kVK_DownArrow, KeyCode::Down)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad0, KeyCode::NumPad0)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad1, KeyCode::NumPad1)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad2, KeyCode::NumPad2)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad3, KeyCode::NumPad3)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad4, KeyCode::NumPad4)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad5, KeyCode::NumPad5)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad6, KeyCode::NumPad6)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad7, KeyCode::NumPad7)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad8, KeyCode::NumPad8)
+ CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad9, KeyCode::NumPad9)
+ CRU_DEFINE_KEYCODE_MAP(kVK_Space, KeyCode::Space)
+ default:
+ return KeyCode::Unknown;
+ }
+
+#undef CRU_DEFINE_KEYCODE_MAP
+}
+
+unsigned short KeyCodeFromCruToOsx(KeyCode k) {
+ switch (k) {
+#define CRU_DEFINE_KEYCODE_MAP(cru, osx) \
+ case cru: \
+ return osx;
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N0, kVK_ANSI_0)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N1, kVK_ANSI_1)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N2, kVK_ANSI_2)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N3, kVK_ANSI_3)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N4, kVK_ANSI_4)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N5, kVK_ANSI_5)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N6, kVK_ANSI_6)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N7, kVK_ANSI_7)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N8, kVK_ANSI_8)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N9, kVK_ANSI_9)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::A, kVK_ANSI_A)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::B, kVK_ANSI_B)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::C, kVK_ANSI_C)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::D, kVK_ANSI_D)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::E, kVK_ANSI_E)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F, kVK_ANSI_F)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::G, kVK_ANSI_G)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::H, kVK_ANSI_H)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::I, kVK_ANSI_I)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::J, kVK_ANSI_J)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::K, kVK_ANSI_K)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::L, kVK_ANSI_L)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::M, kVK_ANSI_M)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N, kVK_ANSI_N)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::O, kVK_ANSI_O)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::P, kVK_ANSI_P)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Q, kVK_ANSI_Q)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::R, kVK_ANSI_R)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::S, kVK_ANSI_S)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::T, kVK_ANSI_T)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::U, kVK_ANSI_U)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::V, kVK_ANSI_V)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::W, kVK_ANSI_W)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::X, kVK_ANSI_X)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Y, kVK_ANSI_Y)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Z, kVK_ANSI_Z)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Comma, kVK_ANSI_Comma)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Period, kVK_ANSI_Period)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Slash, kVK_ANSI_Slash)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Semicolon, kVK_ANSI_Semicolon)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Quote, kVK_ANSI_Quote)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftSquareBracket, kVK_ANSI_LeftBracket)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::RightSquareBracket, kVK_ANSI_RightBracket)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Minus, kVK_ANSI_Minus)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Equal, kVK_ANSI_Equal)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::BackSlash, kVK_ANSI_Backslash)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::GraveAccent, kVK_ANSI_Grave)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Escape, kVK_Escape)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Tab, kVK_Tab)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::CapsLock, kVK_CapsLock)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftShift, kVK_Shift)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::RightShift, kVK_RightShift)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftCtrl, kVK_Control)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::RightCtrl, kVK_RightControl)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftAlt, kVK_Option)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::RightAlt, kVK_RightOption)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftCommand, kVK_Command)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::RightCommand, kVK_RightCommand)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Backspace, kVK_Delete)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Return, kVK_Return)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Delete, kVK_ForwardDelete)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Home, kVK_Home)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::End, kVK_End)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::PageUp, kVK_PageUp)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::PageDown, kVK_PageDown)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Left, kVK_LeftArrow)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Right, kVK_RightArrow)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Up, kVK_UpArrow)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Down, kVK_DownArrow)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad0, kVK_ANSI_Keypad0)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad1, kVK_ANSI_Keypad1)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad2, kVK_ANSI_Keypad2)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad3, kVK_ANSI_Keypad3)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad4, kVK_ANSI_Keypad4)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad5, kVK_ANSI_Keypad5)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad6, kVK_ANSI_Keypad6)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad7, kVK_ANSI_Keypad7)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad8, kVK_ANSI_Keypad8)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad9, kVK_ANSI_Keypad9)
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Space, kVK_Space)
+ default:
+ return 0;
+ }
+#undef CRU_DEFINE_KEYCODE_MAP
+}
+
+NSString* ConvertKeyCodeToKeyEquivalent(KeyCode key_code) {
+#define CRU_DEFINE_KEYCODE_MAP(key_code, str) \
+ case key_code: \
+ return str;
+
+ switch (key_code) {
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::A, @"a")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::B, @"b")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::C, @"c")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::D, @"d")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::E, @"e")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F, @"f")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::G, @"g")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::H, @"h")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::I, @"i")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::J, @"j")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::K, @"k")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::L, @"l")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::M, @"m")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N, @"n")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::O, @"o")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::P, @"p")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Q, @"q")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::R, @"r")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::S, @"s")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::T, @"t")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::U, @"u")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::V, @"v")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::W, @"w")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::X, @"x")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Y, @"y")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Z, @"z")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N0, @"0")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N1, @"1")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N2, @"2")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N3, @"3")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N4, @"4")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N5, @"5")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N6, @"6")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N7, @"7")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N8, @"8")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::N9, @"9")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F1, @"F1")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F2, @"F2")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F3, @"F3")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F4, @"F4")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F5, @"F5")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F6, @"F6")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F7, @"F7")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F8, @"F8")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F9, @"F9")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F10, @"F10")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F11, @"F11")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::F12, @"F12")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Minus, @"-")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Equal, @"=")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Comma, @",")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Period, @".")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Slash, @"/")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Semicolon, @";")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Quote, @"'")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftSquareBracket, @"[")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::RightSquareBracket, @"]")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::BackSlash, @"\\")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::GraveAccent, @"`")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Return, @"\n")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Escape, @"\e")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Tab, @"\t")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Backspace, @"\x08")
+ CRU_DEFINE_KEYCODE_MAP(KeyCode::Delete, @"\x7F")
+ default:
+ throw Exception(u"Failed to convert key code to key equivalent string.");
+ }
+#undef CRU_DEFINE_KEYCODE_MAP
+}
+
+NSEventModifierFlags ConvertKeyModifier(KeyModifier k) {
+ NSEventModifierFlags flags = 0;
+ if (k & KeyModifiers::shift) {
+ flags |= NSEventModifierFlagShift;
+ }
+ if (k & KeyModifiers::ctrl) {
+ flags |= NSEventModifierFlagControl;
+ }
+ if (k & KeyModifiers::alt) {
+ flags |= NSEventModifierFlagOption;
+ }
+ if (k & KeyModifiers::command) {
+ flags |= NSEventModifierFlagCommand;
+ }
+ return flags;
+}
+}
diff --git a/src/platform/gui/osx/KeyboardPrivate.h b/src/platform/gui/osx/KeyboardPrivate.h
new file mode 100644
index 00000000..4bf53cc4
--- /dev/null
+++ b/src/platform/gui/osx/KeyboardPrivate.h
@@ -0,0 +1,9 @@
+#pragma once
+#include "cru/platform/gui/osx/Keyboard.h"
+
+#import <AppKit/NSEvent.h>
+
+namespace cru::platform::gui::osx {
+NSString* ConvertKeyCodeToKeyEquivalent(KeyCode key_code);
+NSEventModifierFlags ConvertKeyModifier(KeyModifier k);
+}
diff --git a/src/platform/gui/osx/Menu.mm b/src/platform/gui/osx/Menu.mm
new file mode 100644
index 00000000..568a5208
--- /dev/null
+++ b/src/platform/gui/osx/Menu.mm
@@ -0,0 +1,180 @@
+#include "cru/platform/gui/osx/Menu.h"
+#import "MenuPrivate.h"
+
+#include "KeyboardPrivate.h"
+#include "cru/common/platform/osx/Convert.h"
+
+#import <AppKit/NSApplication.h>
+
+namespace cru::platform::gui::osx {
+using platform::osx::Convert;
+
+namespace {
+std::unique_ptr<OsxMenu> application_menu = nullptr;
+}
+
+namespace details {
+OsxMenuItemPrivate::OsxMenuItemPrivate(OsxMenuItem* d) {
+ d_ = d;
+ sub_menu_ = new OsxMenu(d->GetUiApplication());
+ sub_menu_->p_->SetParentItem(d);
+ handler_ = [[CruOsxMenuItemClickHandler alloc] init:this];
+}
+
+OsxMenuItemPrivate::~OsxMenuItemPrivate() { delete sub_menu_; }
+
+void OsxMenuItemPrivate::AttachToNative(NSMenuItem* native_menu_item, bool check_submenu) {
+ Expects(sub_menu_);
+
+ menu_item_ = native_menu_item;
+ [native_menu_item setTarget:handler_];
+ [native_menu_item setAction:@selector(handleClick)];
+ if (check_submenu && [native_menu_item hasSubmenu]) {
+ sub_menu_->p_->AttachToNative([native_menu_item submenu]);
+ }
+}
+
+OsxMenuPrivate::OsxMenuPrivate(OsxMenu* d) { d_ = d; }
+
+OsxMenuPrivate::~OsxMenuPrivate() {
+ for (auto item : items_) {
+ delete item;
+ }
+}
+
+void OsxMenuPrivate::AttachToNative(NSMenu* native_menu) {
+ menu_ = native_menu;
+
+ auto item_count = [native_menu numberOfItems];
+ for (int i = 0; i < item_count; i++) {
+ auto native_item = [native_menu itemAtIndex:i];
+ auto item = new OsxMenuItem(d_->GetUiApplication());
+ item->p_->SetParentMenu(d_);
+ item->p_->AttachToNative(native_item, true);
+ items_.push_back(item);
+ }
+}
+}
+
+OsxMenuItem::OsxMenuItem(IUiApplication* ui_application) : OsxGuiResource(ui_application) {
+ p_ = new details::OsxMenuItemPrivate(this);
+}
+
+OsxMenuItem::~OsxMenuItem() { delete p_; }
+
+String OsxMenuItem::GetTitle() { return Convert((CFStringRef)[p_->menu_item_ title]); }
+
+void OsxMenuItem::SetTitle(String title) { [p_->menu_item_ setTitle:(NSString*)Convert(title)]; }
+
+bool OsxMenuItem::IsEnabled() { return [p_->menu_item_ isEnabled]; }
+
+void OsxMenuItem::SetEnabled(bool enabled) { [p_->menu_item_ setEnabled:enabled]; }
+
+IMenu* OsxMenuItem::GetParentMenu() { return p_->parent_menu_; }
+
+IMenu* OsxMenuItem::GetSubmenu() { return p_->sub_menu_; }
+
+void OsxMenuItem::SetKeyboardShortcut(KeyCode key, KeyModifier modifiers) {
+ [p_->menu_item_ setKeyEquivalent:ConvertKeyCodeToKeyEquivalent(key)];
+ [p_->menu_item_ setKeyEquivalentModifierMask:ConvertKeyModifier(modifiers)];
+}
+
+void OsxMenuItem::DeleteKeyboardShortcut() {
+ [p_->menu_item_ setKeyEquivalent:@""];
+ [p_->menu_item_ setKeyEquivalentModifierMask:0];
+}
+
+void OsxMenuItem::SetOnClickHandler(std::function<void()> handler) {
+ p_->on_click_handler_ = std::move(handler);
+}
+
+OsxMenu* OsxMenu::CreateOrGetApplicationMenu(IUiApplication* ui_application) {
+ if (application_menu) {
+ return application_menu.get();
+ }
+
+ NSMenu* native_main_menu = [[NSMenu alloc] init];
+ [NSApp setMainMenu:native_main_menu];
+ [native_main_menu setAutoenablesItems:NO];
+
+ application_menu.reset(new OsxMenu(ui_application));
+ application_menu->p_->AttachToNative(native_main_menu);
+
+ application_menu->CreateItemAt(0);
+
+ return application_menu.get();
+}
+
+OsxMenu::OsxMenu(IUiApplication* ui_application) : OsxGuiResource(ui_application) {
+ p_ = new details::OsxMenuPrivate(this);
+}
+
+OsxMenu::~OsxMenu() { delete p_; }
+
+IMenuItem* OsxMenu::GetItemAt(int index) {
+ if (index < 0 || index >= p_->items_.size()) {
+ return nullptr;
+ }
+
+ return p_->items_[index];
+}
+
+int OsxMenu::GetItemCount() { return p_->items_.size(); }
+
+IMenuItem* OsxMenu::CreateItemAt(int index) {
+ if (index < 0) index = 0;
+ if (index > p_->items_.size()) index = p_->items_.size();
+
+ if (p_->parent_item_ && p_->items_.empty()) {
+ Expects(p_->menu_ == nullptr);
+ p_->menu_ = [[NSMenu alloc] init];
+ [p_->menu_ setAutoenablesItems:NO];
+ [p_->parent_item_->p_->GetNative() setSubmenu:p_->menu_];
+ }
+
+ auto native_item = [[NSMenuItem alloc] init];
+ [p_->menu_ insertItem:native_item atIndex:index];
+
+ auto item = new OsxMenuItem(GetUiApplication());
+ item->p_->SetParentMenu(this);
+ item->p_->AttachToNative(native_item, false);
+ p_->items_.insert(p_->items_.begin() + index, item);
+
+ return item;
+}
+
+void OsxMenu::RemoveItemAt(int index) {
+ if (index < 0 || index >= p_->items_.size()) {
+ return;
+ }
+
+ auto item = p_->items_[index];
+ [p_->menu_ removeItem:item->p_->GetNative()];
+ p_->items_.erase(p_->items_.begin() + index);
+
+ delete item;
+
+ if (p_->items_.empty() && p_->parent_item_) {
+ Expects(p_->menu_ != nullptr);
+ [p_->parent_item_->p_->GetNative() setSubmenu:nullptr];
+ p_->menu_ = nullptr;
+ }
+}
+}
+
+@implementation CruOsxMenuItemClickHandler {
+ cru::platform::gui::osx::details::OsxMenuItemPrivate* p_;
+}
+
+- (id)init:(cru::platform::gui::osx::details::OsxMenuItemPrivate*)p {
+ p_ = p;
+ return self;
+}
+
+- (void)handleClick {
+ if (p_->GetOnClickHandler()) {
+ p_->GetOnClickHandler()();
+ }
+}
+
+@end
diff --git a/src/platform/gui/osx/MenuPrivate.h b/src/platform/gui/osx/MenuPrivate.h
new file mode 100644
index 00000000..cda8216b
--- /dev/null
+++ b/src/platform/gui/osx/MenuPrivate.h
@@ -0,0 +1,65 @@
+#pragma once
+#include "cru/platform/gui/osx/Menu.h"
+
+#import <AppKit/NSMenu.h>
+#import <AppKit/NSMenuItem.h>
+
+@interface CruOsxMenuItemClickHandler : NSObject
+- init:(cru::platform::gui::osx::details::OsxMenuItemPrivate*)p;
+- (void)handleClick;
+@end
+
+namespace cru::platform::gui::osx {
+namespace details {
+
+class OsxMenuItemPrivate {
+ friend OsxMenuItem;
+
+ public:
+ explicit OsxMenuItemPrivate(OsxMenuItem* d);
+
+ CRU_DELETE_COPY(OsxMenuItemPrivate)
+ CRU_DELETE_MOVE(OsxMenuItemPrivate)
+
+ ~OsxMenuItemPrivate();
+
+ public:
+ NSMenuItem* GetNative() { return menu_item_; }
+ void SetParentMenu(OsxMenu* menu) { parent_menu_ = menu; }
+ void AttachToNative(NSMenuItem* native_menu_item, bool check_submenu);
+
+ const std::function<void()> GetOnClickHandler() const { return on_click_handler_; }
+
+ private:
+ OsxMenuItem* d_;
+ OsxMenu* parent_menu_ = nullptr;
+ NSMenuItem* menu_item_ = nullptr;
+ OsxMenu* sub_menu_ = nullptr;
+ std::function<void()> on_click_handler_;
+ CruOsxMenuItemClickHandler* handler_;
+};
+
+class OsxMenuPrivate {
+ friend OsxMenu;
+
+ public:
+ explicit OsxMenuPrivate(OsxMenu* d);
+
+ CRU_DELETE_COPY(OsxMenuPrivate)
+ CRU_DELETE_MOVE(OsxMenuPrivate)
+
+ ~OsxMenuPrivate();
+
+ public:
+ void SetParentItem(OsxMenuItem* item) { parent_item_ = item; }
+ void AttachToNative(NSMenu* native_menu);
+
+ private:
+ OsxMenu* d_;
+ OsxMenuItem* parent_item_ = nullptr;
+ NSMenu* menu_ = nullptr;
+ std::vector<OsxMenuItem*> items_;
+};
+} // namespace details
+
+} // namespace cru::platform::gui::osx
diff --git a/src/platform/gui/osx/Resource.cpp b/src/platform/gui/osx/Resource.cpp
new file mode 100644
index 00000000..d33133c7
--- /dev/null
+++ b/src/platform/gui/osx/Resource.cpp
@@ -0,0 +1,6 @@
+#include "cru/platform/gui/osx/Resource.h"
+
+namespace cru::platform::gui::osx {
+OsxGuiResource::OsxGuiResource(IUiApplication* ui_application)
+ : ui_application_(ui_application) {}
+} // namespace cru::platform::gui::osx
diff --git a/src/platform/gui/osx/UiApplication.mm b/src/platform/gui/osx/UiApplication.mm
new file mode 100644
index 00000000..ef62af58
--- /dev/null
+++ b/src/platform/gui/osx/UiApplication.mm
@@ -0,0 +1,260 @@
+#include "cru/platform/gui/osx/UiApplication.h"
+
+#include "ClipboardPrivate.h"
+#include "cru/common/log/Logger.h"
+#include "cru/common/platform/osx/Convert.h"
+#include "cru/platform/graphics/quartz/Factory.h"
+#include "cru/platform/gui/osx/Clipboard.h"
+#include "cru/platform/gui/osx/Cursor.h"
+#include "cru/platform/gui/osx/Menu.h"
+#include "cru/platform/gui/osx/Window.h"
+#include "cru/platform/graphics/Factory.h"
+#include "cru/platform/gui/Base.h"
+#include "cru/platform/gui/UiApplication.h"
+#include "cru/platform/gui/Window.h"
+
+#include <AppKit/NSApplication.h>
+#include <Foundation/NSRunLoop.h>
+#include <UniformTypeIdentifiers/UTType.h>
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <unordered_map>
+#include <vector>
+
+@interface CruAppDelegate : NSObject <NSApplicationDelegate>
+- (id)init:(cru::platform::gui::osx::details::OsxUiApplicationPrivate*)p;
+- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender;
+- (void)applicationWillTerminate:(NSNotification*)notification;
+@end
+
+namespace cru::platform::gui::osx {
+
+using cru::platform::osx::Convert;
+
+namespace details {
+class OsxUiApplicationPrivate {
+ friend OsxUiApplication;
+
+ public:
+ explicit OsxUiApplicationPrivate(OsxUiApplication* osx_ui_application)
+ : osx_ui_application_(osx_ui_application) {
+ app_delegate_ = [[CruAppDelegate alloc] init:this];
+ }
+
+ CRU_DELETE_COPY(OsxUiApplicationPrivate)
+ CRU_DELETE_MOVE(OsxUiApplicationPrivate)
+
+ ~OsxUiApplicationPrivate() = default;
+
+ void CallQuitHandlers();
+
+ private:
+ OsxUiApplication* osx_ui_application_;
+ CruAppDelegate* app_delegate_;
+ std::vector<std::function<void()>> quit_handlers_;
+ bool quit_on_all_window_closed_ = true;
+
+ long long current_timer_id_ = 1;
+ std::unordered_map<long long, std::function<void()>> next_tick_;
+ std::unordered_map<long long, NSTimer*> timers_;
+
+ std::vector<OsxWindow*> windows_;
+
+ std::unique_ptr<OsxCursorManager> cursor_manager_;
+
+ std::unique_ptr<OsxClipboard> clipboard_;
+
+ std::unique_ptr<platform::graphics::quartz::QuartzGraphicsFactory> quartz_graphics_factory_;
+};
+
+void OsxUiApplicationPrivate::CallQuitHandlers() {
+ for (const auto& handler : quit_handlers_) {
+ handler();
+ }
+}
+}
+
+OsxUiApplication::OsxUiApplication()
+ : OsxGuiResource(this), p_(new details::OsxUiApplicationPrivate(this)) {
+ [NSApplication sharedApplication];
+
+ [NSApp setDelegate:p_->app_delegate_];
+ p_->quartz_graphics_factory_ = std::make_unique<graphics::quartz::QuartzGraphicsFactory>();
+ p_->cursor_manager_ = std::make_unique<OsxCursorManager>(this);
+ p_->clipboard_ = std::make_unique<OsxClipboard>(
+ this, std::make_unique<details::OsxClipboardPrivate>([NSPasteboard generalPasteboard]));
+}
+
+OsxUiApplication::~OsxUiApplication() {}
+
+int OsxUiApplication::Run() {
+ [NSApp run];
+ return 0;
+}
+
+void OsxUiApplication::RequestQuit(int quit_code) {
+ [NSApp terminate:[NSNumber numberWithInteger:quit_code]];
+}
+
+void OsxUiApplication::AddOnQuitHandler(std::function<void()> handler) {
+ p_->quit_handlers_.push_back(std::move(handler));
+}
+
+bool OsxUiApplication::IsQuitOnAllWindowClosed() { return p_->quit_on_all_window_closed_; }
+
+void OsxUiApplication::SetQuitOnAllWindowClosed(bool quit_on_all_window_closed) {
+ p_->quit_on_all_window_closed_ = quit_on_all_window_closed;
+}
+
+long long OsxUiApplication::SetImmediate(std::function<void()> action) {
+ const long long id = p_->current_timer_id_++;
+ p_->next_tick_.emplace(id, std::move(action));
+
+ [[NSRunLoop mainRunLoop] performBlock:^{
+ const auto i = p_->next_tick_.find(id);
+ if (i != p_->next_tick_.cend()) {
+ i->second();
+ }
+ p_->next_tick_.erase(i);
+ }];
+
+ return id;
+}
+
+long long OsxUiApplication::SetTimeout(std::chrono::milliseconds milliseconds,
+ std::function<void()> action) {
+ long long id = p_->current_timer_id_++;
+ p_->timers_.emplace(id, [NSTimer scheduledTimerWithTimeInterval:milliseconds.count() / 1000.0
+ repeats:false
+ block:^(NSTimer* timer) {
+ action();
+ p_->timers_.erase(id);
+ }]);
+
+ return id;
+}
+
+long long OsxUiApplication::SetInterval(std::chrono::milliseconds milliseconds,
+ std::function<void()> action) {
+ long long id = p_->current_timer_id_++;
+ p_->timers_.emplace(id, [NSTimer scheduledTimerWithTimeInterval:milliseconds.count() / 1000.0
+ repeats:true
+ block:^(NSTimer* timer) {
+ action();
+ }]);
+
+ return id;
+}
+
+void OsxUiApplication::CancelTimer(long long id) {
+ p_->next_tick_.erase(id);
+ auto i = p_->timers_.find(id);
+ if (i != p_->timers_.cend()) {
+ [i->second invalidate];
+ p_->timers_.erase(i);
+ }
+}
+
+std::vector<INativeWindow*> OsxUiApplication::GetAllWindow() {
+ std::vector<INativeWindow*> result;
+ std::transform(p_->windows_.cbegin(), p_->windows_.cend(), std::back_inserter(result),
+ [](OsxWindow* w) { return static_cast<INativeWindow*>(w); });
+ return result;
+}
+
+INativeWindow* OsxUiApplication::CreateWindow() {
+ auto window = new OsxWindow(this);
+ p_->windows_.push_back(window);
+ return window;
+}
+
+ICursorManager* OsxUiApplication::GetCursorManager() { return p_->cursor_manager_.get(); }
+
+IClipboard* OsxUiApplication::GetClipboard() { return p_->clipboard_.get(); }
+
+IMenu* OsxUiApplication::GetApplicationMenu() { return OsxMenu::CreateOrGetApplicationMenu(this); }
+
+graphics::IGraphicsFactory* OsxUiApplication::GetGraphicsFactory() {
+ return p_->quartz_graphics_factory_.get();
+}
+
+std::optional<String> OsxUiApplication::ShowSaveDialog(SaveDialogOptions options) {
+ NSSavePanel* panel = [NSSavePanel savePanel];
+ [panel setTitle:(NSString*)Convert(options.title)];
+ [panel setPrompt:(NSString*)Convert(options.prompt)];
+ [panel setMessage:(NSString*)Convert(options.message)];
+
+ NSMutableArray* allowed_content_types = [NSMutableArray array];
+
+ for (const auto& file_type : options.allowed_file_types) {
+ [allowed_content_types
+ addObject:[UTType typeWithFilenameExtension:(NSString*)Convert(file_type)]];
+ }
+
+ [panel setAllowedContentTypes:allowed_content_types];
+ [panel setAllowsOtherFileTypes:options.allow_all_file_types];
+
+ auto model_result = [panel runModal];
+ if (model_result == NSModalResponseOK) {
+ return Convert((CFStringRef)[[panel URL] path]);
+ } else {
+ return std::nullopt;
+ }
+}
+
+std::optional<std::vector<String>> OsxUiApplication::ShowOpenDialog(OpenDialogOptions options) {
+ NSOpenPanel* panel = [NSOpenPanel openPanel];
+ [panel setTitle:(NSString*)Convert(options.title)];
+ [panel setPrompt:(NSString*)Convert(options.prompt)];
+ [panel setMessage:(NSString*)Convert(options.message)];
+
+ NSMutableArray* allowed_content_types = [NSMutableArray array];
+
+ for (const auto& file_type : options.allowed_file_types) {
+ [allowed_content_types
+ addObject:[UTType typeWithFilenameExtension:(NSString*)Convert(file_type)]];
+ }
+
+ [panel setAllowedContentTypes:allowed_content_types];
+ [panel setAllowsOtherFileTypes:options.allow_all_file_types];
+
+ [panel setCanChooseFiles:options.can_choose_files];
+ [panel setCanChooseDirectories:options.can_choose_directories];
+ [panel setAllowsMultipleSelection:options.allow_mulitple_selection];
+
+ auto model_result = [panel runModal];
+ if (model_result == NSModalResponseOK) {
+ std::vector<String> result;
+ for (NSURL* url in [panel URLs]) {
+ result.push_back(Convert((CFStringRef)[url path]));
+ }
+ return result;
+ } else {
+ return std::nullopt;
+ }
+}
+
+void OsxUiApplication::UnregisterWindow(OsxWindow* window) {
+ p_->windows_.erase(
+ std::remove(p_->windows_.begin(), p_->windows_.end(), static_cast<INativeWindow*>(window)),
+ p_->windows_.cend());
+}
+}
+
+@implementation CruAppDelegate {
+ cru::platform::gui::osx::details::OsxUiApplicationPrivate* _p;
+}
+
+- (id)init:(cru::platform::gui::osx::details::OsxUiApplicationPrivate*)p {
+ _p = p;
+ return self;
+}
+- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender {
+ return NSApplicationTerminateReply::NSTerminateNow;
+}
+- (void)applicationWillTerminate:(NSNotification*)notification {
+ _p->CallQuitHandlers();
+}
+@end
diff --git a/src/platform/gui/osx/Window.mm b/src/platform/gui/osx/Window.mm
new file mode 100644
index 00000000..2c55d2dd
--- /dev/null
+++ b/src/platform/gui/osx/Window.mm
@@ -0,0 +1,800 @@
+#include "cru/platform/gui/osx/Window.h"
+#include "WindowPrivate.h"
+
+#include "CursorPrivate.h"
+#include "InputMethodPrivate.h"
+#include "cru/common/Range.h"
+#include "cru/common/log/Logger.h"
+#include "cru/platform/osx/Convert.h"
+#include "cru/platform/graphics/quartz/Convert.h"
+#include "cru/platform/graphics/quartz/Painter.h"
+#include "cru/platform/gui/osx/Cursor.h"
+#include "cru/platform/gui/osx/InputMethod.h"
+#include "cru/platform/gui/osx/Keyboard.h"
+#include "cru/platform/gui/osx/Resource.h"
+#include "cru/platform/gui/osx/UiApplication.h"
+#include "cru/platform/Check.h"
+#include "cru/platform/graphics/NullPainter.h"
+#include "cru/platform/gui/TimerHelper.h"
+
+#include <AppKit/AppKit.h>
+#include <Foundation/Foundation.h>
+
+#include <limits>
+#include <memory>
+#include <unordered_set>
+
+namespace {
+constexpr int key_down_debug = 0;
+}
+
+using cru::platform::osx::Convert;
+using cru::platform::graphics::quartz::Convert;
+
+namespace cru::platform::gui::osx {
+namespace {
+inline NSWindowStyleMask CalcWindowStyleMask(WindowStyleFlag flag) {
+ return flag & WindowStyleFlags::NoCaptionAndBorder
+ ? NSWindowStyleMaskBorderless
+ : NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
+ NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
+}
+}
+
+namespace details {
+OsxWindowPrivate::OsxWindowPrivate(OsxWindow* osx_window) : osx_window_(osx_window) {
+ window_delegate_ = [[CruWindowDelegate alloc] init:this];
+
+ content_rect_ = {100, 100, 400, 200};
+
+ input_method_context_ = std::make_unique<OsxInputMethodContext>(osx_window);
+}
+
+OsxWindowPrivate::~OsxWindowPrivate() {}
+
+void OsxWindowPrivate::OnWindowWillClose() {
+ if (window_) destroy_event_.Raise(nullptr);
+ window_ = nil;
+ CGLayerRelease(draw_layer_);
+ draw_layer_ = nullptr;
+
+ if (osx_window_->GetUiApplication()->IsQuitOnAllWindowClosed()) {
+ const auto& all_window = osx_window_->GetUiApplication()->GetAllWindow();
+
+ bool quit = true;
+
+ for (auto window : all_window) {
+ auto w = CheckPlatform<OsxWindow>(window, osx_window_->GetPlatformId());
+ if (w->p_->window_) {
+ quit = false;
+ break;
+ }
+ }
+
+ if (quit) {
+ osx_window_->GetUiApplication()->RequestQuit(0);
+ }
+ }
+}
+
+void OsxWindowPrivate::OnWindowDidExpose() { osx_window_->RequestRepaint(); }
+void OsxWindowPrivate::OnWindowDidUpdate() {}
+void OsxWindowPrivate::OnWindowDidMove() { content_rect_ = RetrieveContentRect(); }
+
+void OsxWindowPrivate::OnWindowDidResize() {
+ content_rect_ = RetrieveContentRect();
+
+ auto view = [window_ contentView];
+ [view removeTrackingArea:[view trackingAreas][0]];
+ auto tracking_area = [[NSTrackingArea alloc]
+ initWithRect:CGRectMake(0, 0, content_rect_.width, content_rect_.height)
+ options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways)
+ owner:view
+ userInfo:nil];
+ [view addTrackingArea:tracking_area];
+
+ CGLayerRelease(draw_layer_);
+ draw_layer_ = CreateLayer(Convert(content_rect_.GetSize()));
+
+ resize_event_.Raise(osx_window_->GetClientSize());
+
+ osx_window_->RequestRepaint();
+}
+
+void OsxWindowPrivate::OnBecomeKeyWindow() { focus_event_.Raise(FocusChangeType::Gain); }
+
+void OsxWindowPrivate::OnResignKeyWindow() { focus_event_.Raise(FocusChangeType::Lose); }
+
+void OsxWindowPrivate::OnMouseEnterLeave(MouseEnterLeaveType type) {
+ mouse_enter_leave_event_.Raise(type);
+ if (type == MouseEnterLeaveType::Enter) {
+ mouse_in_ = true;
+ UpdateCursor();
+ } else {
+ mouse_in_ = false;
+ }
+}
+
+void OsxWindowPrivate::OnMouseMove(Point p) { mouse_move_event_.Raise(TransformMousePoint(p)); }
+
+void OsxWindowPrivate::OnMouseDown(MouseButton button, Point p, KeyModifier key_modifier) {
+ mouse_down_event_.Raise({button, TransformMousePoint(p), key_modifier});
+}
+
+void OsxWindowPrivate::OnMouseUp(MouseButton button, Point p, KeyModifier key_modifier) {
+ mouse_up_event_.Raise({button, TransformMousePoint(p), key_modifier});
+}
+
+void OsxWindowPrivate::OnMouseWheel(float delta, Point p, KeyModifier key_modifier,
+ bool horizontal) {
+ mouse_wheel_event_.Raise({delta, TransformMousePoint(p), key_modifier, horizontal});
+}
+
+void OsxWindowPrivate::OnKeyDown(KeyCode key, KeyModifier key_modifier) {
+ key_down_event_.Raise({key, key_modifier});
+}
+
+void OsxWindowPrivate::OnKeyUp(KeyCode key, KeyModifier key_modifier) {
+ key_up_event_.Raise({key, key_modifier});
+}
+
+CGLayerRef OsxWindowPrivate::CreateLayer(const CGSize& size) {
+ auto s = size;
+ if (s.width == 0) s.width = 1;
+ if (s.height == 0) s.height = 1;
+
+ auto draw_layer = CGLayerCreateWithContext(nullptr, s, nullptr);
+ Ensures(draw_layer);
+
+ return draw_layer;
+}
+
+void OsxWindowPrivate::UpdateCursor() {
+ auto cursor = cursor_ == nullptr
+ ? std::dynamic_pointer_cast<OsxCursor>(
+ osx_window_->GetUiApplication()->GetCursorManager()->GetSystemCursor(
+ SystemCursorType::Arrow))
+ : cursor_;
+
+ [cursor->p_->ns_cursor_ set];
+}
+
+Point OsxWindowPrivate::TransformMousePoint(const Point& point) {
+ Point r = point;
+ r.y = content_rect_.height - r.y;
+ return r;
+}
+
+void OsxWindowPrivate::CreateWindow() {
+ Expects(!window_);
+
+ NSWindowStyleMask style_mask = CalcWindowStyleMask(style_flag_);
+ window_ = [[CruWindow alloc] init:this
+ contentRect:{0, 0, content_rect_.width, content_rect_.height}
+ style:style_mask];
+ Ensures(window_);
+
+ osx_window_->SetClientRect(content_rect_);
+
+ [window_ setDelegate:window_delegate_];
+
+ if (parent_) {
+ auto parent = CheckPlatform<OsxWindow>(parent_, this->osx_window_->GetPlatformId());
+ [window_ setParentWindow:parent->p_->window_];
+ }
+
+ NSView* content_view = [[CruView alloc] init:this
+ input_context_p:input_method_context_->p_.get()
+ frame:Rect(Point{}, content_rect_.GetSize())];
+
+ [window_ setContentView:content_view];
+
+ auto title_str = Convert(title_);
+ [window_ setTitle:(NSString*)title_str];
+ CFRelease(title_str);
+
+ draw_layer_ = CreateLayer(Convert(content_rect_.GetSize()));
+
+ create_event_.Raise(nullptr);
+
+ osx_window_->RequestRepaint();
+}
+
+Size OsxWindowPrivate::GetScreenSize() {
+ auto screen = window_ ? [window_ screen] : [NSScreen mainScreen];
+ auto size = [screen frame].size;
+ return Convert(size);
+}
+
+Rect OsxWindowPrivate::RetrieveContentRect() {
+ NSRect rect = [NSWindow contentRectForFrameRect:[window_ frame]
+ styleMask:CalcWindowStyleMask(style_flag_)];
+ rect.origin.y = GetScreenSize().height - rect.origin.y - rect.size.height;
+ return cru::platform::graphics::quartz::Convert(rect);
+}
+
+}
+
+OsxWindow::OsxWindow(OsxUiApplication* ui_application)
+ : OsxGuiResource(ui_application), p_(new details::OsxWindowPrivate(this)) {}
+
+OsxWindow::~OsxWindow() {
+ if (p_->window_) {
+ [p_->window_ close];
+ }
+ dynamic_cast<OsxUiApplication*>(GetUiApplication())->UnregisterWindow(this);
+}
+
+void OsxWindow::Close() {
+ if (p_->window_) {
+ [p_->window_ close];
+ }
+}
+
+INativeWindow* OsxWindow::GetParent() { return p_->parent_; }
+
+void OsxWindow::SetParent(INativeWindow* parent) {
+ auto p = CheckPlatform<OsxWindow>(parent, GetPlatformId());
+
+ p_->parent_ = parent;
+
+ if (p_->window_) {
+ [p_->window_ setParentWindow:p->p_->window_];
+ }
+}
+
+WindowStyleFlag OsxWindow::GetStyleFlag() { return p_->style_flag_; }
+
+void OsxWindow::SetStyleFlag(WindowStyleFlag flag) {
+ p_->style_flag_ = flag;
+
+ if (p_->window_) {
+ [p_->window_ close];
+ }
+}
+
+String OsxWindow::GetTitle() { return p_->title_; }
+
+void OsxWindow::SetTitle(String title) {
+ p_->title_ = title;
+
+ if (p_->window_) {
+ auto str = Convert(title);
+ [p_->window_ setTitle:(NSString*)str];
+ CFRelease(str);
+ }
+}
+
+WindowVisibilityType OsxWindow::GetVisibility() {
+ if (!p_->window_) return WindowVisibilityType::Hide;
+ if ([p_->window_ isMiniaturized]) return WindowVisibilityType::Minimize;
+ return [p_->window_ isVisible] ? WindowVisibilityType::Show : WindowVisibilityType::Hide;
+}
+
+void OsxWindow::SetVisibility(WindowVisibilityType visibility) {
+ if (p_->window_) {
+ if (visibility == WindowVisibilityType::Show) {
+ [p_->window_ orderFront:nil];
+ p_->visibility_change_event_.Raise(WindowVisibilityType::Show);
+ } else if (visibility == WindowVisibilityType::Hide) {
+ [p_->window_ orderOut:nil];
+ p_->visibility_change_event_.Raise(WindowVisibilityType::Hide);
+ } else if (visibility == WindowVisibilityType::Minimize) {
+ [p_->window_ miniaturize:nil];
+ }
+ } else {
+ if (visibility == WindowVisibilityType::Show) {
+ p_->CreateWindow();
+ [p_->window_ orderFront:nil];
+ p_->visibility_change_event_.Raise(WindowVisibilityType::Show);
+ }
+ }
+}
+
+Size OsxWindow::GetClientSize() { return p_->content_rect_.GetSize(); }
+
+void OsxWindow::SetClientSize(const Size& size) {
+ if (p_->window_) {
+ auto rect = GetClientRect();
+ rect.SetSize(size);
+ SetClientRect(rect);
+ } else {
+ p_->content_rect_.SetSize(size);
+ }
+}
+
+Rect OsxWindow::GetClientRect() { return p_->content_rect_; }
+
+void OsxWindow::SetClientRect(const Rect& rect) {
+ if (p_->window_) {
+ auto r = Convert(rect);
+ r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height;
+ r = [NSWindow frameRectForContentRect:r styleMask:CalcWindowStyleMask(p_->style_flag_)];
+ [p_->window_ setFrame:r display:false];
+ } else {
+ p_->content_rect_ = rect;
+ }
+}
+
+Rect OsxWindow::GetWindowRect() {
+ auto r = Convert(p_->content_rect_);
+ r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height;
+ r = [NSWindow frameRectForContentRect:r styleMask:CalcWindowStyleMask(p_->style_flag_)];
+ r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height;
+ return Convert(r);
+}
+
+void OsxWindow::SetWindowRect(const Rect& rect) {
+ auto r = Convert(rect);
+ r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height;
+ r = [NSWindow frameRectForContentRect:r styleMask:CalcWindowStyleMask(p_->style_flag_)];
+ r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height;
+ SetClientRect(Convert(r));
+}
+
+void OsxWindow::RequestRepaint() {
+ if (!p_->draw_timer_) {
+ p_->draw_timer_ = GetUiApplication()->SetImmediate([this] {
+ p_->paint_event_.Raise(nullptr);
+ p_->draw_timer_.Release();
+ });
+ }
+}
+
+std::unique_ptr<graphics::IPainter> OsxWindow::BeginPaint() {
+ if (!p_->window_) {
+ return std::make_unique<graphics::NullPainter>();
+ }
+
+ CGContextRef cg_context = CGLayerGetContext(p_->draw_layer_);
+
+ return std::make_unique<cru::platform::graphics::quartz::QuartzCGContextPainter>(
+ GetUiApplication()->GetGraphicsFactory(), cg_context, false, GetClientSize(),
+ [this](graphics::quartz::QuartzCGContextPainter*) {
+ [[p_->window_ contentView] setNeedsDisplay:YES];
+ });
+}
+
+bool OsxWindow::RequestFocus() {
+ if (!p_->window_) return false;
+ [p_->window_ makeKeyWindow];
+ return true;
+}
+
+Point OsxWindow::GetMousePosition() {
+ auto p = [p_->window_ mouseLocationOutsideOfEventStream];
+ return Point(p.x, p.y);
+}
+
+bool OsxWindow::CaptureMouse() { return true; }
+
+bool OsxWindow::ReleaseMouse() { return true; }
+
+void OsxWindow::SetCursor(std::shared_ptr<ICursor> cursor) {
+ p_->cursor_ = CheckPlatform<OsxCursor>(cursor, GetPlatformId());
+ p_->UpdateCursor();
+}
+
+void OsxWindow::SetToForeground() {
+ if (!p_->window_) return;
+ [p_->window_ makeMainWindow];
+ [p_->window_ orderFrontRegardless];
+}
+
+IEvent<std::nullptr_t>* OsxWindow::CreateEvent() { return &p_->create_event_; }
+IEvent<std::nullptr_t>* OsxWindow::DestroyEvent() { return &p_->destroy_event_; }
+IEvent<std::nullptr_t>* OsxWindow::PaintEvent() { return &p_->paint_event_; }
+IEvent<WindowVisibilityType>* OsxWindow::VisibilityChangeEvent() {
+ return &p_->visibility_change_event_;
+}
+IEvent<Size>* OsxWindow::ResizeEvent() { return &p_->resize_event_; }
+IEvent<FocusChangeType>* OsxWindow::FocusEvent() { return &p_->focus_event_; }
+IEvent<MouseEnterLeaveType>* OsxWindow::MouseEnterLeaveEvent() {
+ return &p_->mouse_enter_leave_event_;
+}
+IEvent<Point>* OsxWindow::MouseMoveEvent() { return &p_->mouse_move_event_; }
+IEvent<NativeMouseButtonEventArgs>* OsxWindow::MouseDownEvent() { return &p_->mouse_down_event_; }
+IEvent<NativeMouseButtonEventArgs>* OsxWindow::MouseUpEvent() { return &p_->mouse_up_event_; }
+IEvent<NativeMouseWheelEventArgs>* OsxWindow::MouseWheelEvent() { return &p_->mouse_wheel_event_; }
+IEvent<NativeKeyEventArgs>* OsxWindow::KeyDownEvent() { return &p_->key_down_event_; }
+IEvent<NativeKeyEventArgs>* OsxWindow::KeyUpEvent() { return &p_->key_up_event_; }
+
+IInputMethodContext* OsxWindow::GetInputMethodContext() { return p_->input_method_context_.get(); }
+}
+
+namespace {
+cru::platform::gui::KeyModifier GetKeyModifier(NSEvent* event) {
+ cru::platform::gui::KeyModifier key_modifier;
+ if (event.modifierFlags & NSEventModifierFlagControl)
+ key_modifier |= cru::platform::gui::KeyModifiers::ctrl;
+ if (event.modifierFlags & NSEventModifierFlagOption)
+ key_modifier |= cru::platform::gui::KeyModifiers::alt;
+ if (event.modifierFlags & NSEventModifierFlagShift)
+ key_modifier |= cru::platform::gui::KeyModifiers::shift;
+ if (event.modifierFlags & NSEventModifierFlagCommand)
+ key_modifier |= cru::platform::gui::KeyModifiers::command;
+ return key_modifier;
+}
+}
+
+@implementation CruWindow {
+ cru::platform::gui::osx::details::OsxWindowPrivate* _p;
+}
+
+- (instancetype)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p
+ contentRect:(NSRect)contentRect
+ style:(NSWindowStyleMask)style {
+ [super initWithContentRect:contentRect
+ styleMask:style
+ backing:NSBackingStoreBuffered
+ defer:false];
+ _p = p;
+
+ [self setAcceptsMouseMovedEvents:YES];
+
+ return self;
+}
+
+- (BOOL)canBecomeMainWindow {
+ return YES;
+}
+
+- (BOOL)canBecomeKeyWindow {
+ return YES;
+}
+@end
+
+@implementation CruView {
+ cru::platform::gui::osx::details::OsxWindowPrivate* _p;
+ cru::platform::gui::osx::details::OsxInputMethodContextPrivate* _input_context_p;
+ NSMutableAttributedString* _input_context_text;
+}
+
+- (instancetype)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p
+ input_context_p:
+ (cru::platform::gui::osx::details::OsxInputMethodContextPrivate*)input_context_p
+ frame:(cru::platform::Rect)frame {
+ [super initWithFrame:cru::platform::graphics::quartz::Convert(frame)];
+ _p = p;
+ _input_context_p = input_context_p;
+
+ auto tracking_area = [[NSTrackingArea alloc]
+ initWithRect:Convert(frame)
+ options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways)
+ owner:self
+ userInfo:nil];
+ [self addTrackingArea:tracking_area];
+
+ return self;
+}
+
+- (void)drawRect:(NSRect)dirtyRect {
+ auto cg_context = [[NSGraphicsContext currentContext] CGContext];
+ auto layer = _p->GetDrawLayer();
+ Ensures(layer);
+ CGContextDrawLayerAtPoint(cg_context, CGPointMake(0, 0), layer);
+}
+
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+- (BOOL)canBecomeKeyView {
+ return YES;
+}
+
+- (void)mouseMoved:(NSEvent*)event {
+ _p->OnMouseMove(cru::platform::Point(event.locationInWindow.x, event.locationInWindow.y));
+}
+
+- (void)mouseDragged:(NSEvent*)event {
+ _p->OnMouseMove(cru::platform::Point(event.locationInWindow.x, event.locationInWindow.y));
+}
+
+- (void)rightMouseDragged:(NSEvent*)event {
+ _p->OnMouseMove(cru::platform::Point(event.locationInWindow.x, event.locationInWindow.y));
+}
+
+- (void)mouseEntered:(NSEvent*)event {
+ _p->OnMouseEnterLeave(cru::platform::gui::MouseEnterLeaveType::Enter);
+}
+
+- (void)mouseExited:(NSEvent*)event {
+ _p->OnMouseEnterLeave(cru::platform::gui::MouseEnterLeaveType::Leave);
+}
+
+- (void)mouseDown:(NSEvent*)event {
+ [[self window] makeKeyWindow];
+
+ auto key_modifier = GetKeyModifier(event);
+ cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y);
+
+ _p->OnMouseDown(cru::platform::gui::mouse_buttons::left, p, key_modifier);
+}
+
+- (void)mouseUp:(NSEvent*)event {
+ auto key_modifier = GetKeyModifier(event);
+ cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y);
+
+ _p->OnMouseUp(cru::platform::gui::mouse_buttons::left, p, key_modifier);
+}
+
+- (void)rightMouseDown:(NSEvent*)event {
+ auto key_modifier = GetKeyModifier(event);
+ cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y);
+
+ _p->OnMouseDown(cru::platform::gui::mouse_buttons::right, p, key_modifier);
+}
+
+- (void)rightMouseUp:(NSEvent*)event {
+ auto key_modifier = GetKeyModifier(event);
+ cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y);
+
+ _p->OnMouseUp(cru::platform::gui::mouse_buttons::right, p, key_modifier);
+}
+
+- (void)scrollWheel:(NSEvent*)event {
+ auto key_modifier = GetKeyModifier(event);
+ cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y);
+
+ if (event.scrollingDeltaY) {
+ _p->OnMouseWheel(static_cast<float>(event.scrollingDeltaY), p, key_modifier, false);
+ }
+
+ if (event.scrollingDeltaX) {
+ _p->OnMouseWheel(static_cast<float>(event.scrollingDeltaX), p, key_modifier, true);
+ }
+}
+
+namespace {
+using cru::platform::gui::KeyCode;
+const std::unordered_set<KeyCode> input_context_handle_codes{
+ KeyCode::A,
+ KeyCode::B,
+ KeyCode::C,
+ KeyCode::D,
+ KeyCode::E,
+ KeyCode::F,
+ KeyCode::G,
+ KeyCode::H,
+ KeyCode::I,
+ KeyCode::J,
+ KeyCode::K,
+ KeyCode::L,
+ KeyCode::M,
+ KeyCode::N,
+ KeyCode::O,
+ KeyCode::P,
+ KeyCode::Q,
+ KeyCode::R,
+ KeyCode::S,
+ KeyCode::T,
+ KeyCode::U,
+ KeyCode::V,
+ KeyCode::W,
+ KeyCode::X,
+ KeyCode::Y,
+ KeyCode::Z,
+ KeyCode::N0,
+ KeyCode::N1,
+ KeyCode::N2,
+ KeyCode::N3,
+ KeyCode::N4,
+ KeyCode::N5,
+ KeyCode::N6,
+ KeyCode::N7,
+ KeyCode::N8,
+ KeyCode::N9,
+ KeyCode::Comma,
+ KeyCode::Period,
+ KeyCode::Slash,
+ KeyCode::Semicolon,
+ KeyCode::Quote,
+ KeyCode::LeftSquareBracket,
+ KeyCode::RightSquareBracket,
+ KeyCode::BackSlash,
+ KeyCode::Minus,
+ KeyCode::Equal,
+ KeyCode::GraveAccent,
+};
+}
+
+const std::unordered_set<KeyCode> input_context_handle_codes_when_has_text{
+ KeyCode::Backspace, KeyCode::Space, KeyCode::Return, KeyCode::Left,
+ KeyCode::Right, KeyCode::Up, KeyCode::Down};
+
+- (void)keyDown:(NSEvent*)event {
+ auto key_modifier = GetKeyModifier(event);
+
+ bool handled = false;
+
+ auto input_context = dynamic_cast<cru::platform::gui::osx::OsxInputMethodContext*>(
+ _p->GetWindow()->GetInputMethodContext());
+ Ensures(input_context);
+
+ auto c = cru::platform::gui::osx::KeyCodeFromOsxToCru(event.keyCode);
+
+ if (input_context->IsEnabled()) {
+ if (input_context_handle_codes.count(c) &&
+ !(key_modifier & ~cru::platform::gui::KeyModifiers::shift)) {
+ handled = [[self inputContext] handleEvent:event];
+ } else if (input_context_handle_codes_when_has_text.count(c) && !key_modifier) {
+ if (!input_context->GetCompositionText().text.empty()) {
+ handled = [[self inputContext] handleEvent:event];
+ } else {
+ if (c == KeyCode::Return) {
+ _input_context_p->RaiseTextEvent(u"\n");
+ handled = true;
+ } else if (c == KeyCode::Space) {
+ _input_context_p->RaiseTextEvent(u" ");
+ handled = true;
+ }
+ }
+ }
+ }
+
+ if (!handled) {
+ _p->OnKeyDown(c, key_modifier);
+ }
+}
+
+- (void)keyUp:(NSEvent*)event {
+ // cru::CRU_LOG_DEBUG(u"CruView", u"Recieved key up.");
+
+ auto key_modifier = GetKeyModifier(event);
+ auto c = cru::platform::gui::osx::KeyCodeFromOsxToCru(event.keyCode);
+
+ _p->OnKeyUp(c, key_modifier);
+}
+
+- (BOOL)hasMarkedText {
+ return _input_context_text != nil;
+}
+
+- (NSRange)markedRange {
+ return _input_context_text == nil ? NSRange{NSNotFound, 0}
+ : NSRange{0, [_input_context_text length]};
+}
+
+- (NSRange)selectedRange {
+ return NSMakeRange(_input_context_p->GetSelectionRange().position,
+ _input_context_p->GetSelectionRange().count);
+}
+
+- (void)setMarkedText:(id)string
+ selectedRange:(NSRange)selectedRange
+ replacementRange:(NSRange)replacementRange {
+ CFStringRef s;
+ if ([string isKindOfClass:[NSString class]]) {
+ s = (CFStringRef)string;
+ } else {
+ auto as = (CFAttributedStringRef)string;
+ s = CFAttributedStringGetString(as);
+ }
+
+ auto ss = Convert(s);
+
+ // cru::CRU_LOG_DEBUG(u"CruView",
+ // u"Received setMarkedText string: {}, selected range: ({}, {}), "
+ // u"replacement range: ({}, {}).",
+ // ss, selectedRange.location, selectedRange.length, replacementRange.location,
+ // replacementRange.length);
+
+ if (_input_context_text == nil) {
+ _input_context_text = [[NSMutableAttributedString alloc] init];
+ _input_context_p->RaiseCompositionStartEvent();
+ }
+
+ if (replacementRange.location == NSNotFound) replacementRange.location = 0;
+
+ [_input_context_text
+ replaceCharactersInRange:NSMakeRange(0, [_input_context_text length])
+ withAttributedString:[[NSAttributedString alloc] initWithString:(NSString*)s]];
+
+ cru::platform::gui::CompositionText composition_text;
+ composition_text.text = Convert((CFStringRef)[_input_context_text string]);
+ composition_text.selection.position = ss.IndexFromCodePointToCodeUnit(selectedRange.location);
+ composition_text.selection.count =
+ ss.IndexFromCodePointToCodeUnit(selectedRange.location + selectedRange.length) -
+ composition_text.selection.position;
+ _input_context_p->SetCompositionText(composition_text);
+ _input_context_p->RaiseCompositionEvent();
+}
+
+- (void)unmarkText {
+ _input_context_text = nil;
+ _input_context_p->RaiseCompositionEndEvent();
+}
+
+- (NSArray<NSAttributedStringKey>*)validAttributesForMarkedText {
+ return @[
+ (NSString*)kCTUnderlineColorAttributeName, (NSString*)kCTUnderlineStyleAttributeName,
+ (NSString*)kCTForegroundColorAttributeName, (NSString*)kCTBackgroundColorAttributeName
+ ];
+}
+
+- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
+ actualRange:(NSRangePointer)actualRange {
+ cru::Range r(range.location, range.length);
+
+ r = r.CoerceInto(0, [_input_context_text length]);
+
+ return [_input_context_text attributedSubstringFromRange:NSMakeRange(r.position, r.count)];
+}
+
+- (void)insertText:(id)string replacementRange:(NSRange)replacementRange {
+ CFStringRef s;
+ if ([string isKindOfClass:[NSString class]]) {
+ s = (CFStringRef)string;
+ } else {
+ auto as = (CFAttributedStringRef)string;
+ s = CFAttributedStringGetString(as);
+ }
+
+ _input_context_text = nil;
+ _input_context_p->SetCompositionText(cru::platform::gui::CompositionText());
+ cru::String ss = Convert(s);
+
+ // cru::CRU_LOG_DEBUG(u"CruView", u"Finish composition: {}, replacement range: ({}, {})", ss,
+ // replacementRange.location, replacementRange.length);
+
+ _input_context_p->RaiseCompositionEvent();
+ _input_context_p->RaiseCompositionEndEvent();
+ _input_context_p->RaiseTextEvent(ss);
+}
+
+- (NSUInteger)characterIndexForPoint:(NSPoint)point {
+ return NSNotFound;
+}
+
+- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
+ NSRect result;
+ result.origin.x = _input_context_p->GetCandidateWindowPosition().x;
+ result.origin.y = _input_context_p->GetCandidateWindowPosition().y;
+ result.size.height = 16;
+ result.size.width = 0;
+ return result;
+}
+
+- (void)doCommandBySelector:(SEL)selector {
+ _input_context_p->PerformSel(selector);
+}
+@end
+
+@implementation CruWindowDelegate {
+ cru::platform::gui::osx::details::OsxWindowPrivate* _p;
+}
+
+- (id)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p {
+ _p = p;
+ return self;
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ _p->OnWindowWillClose();
+}
+
+- (void)windowDidExpose:(NSNotification*)notification {
+ _p->OnWindowDidExpose();
+}
+
+- (void)windowDidUpdate:(NSNotification*)notification {
+ _p->OnWindowDidUpdate();
+}
+
+- (void)windowDidMove:(NSNotification*)notification {
+ _p->OnWindowDidMove();
+}
+
+- (void)windowDidResize:(NSNotification*)notification {
+ _p->OnWindowDidResize();
+}
+
+- (void)windowDidBecomeKey:(NSNotification*)notification {
+ _p->OnBecomeKeyWindow();
+}
+
+- (void)windowDidResignKey:(NSNotification*)notification {
+ _p->OnResignKeyWindow();
+}
+@end
diff --git a/src/platform/gui/osx/WindowPrivate.h b/src/platform/gui/osx/WindowPrivate.h
new file mode 100644
index 00000000..49cc0154
--- /dev/null
+++ b/src/platform/gui/osx/WindowPrivate.h
@@ -0,0 +1,118 @@
+#pragma once
+#include "cru/platform/gui/osx/Window.h"
+
+#include "cru/common/Event.h"
+#include "cru/platform/gui/osx/Cursor.h"
+#include "cru/platform/gui/TimerHelper.h"
+#include "cru/platform/gui/Window.h"
+
+#import <AppKit/AppKit.h>
+
+@interface CruWindowDelegate : NSObject <NSWindowDelegate>
+- (id)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p;
+@end
+
+@interface CruWindow : NSWindow
+- (instancetype)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p
+ contentRect:(NSRect)contentRect
+ style:(NSWindowStyleMask)style;
+@end
+
+@interface CruView : NSView <NSTextInputClient>
+- (instancetype)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p
+ input_context_p:
+ (cru::platform::gui::osx::details::OsxInputMethodContextPrivate*)input_context_p
+ frame:(cru::platform::Rect)frame;
+@end
+
+namespace cru::platform::gui::osx {
+
+namespace details {
+class OsxInputMethodContextPrivate;
+
+class OsxWindowPrivate {
+ friend OsxWindow;
+ friend OsxInputMethodContextPrivate;
+
+ public:
+ explicit OsxWindowPrivate(OsxWindow* osx_window);
+
+ CRU_DELETE_COPY(OsxWindowPrivate)
+ CRU_DELETE_MOVE(OsxWindowPrivate)
+
+ ~OsxWindowPrivate();
+
+ public:
+ void OnMouseEnterLeave(MouseEnterLeaveType type);
+ void OnMouseMove(Point p);
+ void OnMouseDown(MouseButton button, Point p, KeyModifier key_modifier);
+ void OnMouseUp(MouseButton button, Point p, KeyModifier key_modifier);
+ void OnMouseWheel(float delta, Point p, KeyModifier key_modifier, bool horizontal);
+ void OnKeyDown(KeyCode key, KeyModifier key_modifier);
+ void OnKeyUp(KeyCode key, KeyModifier key_modifier);
+
+ void OnWindowWillClose();
+ void OnWindowDidExpose();
+ void OnWindowDidUpdate();
+ void OnWindowDidMove();
+ void OnWindowDidResize();
+ void OnBecomeKeyWindow();
+ void OnResignKeyWindow();
+
+ CGLayerRef GetDrawLayer() { return draw_layer_; }
+
+ OsxWindow* GetWindow() { return osx_window_; }
+ NSWindow* GetNSWindow() { return window_; }
+
+ private:
+ Size GetScreenSize();
+
+ void CreateWindow();
+
+ void UpdateCursor();
+
+ Point TransformMousePoint(const Point& point);
+
+ CGLayerRef CreateLayer(const CGSize& size);
+
+ Rect RetrieveContentRect();
+
+ private:
+ OsxWindow* osx_window_;
+
+ INativeWindow* parent_ = nullptr;
+ WindowStyleFlag style_flag_ = WindowStyleFlag{};
+
+ String title_;
+
+ Rect content_rect_;
+
+ NSWindow* window_ = nil;
+ CruWindowDelegate* window_delegate_ = nil;
+
+ CGLayerRef draw_layer_ = nullptr;
+
+ bool mouse_in_ = false;
+
+ std::shared_ptr<OsxCursor> cursor_ = nullptr;
+
+ std::unique_ptr<OsxInputMethodContext> input_method_context_;
+
+ TimerAutoCanceler draw_timer_;
+
+ Event<std::nullptr_t> create_event_;
+ Event<std::nullptr_t> destroy_event_;
+ Event<std::nullptr_t> paint_event_;
+ Event<WindowVisibilityType> visibility_change_event_;
+ Event<Size> resize_event_;
+ Event<FocusChangeType> focus_event_;
+ Event<MouseEnterLeaveType> mouse_enter_leave_event_;
+ Event<Point> mouse_move_event_;
+ Event<NativeMouseButtonEventArgs> mouse_down_event_;
+ Event<NativeMouseButtonEventArgs> mouse_up_event_;
+ Event<NativeMouseWheelEventArgs> mouse_wheel_event_;
+ Event<NativeKeyEventArgs> key_down_event_;
+ Event<NativeKeyEventArgs> key_up_event_;
+};
+} // namespace details
+} // namespace cru::platform::gui::osx