aboutsummaryrefslogtreecommitdiff
path: root/src/platform
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
parent9e0c9d3499bc50c3534b4dc500d8b5d0b5f22752 (diff)
downloadcru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.tar.gz
cru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.tar.bz2
cru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.zip
...
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/CMakeLists.txt4
-rw-r--r--src/platform/bootstrap/Bootstrap.cpp6
-rw-r--r--src/platform/graphics/quartz/Brush.cpp36
-rw-r--r--src/platform/graphics/quartz/CMakeLists.txt19
-rw-r--r--src/platform/graphics/quartz/Convert.cpp63
-rw-r--r--src/platform/graphics/quartz/Factory.cpp43
-rw-r--r--src/platform/graphics/quartz/Font.cpp30
-rw-r--r--src/platform/graphics/quartz/Geometry.cpp79
-rw-r--r--src/platform/graphics/quartz/Image.cpp58
-rw-r--r--src/platform/graphics/quartz/ImageFactory.cpp109
-rw-r--r--src/platform/graphics/quartz/Painter.cpp230
-rw-r--r--src/platform/graphics/quartz/Resource.cpp1
-rw-r--r--src/platform/graphics/quartz/TextLayout.cpp456
-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
-rw-r--r--src/platform/osx/CMakeLists.txt8
-rw-r--r--src/platform/osx/Resource.cpp1
30 files changed, 3219 insertions, 3 deletions
diff --git a/src/platform/CMakeLists.txt b/src/platform/CMakeLists.txt
index a63aab44..bb9259ae 100644
--- a/src/platform/CMakeLists.txt
+++ b/src/platform/CMakeLists.txt
@@ -13,6 +13,10 @@ if (WIN32)
add_subdirectory(win)
add_subdirectory(graphics/direct2d)
add_subdirectory(gui/win)
+elseif (APPLE)
+ add_subdirectory(osx)
+ add_subdirectory(graphics/quartz)
+ add_subdirectory(gui/osx)
endif()
add_subdirectory(bootstrap)
diff --git a/src/platform/bootstrap/Bootstrap.cpp b/src/platform/bootstrap/Bootstrap.cpp
index 5dcd0c77..30099b96 100644
--- a/src/platform/bootstrap/Bootstrap.cpp
+++ b/src/platform/bootstrap/Bootstrap.cpp
@@ -4,8 +4,8 @@
#include "cru/platform/graphics/direct2d/Factory.h"
#include "cru/platform/gui/win/UiApplication.h"
#else
-#include "cru/osx/graphics/quartz/Factory.h"
-#include "cru/osx/gui/UiApplication.h"
+#include "cru/platform/graphics/quartz/Factory.h"
+#include "cru/platform/gui/osx/UiApplication.h"
#endif
namespace cru::platform::bootstrap {
@@ -24,7 +24,7 @@ CreateGraphicsFactory() {
#ifdef CRU_PLATFORM_WINDOWS
return new cru::platform::graphics::direct2d::DirectGraphicsFactory();
#elif CRU_PLATFORM_OSX
- return new cru::platform::graphics::osx::quartz::QuartzGraphicsFactory();
+ return new cru::platform::graphics::quartz::QuartzGraphicsFactory();
#else
return nullptr;
#endif
diff --git a/src/platform/graphics/quartz/Brush.cpp b/src/platform/graphics/quartz/Brush.cpp
new file mode 100644
index 00000000..2aa31bd8
--- /dev/null
+++ b/src/platform/graphics/quartz/Brush.cpp
@@ -0,0 +1,36 @@
+#include "cru/platform/graphics/quartz/Brush.h"
+#include "cru/common/String.h"
+#include "cru/common/Format.h"
+
+namespace cru::platform::graphics::quartz {
+QuartzSolidColorBrush::QuartzSolidColorBrush(IGraphicsFactory* graphics_factory,
+ const Color& color)
+ : QuartzBrush(graphics_factory), color_(color) {
+ cg_color_ =
+ CGColorCreateGenericRGB(color.GetFloatRed(), color.GetFloatGreen(),
+ color.GetFloatBlue(), color.GetFloatAlpha());
+ Ensures(cg_color_);
+}
+
+QuartzSolidColorBrush::~QuartzSolidColorBrush() { CGColorRelease(cg_color_); }
+
+void QuartzSolidColorBrush::SetColor(const Color& color) {
+ color_ = color;
+ CGColorRelease(cg_color_);
+ cg_color_ =
+ CGColorCreateGenericRGB(color.GetFloatRed(), color.GetFloatGreen(),
+ color.GetFloatBlue(), color.GetFloatAlpha());
+ Ensures(cg_color_);
+}
+
+void QuartzSolidColorBrush::Select(CGContextRef context) {
+ Expects(context);
+ Expects(cg_color_);
+ CGContextSetStrokeColorWithColor(context, cg_color_);
+ CGContextSetFillColorWithColor(context, cg_color_);
+}
+
+String QuartzSolidColorBrush::GetDebugString() {
+ return Format(u"QuartzSolidColorBrush(Color: {})", color_);
+}
+} // namespace cru::platform::graphics::quartz
diff --git a/src/platform/graphics/quartz/CMakeLists.txt b/src/platform/graphics/quartz/CMakeLists.txt
new file mode 100644
index 00000000..1fcaff26
--- /dev/null
+++ b/src/platform/graphics/quartz/CMakeLists.txt
@@ -0,0 +1,19 @@
+add_library(CruPlatformGraphicsQuartz SHARED
+ Brush.cpp
+ Convert.cpp
+ Factory.cpp
+ Font.cpp
+ Geometry.cpp
+ Image.cpp
+ ImageFactory.cpp
+ Painter.cpp
+ Resource.cpp
+ TextLayout.cpp
+)
+
+find_library(CORE_GRAPHICS CoreGraphics REQUIRED)
+find_library(CORE_TEXT CoreText REQUIRED)
+find_library(IMAGE_IO ImageIO REQUIRED)
+
+target_link_libraries(CruPlatformGraphicsQuartz PUBLIC ${CORE_GRAPHICS} ${CORE_TEXT} ${IMAGE_IO})
+target_link_libraries(CruPlatformGraphicsQuartz PUBLIC CruPlatformBaseOsx CruPlatformGraphics)
diff --git a/src/platform/graphics/quartz/Convert.cpp b/src/platform/graphics/quartz/Convert.cpp
new file mode 100644
index 00000000..06720982
--- /dev/null
+++ b/src/platform/graphics/quartz/Convert.cpp
@@ -0,0 +1,63 @@
+#include "cru/platform/graphics/quartz/Convert.h"
+#include <cstdint>
+
+namespace cru::platform::graphics::quartz {
+
+CGPoint Convert(const Point& point) { return CGPoint{point.x, point.y}; }
+Point Convert(const CGPoint& point) { return Point(point.x, point.y); }
+
+CGSize Convert(const Size& size) { return CGSize{size.width, size.height}; }
+Size Convert(const CGSize& size) { return Size(size.width, size.height); }
+
+CGAffineTransform Convert(const Matrix& matrix) {
+ return CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22,
+ matrix.m31, matrix.m32);
+}
+
+Matrix Convert(const CGAffineTransform& matrix) {
+ return Matrix(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty);
+}
+
+CGRect Convert(const Rect& rect) {
+ return CGRect{CGPoint{rect.left, rect.top}, CGSize{rect.width, rect.height}};
+}
+
+Rect Convert(const CGRect& rect) {
+ return Rect{static_cast<float>(rect.origin.x),
+ static_cast<float>(rect.origin.y),
+ static_cast<float>(rect.size.width),
+ static_cast<float>(rect.size.height)};
+}
+
+const CGDataProviderSequentialCallbacks kStreamToCGDataProviderCallbacks{
+ 1,
+ [](void* stream, void* buffer, size_t size) -> size_t {
+ return static_cast<io::Stream*>(stream)->Read(
+ static_cast<std::byte*>(buffer), size);
+ },
+ [](void* stream, off_t offset) -> off_t {
+ auto s = static_cast<io::Stream*>(stream);
+ auto current_position = s->Tell();
+ s->Seek(offset, io::Stream::SeekOrigin::Current);
+ return s->Tell() - current_position;
+ },
+ [](void* stream) { static_cast<io::Stream*>(stream)->Rewind(); },
+ [](void* stream) {}};
+
+CGDataProviderRef ConvertStreamToCGDataProvider(io::Stream* stream) {
+ return CGDataProviderCreateSequential(stream,
+ &kStreamToCGDataProviderCallbacks);
+}
+
+const CGDataConsumerCallbacks kStreamToCGDataConsumerCallbacks{
+ [](void* info, const void* buffer, size_t count) -> size_t {
+ return static_cast<io::Stream*>(info)->Write(
+ static_cast<const std::byte*>(buffer), count);
+ },
+ [](void* info) {}};
+
+CGDataConsumerRef ConvertStreamToCGDataConsumer(io::Stream* stream) {
+ return CGDataConsumerCreate(stream, &kStreamToCGDataConsumerCallbacks);
+}
+
+} // namespace cru::platform::graphics::quartz
diff --git a/src/platform/graphics/quartz/Factory.cpp b/src/platform/graphics/quartz/Factory.cpp
new file mode 100644
index 00000000..862c0966
--- /dev/null
+++ b/src/platform/graphics/quartz/Factory.cpp
@@ -0,0 +1,43 @@
+#include "cru/platform/graphics/quartz/Factory.h"
+
+#include "cru/platform/graphics/quartz/Brush.h"
+#include "cru/platform/graphics/quartz/Font.h"
+#include "cru/platform/graphics/quartz/Geometry.h"
+#include "cru/platform/graphics/quartz/ImageFactory.h"
+#include "cru/platform/graphics/quartz/TextLayout.h"
+#include "cru/platform/Check.h"
+#include "cru/platform/graphics/ImageFactory.h"
+
+#include <memory>
+
+namespace cru::platform::graphics::quartz {
+QuartzGraphicsFactory::QuartzGraphicsFactory()
+ : OsxQuartzResource(this), image_factory_(new QuartzImageFactory(this)) {}
+
+QuartzGraphicsFactory::~QuartzGraphicsFactory() {}
+
+std::unique_ptr<ISolidColorBrush>
+QuartzGraphicsFactory::CreateSolidColorBrush() {
+ return std::make_unique<QuartzSolidColorBrush>(this, colors::black);
+}
+
+std::unique_ptr<IGeometryBuilder>
+QuartzGraphicsFactory::CreateGeometryBuilder() {
+ return std::make_unique<QuartzGeometryBuilder>(this);
+}
+
+std::unique_ptr<IFont> QuartzGraphicsFactory::CreateFont(String font_family,
+ float font_size) {
+ return std::make_unique<OsxCTFont>(this, font_family, font_size);
+}
+
+std::unique_ptr<ITextLayout> QuartzGraphicsFactory::CreateTextLayout(
+ std::shared_ptr<IFont> font, String text) {
+ auto f = CheckPlatform<OsxCTFont>(font, GetPlatformId());
+ return std::make_unique<OsxCTTextLayout>(this, f, text);
+}
+
+IImageFactory* QuartzGraphicsFactory::GetImageFactory() {
+ return image_factory_.get();
+}
+} // namespace cru::platform::graphics::quartz
diff --git a/src/platform/graphics/quartz/Font.cpp b/src/platform/graphics/quartz/Font.cpp
new file mode 100644
index 00000000..62052b0a
--- /dev/null
+++ b/src/platform/graphics/quartz/Font.cpp
@@ -0,0 +1,30 @@
+#include "cru/platform/graphics/quartz/Font.h"
+
+#include "cru/platform/osx/Convert.h"
+#include "cru/platform/graphics/quartz/Convert.h"
+#include "cru/platform/graphics/quartz/Resource.h"
+
+namespace cru::platform::graphics::quartz {
+using cru::platform::osx::Convert;
+
+OsxCTFont::OsxCTFont(IGraphicsFactory* graphics_factory, const String& name,
+ float size)
+ : OsxQuartzResource(graphics_factory), name_(name) {
+ CFStringRef n = Convert(name);
+
+ if (name.empty()) {
+ ct_font_ =
+ CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, size, nullptr);
+ } else {
+ ct_font_ = CTFontCreateWithName(n, size, nullptr);
+ }
+ Ensures(ct_font_);
+
+ CFRelease(n);
+}
+
+OsxCTFont::~OsxCTFont() { CFRelease(ct_font_); }
+
+String OsxCTFont::GetFontName() { return name_; }
+float OsxCTFont::GetFontSize() { return CTFontGetSize(ct_font_); }
+} // namespace cru::platform::graphics::quartz
diff --git a/src/platform/graphics/quartz/Geometry.cpp b/src/platform/graphics/quartz/Geometry.cpp
new file mode 100644
index 00000000..e6558fbb
--- /dev/null
+++ b/src/platform/graphics/quartz/Geometry.cpp
@@ -0,0 +1,79 @@
+#include "cru/platform/graphics/quartz/Geometry.h"
+#include "cru/platform/graphics/quartz/Convert.h"
+
+#include <memory>
+
+namespace cru::platform::graphics::quartz {
+QuartzGeometry::QuartzGeometry(IGraphicsFactory *graphics_factory,
+ CGPathRef cg_path)
+ : OsxQuartzResource(graphics_factory), cg_path_(cg_path) {}
+
+QuartzGeometry::~QuartzGeometry() { CGPathRelease(cg_path_); }
+
+bool QuartzGeometry::FillContains(const Point &point) {
+ return CGPathContainsPoint(cg_path_, nullptr, CGPoint{point.x, point.y},
+ kCGPathFill);
+}
+
+Rect QuartzGeometry::GetBounds() {
+ auto bounds = CGPathGetPathBoundingBox(cg_path_);
+ if (CGRectIsNull(bounds)) return {};
+ return Convert(bounds);
+}
+
+std::unique_ptr<IGeometry> QuartzGeometry::Transform(const Matrix &matrix) {
+ auto cg_matrix = Convert(matrix);
+ auto cg_path = CGPathCreateCopyByTransformingPath(cg_path_, &cg_matrix);
+ return std::make_unique<QuartzGeometry>(GetGraphicsFactory(), cg_path);
+}
+
+std::unique_ptr<IGeometry> QuartzGeometry::CreateStrokeGeometry(float width) {
+ auto cg_path = CGPathCreateCopyByStrokingPath(
+ cg_path_, nullptr, width, kCGLineCapButt, kCGLineJoinMiter, 10);
+ return std::make_unique<QuartzGeometry>(GetGraphicsFactory(), cg_path);
+}
+
+QuartzGeometryBuilder::QuartzGeometryBuilder(IGraphicsFactory *graphics_factory)
+ : OsxQuartzResource(graphics_factory) {
+ cg_mutable_path_ = CGPathCreateMutable();
+}
+
+QuartzGeometryBuilder::~QuartzGeometryBuilder() {
+ CGPathRelease(cg_mutable_path_);
+}
+
+Point QuartzGeometryBuilder::GetCurrentPosition() {
+ return Convert(CGPathGetCurrentPoint(cg_mutable_path_));
+}
+
+void QuartzGeometryBuilder::MoveTo(const Point &point) {
+ CGPathMoveToPoint(cg_mutable_path_, nullptr, point.x, point.y);
+}
+
+void QuartzGeometryBuilder::LineTo(const Point &point) {
+ CGPathAddLineToPoint(cg_mutable_path_, nullptr, point.x, point.y);
+}
+
+void QuartzGeometryBuilder::CubicBezierTo(const Point &start_control_point,
+ const Point &end_control_point,
+ const Point &end_point) {
+ CGPathAddCurveToPoint(cg_mutable_path_, nullptr, start_control_point.x,
+ start_control_point.y, end_control_point.x,
+ end_control_point.y, end_point.x, end_point.y);
+}
+
+void QuartzGeometryBuilder::QuadraticBezierTo(const Point &control_point,
+ const Point &end_point) {
+ CGPathAddQuadCurveToPoint(cg_mutable_path_, nullptr, control_point.x,
+ control_point.y, end_point.x, end_point.y);
+}
+
+void QuartzGeometryBuilder::CloseFigure(bool close) {
+ if (close) CGPathCloseSubpath(cg_mutable_path_);
+}
+
+std::unique_ptr<IGeometry> QuartzGeometryBuilder::Build() {
+ return std::make_unique<QuartzGeometry>(GetGraphicsFactory(),
+ CGPathCreateCopy(cg_mutable_path_));
+}
+} // namespace cru::platform::graphics::quartz
diff --git a/src/platform/graphics/quartz/Image.cpp b/src/platform/graphics/quartz/Image.cpp
new file mode 100644
index 00000000..3fa40937
--- /dev/null
+++ b/src/platform/graphics/quartz/Image.cpp
@@ -0,0 +1,58 @@
+#include "cru/platform/graphics/quartz/Image.h"
+#include "cru/common/Exception.h"
+#include "cru/platform/graphics/quartz/Convert.h"
+#include "cru/platform/graphics/quartz/Painter.h"
+
+namespace cru::platform::graphics::quartz {
+QuartzImage::QuartzImage(IGraphicsFactory* graphics_factory,
+ IImageFactory* image_factory, CGImageRef image,
+ bool auto_release, unsigned char* buffer)
+ : OsxQuartzResource(graphics_factory),
+ image_factory_(image_factory),
+ image_(image),
+ auto_release_(auto_release),
+ buffer_(buffer) {
+ Expects(image);
+}
+
+QuartzImage::~QuartzImage() {
+ if (auto_release_) {
+ CGImageRelease(image_);
+ }
+}
+
+float QuartzImage::GetWidth() { return CGImageGetWidth(image_); }
+
+float QuartzImage::GetHeight() { return CGImageGetHeight(image_); }
+
+std::unique_ptr<IImage> QuartzImage::CreateWithRect(const Rect& rect) {
+ auto new_cg_image = CGImageCreateWithImageInRect(image_, Convert(rect));
+
+ return std::make_unique<QuartzImage>(GetGraphicsFactory(), image_factory_,
+ new_cg_image, true);
+}
+
+std::unique_ptr<IPainter> QuartzImage::CreatePainter() {
+ if (!buffer_)
+ throw Exception(
+ u"Failed to create painter for image because failed to get its "
+ u"buffer.");
+
+ auto width = CGImageGetWidth(image_);
+ auto height = CGImageGetHeight(image_);
+ auto bits_per_component = CGImageGetBitsPerComponent(image_);
+ auto bytes_per_row = CGImageGetBytesPerRow(image_);
+ auto color_space = CGImageGetColorSpace(image_);
+ auto bitmap_info = CGImageGetBitmapInfo(image_);
+
+ auto cg_context =
+ CGBitmapContextCreate(buffer_, width, height, bits_per_component,
+ bytes_per_row, color_space, bitmap_info);
+
+ return std::make_unique<QuartzCGContextPainter>(
+ GetGraphicsFactory(), cg_context, true, Size(width, height),
+ [](QuartzCGContextPainter* painter) {
+
+ });
+}
+} // namespace cru::platform::graphics::quartz
diff --git a/src/platform/graphics/quartz/ImageFactory.cpp b/src/platform/graphics/quartz/ImageFactory.cpp
new file mode 100644
index 00000000..a48b4b86
--- /dev/null
+++ b/src/platform/graphics/quartz/ImageFactory.cpp
@@ -0,0 +1,109 @@
+#include "cru/platform/graphics/quartz/ImageFactory.h"
+#include "cru/common/Exception.h"
+#include "cru/common/platform/osx/Convert.h"
+#include "cru/platform/graphics/quartz/Convert.h"
+#include "cru/platform/graphics/quartz/Image.h"
+#include "cru/platform/Check.h"
+#include "cru/platform/graphics/Image.h"
+
+#include <ImageIO/ImageIO.h>
+
+namespace cru::platform::graphics::quartz {
+using cru::platform::osx::Convert;
+
+QuartzImageFactory::QuartzImageFactory(IGraphicsFactory* graphics_factory)
+ : OsxQuartzResource(graphics_factory) {}
+
+QuartzImageFactory::~QuartzImageFactory() {}
+
+std::unique_ptr<IImage> QuartzImageFactory::DecodeFromStream(
+ io::Stream* stream) {
+ CGDataProviderRef data_provider = ConvertStreamToCGDataProvider(stream);
+ CGImageSourceRef image_source =
+ CGImageSourceCreateWithDataProvider(data_provider, nullptr);
+
+ CGImageRef cg_image =
+ CGImageSourceCreateImageAtIndex(image_source, 0, nullptr);
+
+ CFRelease(image_source);
+ CGDataProviderRelease(data_provider);
+
+ return std::unique_ptr<IImage>(
+ new QuartzImage(GetGraphicsFactory(), this, cg_image, true));
+}
+
+static String GetImageFormatUniformTypeIdentifier(ImageFormat format) {
+ switch (format) {
+ case ImageFormat::Png:
+ return u"public.png";
+ case ImageFormat::Jpeg:
+ return u"public.jpeg";
+ case ImageFormat::Gif:
+ return u"com.compuserve.gif";
+ default:
+ throw Exception(u"Unknown image format.");
+ }
+}
+
+void QuartzImageFactory::EncodeToStream(IImage* image, io::Stream* stream,
+ ImageFormat format, float quality) {
+ if (quality <= 0 || quality > 1) {
+ throw Exception(u"Invalid quality value.");
+ }
+
+ auto quartz_image = CheckPlatform<QuartzImage>(image, GetPlatformId());
+ auto cg_image = quartz_image->GetCGImage();
+
+ CFStringRef uti = Convert(GetImageFormatUniformTypeIdentifier(format));
+ CGDataConsumerRef data_consumer = ConvertStreamToCGDataConsumer(stream);
+ CGImageDestinationRef destination =
+ CGImageDestinationCreateWithDataConsumer(data_consumer, uti, 1, nullptr);
+
+ CFMutableDictionaryRef properties =
+ CFDictionaryCreateMutable(nullptr, 0, nullptr, nullptr);
+ CFNumberRef quality_wrap =
+ CFNumberCreate(nullptr, kCFNumberFloatType, &quality);
+ CFDictionaryAddValue(properties, kCGImageDestinationLossyCompressionQuality,
+ quality_wrap);
+
+ CGImageDestinationAddImage(destination, cg_image, properties);
+
+ if (!CGImageDestinationFinalize(destination)) {
+ throw Exception(u"Failed to finalize image destination.");
+ }
+
+ CFRelease(quality_wrap);
+ CFRelease(properties);
+ CFRelease(destination);
+ CFRelease(data_consumer);
+ CFRelease(uti);
+}
+
+std::unique_ptr<IImage> QuartzImageFactory::CreateBitmap(int width,
+ int height) {
+ if (width <= 0) throw Exception(u"Image width should be greater than 0.");
+ if (height <= 0) throw Exception(u"Image height should be greater than 0.");
+
+ CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB();
+
+ const auto buffer_size = width * height * 4;
+ auto buffer = new unsigned char[buffer_size]{0};
+
+ auto cg_data_provider = CGDataProviderCreateWithData(
+ nullptr, buffer, buffer_size,
+ [](void* info, const void* data, size_t size) {
+ delete[] static_cast<const unsigned char*>(data);
+ });
+
+ auto cg_image =
+ CGImageCreate(width, height, 8, 32, 4 * width, color_space,
+ kCGImageAlphaPremultipliedLast, cg_data_provider, nullptr,
+ true, kCGRenderingIntentDefault);
+
+ CGColorSpaceRelease(color_space);
+ CGDataProviderRelease(cg_data_provider);
+
+ return std::unique_ptr<IImage>(
+ new QuartzImage(GetGraphicsFactory(), this, cg_image, true, buffer));
+}
+} // namespace cru::platform::graphics::quartz
diff --git a/src/platform/graphics/quartz/Painter.cpp b/src/platform/graphics/quartz/Painter.cpp
new file mode 100644
index 00000000..69e187c3
--- /dev/null
+++ b/src/platform/graphics/quartz/Painter.cpp
@@ -0,0 +1,230 @@
+#include "cru/platform/graphics/quartz/Painter.h"
+
+#include "cru/platform/graphics/quartz/Brush.h"
+#include "cru/platform/graphics/quartz/Convert.h"
+#include "cru/platform/graphics/quartz/Geometry.h"
+#include "cru/platform/graphics/quartz/Image.h"
+#include "cru/platform/graphics/quartz/TextLayout.h"
+#include "cru/platform/Check.h"
+#include "cru/platform/Color.h"
+#include "cru/platform/Exception.h"
+
+namespace cru::platform::graphics::quartz {
+QuartzCGContextPainter::QuartzCGContextPainter(
+ IGraphicsFactory* graphics_factory, CGContextRef cg_context,
+ bool auto_release, const Size& size,
+ std::function<void(QuartzCGContextPainter*)> on_end_draw)
+ : OsxQuartzResource(graphics_factory),
+ cg_context_(cg_context),
+ auto_release_(auto_release),
+ size_(size),
+ on_end_draw_(std::move(on_end_draw)) {
+ Expects(cg_context);
+
+ CGContextConcatCTM(cg_context_,
+ CGAffineTransformInvert(CGContextGetCTM(cg_context_)));
+
+ transform_ = Matrix::Scale(1, -1) * Matrix::Translation(0, size.height);
+ CGContextConcatCTM(cg_context_, Convert(transform_));
+}
+
+QuartzCGContextPainter::~QuartzCGContextPainter() {
+ DoEndDraw();
+ if (auto_release_) {
+ CGContextRelease(cg_context_);
+ cg_context_ = nullptr;
+ }
+}
+
+Matrix QuartzCGContextPainter::GetTransform() { return transform_; }
+
+void QuartzCGContextPainter::SetTransform(const Matrix& matrix) {
+ CGContextConcatCTM(cg_context_, Convert(*transform_.Inverted()));
+ CGContextConcatCTM(cg_context_, Convert(matrix));
+ transform_ = matrix;
+}
+
+void QuartzCGContextPainter::ConcatTransform(const Matrix& matrix) {
+ CGContextConcatCTM(cg_context_, Convert(matrix));
+ transform_ = matrix * transform_;
+}
+
+void QuartzCGContextPainter::Clear(const Color& color) {
+ Validate();
+
+ CGContextSetRGBFillColor(cg_context_, color.GetFloatRed(),
+ color.GetFloatGreen(), color.GetFloatBlue(),
+ color.GetFloatAlpha());
+ CGContextFillRect(cg_context_, Convert(Rect{Point{}, size_}));
+}
+
+void QuartzCGContextPainter::DrawLine(const Point& start, const Point& end,
+ IBrush* brush, float width) {
+ Validate();
+
+ CGContextBeginPath(cg_context_);
+ CGContextMoveToPoint(cg_context_, start.x, start.y);
+ CGContextAddLineToPoint(cg_context_, end.x, end.y);
+
+ QuartzBrush* b = CheckPlatform<QuartzBrush>(brush, GetPlatformId());
+ b->Select(cg_context_);
+ SetLineWidth(width);
+
+ CGContextStrokePath(cg_context_);
+}
+
+void QuartzCGContextPainter::StrokeRectangle(const Rect& rectangle,
+ IBrush* brush, float width) {
+ Validate();
+
+ QuartzBrush* b = CheckPlatform<QuartzBrush>(brush, GetPlatformId());
+ b->Select(cg_context_);
+ CGContextStrokeRectWithWidth(cg_context_, Convert(rectangle), width);
+}
+
+void QuartzCGContextPainter::FillRectangle(const Rect& rectangle,
+ IBrush* brush) {
+ Validate();
+
+ QuartzBrush* b = CheckPlatform<QuartzBrush>(brush, GetPlatformId());
+ b->Select(cg_context_);
+ CGContextFillRect(cg_context_, Convert(rectangle));
+}
+
+void QuartzCGContextPainter::StrokeEllipse(const Rect& outline_rect,
+ IBrush* brush, float width) {
+ Validate();
+
+ QuartzBrush* b = CheckPlatform<QuartzBrush>(brush, GetPlatformId());
+ b->Select(cg_context_);
+ SetLineWidth(width);
+
+ CGContextStrokeEllipseInRect(cg_context_, Convert(outline_rect));
+}
+
+void QuartzCGContextPainter::FillEllipse(const Rect& outline_rect,
+ IBrush* brush) {
+ Validate();
+
+ QuartzBrush* b = CheckPlatform<QuartzBrush>(brush, GetPlatformId());
+ b->Select(cg_context_);
+ CGContextFillEllipseInRect(cg_context_, Convert(outline_rect));
+}
+
+void QuartzCGContextPainter::StrokeGeometry(IGeometry* geometry, IBrush* brush,
+ float width) {
+ Validate();
+
+ QuartzGeometry* g = CheckPlatform<QuartzGeometry>(geometry, GetPlatformId());
+ QuartzBrush* b = CheckPlatform<QuartzBrush>(brush, GetPlatformId());
+
+ b->Select(cg_context_);
+ SetLineWidth(width);
+
+ CGContextBeginPath(cg_context_);
+ CGContextAddPath(cg_context_, g->GetCGPath());
+ CGContextStrokePath(cg_context_);
+}
+
+void QuartzCGContextPainter::FillGeometry(IGeometry* geometry, IBrush* brush) {
+ Validate();
+
+ QuartzGeometry* g = CheckPlatform<QuartzGeometry>(geometry, GetPlatformId());
+ QuartzBrush* b = CheckPlatform<QuartzBrush>(brush, GetPlatformId());
+
+ b->Select(cg_context_);
+ CGContextBeginPath(cg_context_);
+ CGContextAddPath(cg_context_, g->GetCGPath());
+ CGContextEOFillPath(cg_context_);
+}
+
+void QuartzCGContextPainter::DrawText(const Point& offset,
+ ITextLayout* text_layout, IBrush* brush) {
+ Validate();
+
+ auto tl = CheckPlatform<OsxCTTextLayout>(text_layout, GetPlatformId());
+
+ Color color;
+
+ if (auto b = dynamic_cast<QuartzSolidColorBrush*>(brush)) {
+ color = b->GetColor();
+ } else {
+ color = colors::black;
+ }
+
+ Matrix transform = tl->GetTransform();
+
+ CGContextSaveGState(cg_context_);
+
+ CGContextConcatCTM(cg_context_, Convert(transform * Matrix::Translation(
+ offset.x, offset.y)));
+
+ auto frame = tl->CreateFrameWithColor(color);
+ Ensures(frame);
+ CTFrameDraw(frame, cg_context_);
+ CFRelease(frame);
+
+ CGContextRestoreGState(cg_context_);
+}
+
+void QuartzCGContextPainter::DrawImage(const Point& offset, IImage* image) {
+ Validate();
+ auto i = CheckPlatform<QuartzImage>(image, GetPlatformId());
+
+ auto cg_image = i->GetCGImage();
+
+ auto width = CGImageGetWidth(cg_image);
+ auto height = CGImageGetHeight(cg_image);
+
+ CGContextDrawImage(cg_context_, CGRectMake(offset.x, offset.y, width, height),
+ cg_image);
+}
+
+void QuartzCGContextPainter::PushLayer(const Rect& bounds) {
+ Validate();
+ clip_stack_.push_back(bounds);
+ CGContextClipToRect(cg_context_, Convert(bounds));
+}
+
+void QuartzCGContextPainter::PopLayer() {
+ Validate();
+ clip_stack_.pop_back();
+ if (clip_stack_.empty()) {
+ CGContextResetClip(cg_context_);
+ } else {
+ CGContextClipToRect(cg_context_, Convert(clip_stack_.back()));
+ }
+}
+
+void QuartzCGContextPainter::PushState() {
+ Validate();
+ CGContextSaveGState(cg_context_);
+}
+
+void QuartzCGContextPainter::PopState() {
+ Validate();
+ CGContextRestoreGState(cg_context_);
+}
+
+void QuartzCGContextPainter::EndDraw() { DoEndDraw(); }
+
+void QuartzCGContextPainter::SetLineWidth(float width) {
+ if (cg_context_) {
+ CGContextSetLineWidth(cg_context_, width);
+ }
+}
+
+void QuartzCGContextPainter::DoEndDraw() {
+ if (cg_context_) {
+ CGContextFlush(cg_context_);
+ CGContextSynchronize(cg_context_);
+
+ on_end_draw_(this);
+ }
+}
+
+void QuartzCGContextPainter::Validate() {
+ if (cg_context_ == nullptr)
+ throw ReuseException(u"QuartzCGContextPainter has already be released.");
+}
+} // namespace cru::platform::graphics::quartz
diff --git a/src/platform/graphics/quartz/Resource.cpp b/src/platform/graphics/quartz/Resource.cpp
new file mode 100644
index 00000000..a5d43747
--- /dev/null
+++ b/src/platform/graphics/quartz/Resource.cpp
@@ -0,0 +1 @@
+#include "cru/platform/graphics/quartz/Resource.h"
diff --git a/src/platform/graphics/quartz/TextLayout.cpp b/src/platform/graphics/quartz/TextLayout.cpp
new file mode 100644
index 00000000..24fd71ef
--- /dev/null
+++ b/src/platform/graphics/quartz/TextLayout.cpp
@@ -0,0 +1,456 @@
+#include "cru/platform/graphics/quartz/TextLayout.h"
+#include "cru/common/Base.h"
+#include "cru/common/Format.h"
+#include "cru/common/StringUtil.h"
+#include "cru/platform/osx/Convert.h"
+#include "cru/platform/graphics/quartz/Convert.h"
+#include "cru/platform/graphics/quartz/Resource.h"
+#include "cru/platform/Check.h"
+#include "cru/platform/graphics/Base.h"
+
+#include <algorithm>
+#include <limits>
+
+namespace cru::platform::graphics::quartz {
+using cru::platform::osx::Convert;
+
+OsxCTTextLayout::OsxCTTextLayout(IGraphicsFactory* graphics_factory,
+ std::shared_ptr<OsxCTFont> font,
+ const String& str)
+ : OsxQuartzResource(graphics_factory),
+ max_width_(std::numeric_limits<float>::max()),
+ max_height_(std::numeric_limits<float>::max()),
+ font_(std::move(font)),
+ text_(str) {
+ Expects(font_);
+
+ DoSetText(std::move(text_));
+
+ RecreateFrame();
+}
+
+OsxCTTextLayout::~OsxCTTextLayout() {
+ ReleaseResource();
+ CFRelease(cf_attributed_text_);
+}
+
+void OsxCTTextLayout::SetFont(std::shared_ptr<IFont> font) {
+ font_ = CheckPlatform<OsxCTFont>(font, GetPlatformId());
+ RecreateFrame();
+}
+
+void OsxCTTextLayout::DoSetText(String text) {
+ text_ = std::move(text);
+
+ if (text_.empty()) {
+ head_empty_line_count_ = 0;
+ tail_empty_line_count_ = 1;
+
+ actual_text_ = {};
+ } else {
+ head_empty_line_count_ = 0;
+ tail_empty_line_count_ = 0;
+
+ for (auto i = text_.cbegin(); i != text_.cend(); ++i) {
+ if (*i == u'\n') {
+ head_empty_line_count_++;
+ } else {
+ break;
+ }
+ }
+
+ for (auto i = text_.crbegin(); i != text_.crend(); ++i) {
+ if (*i == u'\n') {
+ tail_empty_line_count_++;
+ } else {
+ break;
+ }
+ }
+
+ if (text_.size() == tail_empty_line_count_) {
+ head_empty_line_count_ = 1;
+ actual_text_ = {};
+ } else {
+ actual_text_ = String(text_.cbegin() + head_empty_line_count_,
+ text_.cend() - tail_empty_line_count_);
+ }
+ }
+
+ CFStringRef s = Convert(actual_text_);
+ cf_attributed_text_ = CFAttributedStringCreateMutable(nullptr, 0);
+ CFAttributedStringReplaceString(cf_attributed_text_, CFRangeMake(0, 0), s);
+ Ensures(cf_attributed_text_);
+ CFAttributedStringSetAttribute(
+ cf_attributed_text_,
+ CFRangeMake(0, CFAttributedStringGetLength(cf_attributed_text_)),
+ kCTFontAttributeName, font_->GetCTFont());
+ CFRelease(s);
+}
+
+void OsxCTTextLayout::SetText(String new_text) {
+ if (new_text == text_) return;
+
+ CFRelease(cf_attributed_text_);
+ DoSetText(std::move(new_text));
+
+ RecreateFrame();
+}
+
+void OsxCTTextLayout::SetMaxWidth(float max_width) {
+ max_width_ = max_width;
+ RecreateFrame();
+}
+
+void OsxCTTextLayout::SetMaxHeight(float max_height) {
+ max_height_ = max_height;
+ RecreateFrame();
+}
+
+bool OsxCTTextLayout::IsEditMode() { return edit_mode_; }
+
+void OsxCTTextLayout::SetEditMode(bool enable) {
+ edit_mode_ = enable;
+ RecreateFrame();
+}
+
+Index OsxCTTextLayout::GetLineIndexFromCharIndex(Index char_index) {
+ if (char_index < 0 || char_index >= text_.size()) {
+ return -1;
+ }
+
+ auto line_index = 0;
+ for (Index i = 0; i < char_index; ++i) {
+ if (text_[i] == u'\n') {
+ line_index++;
+ }
+ }
+
+ return line_index;
+}
+
+Index OsxCTTextLayout::GetLineCount() { return line_count_; }
+
+float OsxCTTextLayout::GetLineHeight(Index line_index) {
+ if (line_index < 0 || line_index >= line_count_) {
+ return 0.0f;
+ }
+ return line_heights_[line_index];
+}
+
+Rect OsxCTTextLayout::GetTextBounds(bool includingTrailingSpace) {
+ if (text_.empty() && edit_mode_) return Rect(0, 0, 0, font_->GetFontSize());
+
+ auto result = DoGetTextBoundsIncludingEmptyLines(includingTrailingSpace);
+ return Rect(0, 0, result.size.width, result.size.height);
+}
+
+std::vector<Rect> OsxCTTextLayout::TextRangeRect(const TextRange& text_range) {
+ if (text_.empty()) return {};
+
+ auto tr = text_range;
+ tr = text_range.CoerceInto(head_empty_line_count_,
+ text_.size() - tail_empty_line_count_);
+ tr.position -= head_empty_line_count_;
+
+ std::vector<CGRect> results = DoTextRangeRect(tr);
+ std::vector<Rect> r;
+
+ for (auto& rect : results) {
+ r.push_back(transform_.TransformRect(Convert(rect)));
+ }
+
+ return r;
+}
+
+Rect OsxCTTextLayout::TextSinglePoint(Index position, bool trailing) {
+ Expects(position >= 0 && position <= text_.size());
+
+ if (text_.empty()) return {0, 0, 0, font_->GetFontSize()};
+
+ if (position < head_empty_line_count_) {
+ return {0, position * font_->GetFontSize(), 0, font_->GetFontSize()};
+ } else if (position > text_.size() - tail_empty_line_count_) {
+ return {
+ 0,
+ static_cast<float>(text_bounds_without_trailing_space_.size.height) +
+ (head_empty_line_count_ + position -
+ (text_.size() - tail_empty_line_count_) - 1) *
+ font_->GetFontSize(),
+ 0, font_->GetFontSize()};
+ } else {
+ auto result =
+ DoTextSinglePoint(position - head_empty_line_count_, trailing);
+ return transform_.TransformRect(Convert(result));
+ }
+}
+
+TextHitTestResult OsxCTTextLayout::HitTest(const Point& point) {
+ if (point.y < head_empty_line_count_ * font_->GetFontSize()) {
+ if (point.y < 0) {
+ return {0, false, false};
+ } else {
+ for (int i = 1; i <= head_empty_line_count_; ++i) {
+ if (point.y < i * font_->GetFontSize()) {
+ return {i - 1, false, false};
+ }
+ }
+ }
+ }
+
+ auto text_bounds = text_bounds_without_trailing_space_;
+
+ auto text_height = static_cast<float>(text_bounds.size.height);
+ auto th = text_height + head_empty_line_count_ * font_->GetFontSize();
+ if (point.y >= th) {
+ for (int i = 1; i <= tail_empty_line_count_; ++i) {
+ if (point.y < th + i * font_->GetFontSize()) {
+ return {text_.size() - (tail_empty_line_count_ - i), false, false};
+ }
+ }
+ return {text_.size(), false, false};
+ }
+
+ auto p = point;
+ p.y -= head_empty_line_count_ * font_->GetFontSize();
+ p.y = text_height - p.y;
+
+ for (int i = 0; i < line_count_; i++) {
+ auto line = lines_[i];
+ auto line_origin = line_origins_[i];
+
+ auto range = CTLineGetStringRange(line);
+
+ CGRect bounds{line_origin.x, line_origin.y - line_descents_[i],
+ CTLineGetOffsetForStringIndex(
+ line, range.location + range.length, nullptr),
+ line_heights_[i]};
+
+ bool force_inside = false;
+ if (i == 0 && p.y >= bounds.origin.y + bounds.size.height) {
+ force_inside = true;
+ }
+
+ if (i == line_count_ - 1 && p.y < bounds.origin.y) {
+ force_inside = true;
+ }
+
+ if (p.y >= bounds.origin.y || force_inside) {
+ auto pp = p;
+ pp.y = bounds.origin.y;
+ Index po;
+ bool inside_text;
+
+ if (pp.x < bounds.origin.x) {
+ po = actual_text_.IndexFromCodePointToCodeUnit(range.location);
+ inside_text = false;
+ } else if (pp.x > bounds.origin.x + bounds.size.width) {
+ po = actual_text_.IndexFromCodePointToCodeUnit(range.location +
+ range.length);
+ inside_text = false;
+ } else {
+ int position = CTLineGetStringIndexForPosition(
+ line,
+ CGPointMake(pp.x - line_origins_[i].x, pp.y - line_origins_[i].y));
+
+ po = actual_text_.IndexFromCodePointToCodeUnit(position);
+ inside_text = true;
+ }
+
+ if (po != 0 &&
+ po == actual_text_.IndexFromCodePointToCodeUnit(range.location +
+ range.length) &&
+ actual_text_[po - 1] == u'\n') {
+ --po;
+ }
+
+ return {po + head_empty_line_count_, false, inside_text};
+ }
+ }
+
+ return TextHitTestResult{0, false, false};
+}
+
+void OsxCTTextLayout::ReleaseResource() {
+ line_count_ = 0;
+ line_origins_.clear();
+ lines_.clear();
+ line_ascents_.clear();
+ line_descents_.clear();
+ line_heights_.clear();
+ if (ct_framesetter_) CFRelease(ct_framesetter_);
+ if (ct_frame_) CFRelease(ct_frame_);
+}
+
+void OsxCTTextLayout::RecreateFrame() {
+ ReleaseResource();
+
+ ct_framesetter_ =
+ CTFramesetterCreateWithAttributedString(cf_attributed_text_);
+ Ensures(ct_framesetter_);
+
+ CFRange fit_range;
+
+ suggest_height_ =
+ CTFramesetterSuggestFrameSizeWithConstraints(
+ ct_framesetter_,
+ CFRangeMake(0, CFAttributedStringGetLength(cf_attributed_text_)),
+ nullptr, CGSizeMake(max_width_, max_height_), &fit_range)
+ .height;
+
+ auto path = CGPathCreateMutable();
+ Ensures(path);
+ CGPathAddRect(path, nullptr, CGRectMake(0, 0, max_width_, suggest_height_));
+
+ ct_frame_ = CTFramesetterCreateFrame(
+ ct_framesetter_,
+ CFRangeMake(0, CFAttributedStringGetLength(cf_attributed_text_)), path,
+ nullptr);
+ Ensures(ct_frame_);
+
+ CGPathRelease(path);
+
+ const auto lines = CTFrameGetLines(ct_frame_);
+ line_count_ = CFArrayGetCount(lines);
+ lines_.resize(line_count_);
+ line_origins_.resize(line_count_);
+ line_ascents_.resize(line_count_);
+ line_descents_.resize(line_count_);
+ line_heights_.resize(line_count_);
+ CTFrameGetLineOrigins(ct_frame_, CFRangeMake(0, 0), line_origins_.data());
+ for (int i = 0; i < line_count_; i++) {
+ lines_[i] = static_cast<CTLineRef>(CFArrayGetValueAtIndex(lines, i));
+ double ascent, descent;
+ CTLineGetTypographicBounds(lines_[i], &ascent, &descent, nullptr);
+ line_ascents_[i] = static_cast<float>(ascent);
+ line_descents_[i] = static_cast<float>(descent);
+ line_heights_[i] = line_ascents_[i] + line_descents_[i];
+ }
+
+ auto bounds = DoGetTextBounds(false);
+ text_bounds_without_trailing_space_ = bounds;
+ text_bounds_with_trailing_space_ = DoGetTextBounds(true);
+
+ auto right = bounds.origin.x + bounds.size.width;
+ auto bottom = bounds.origin.y + bounds.size.height;
+
+ transform_ =
+ Matrix::Translation(-right / 2, -bottom / 2) * Matrix::Scale(1, -1) *
+ Matrix::Translation(right / 2, bottom / 2) *
+ Matrix::Translation(0, head_empty_line_count_ * font_->GetFontSize());
+}
+
+CTFrameRef OsxCTTextLayout::CreateFrameWithColor(const Color& color) {
+ auto path = CGPathCreateMutable();
+ CGPathAddRect(path, nullptr, CGRectMake(0, 0, max_width_, suggest_height_));
+
+ CGColorRef cg_color =
+ CGColorCreateGenericRGB(color.GetFloatRed(), color.GetFloatGreen(),
+ color.GetFloatBlue(), color.GetFloatAlpha());
+ CFAttributedStringSetAttribute(
+ cf_attributed_text_,
+ CFRangeMake(0, CFAttributedStringGetLength(cf_attributed_text_)),
+ kCTForegroundColorAttributeName, cg_color);
+
+ auto frame = CTFramesetterCreateFrame(
+ ct_framesetter_,
+ CFRangeMake(0, CFAttributedStringGetLength(cf_attributed_text_)), path,
+ nullptr);
+ Ensures(frame);
+
+ CGPathRelease(path);
+
+ return frame;
+}
+
+String OsxCTTextLayout::GetDebugString() {
+ return Format(u"OsxCTTextLayout(text: {}, size: ({}, {}))", text_, max_width_,
+ max_height_);
+}
+
+CGRect OsxCTTextLayout::DoGetTextBounds(bool includingTrailingSpace) {
+ if (actual_text_.empty()) return CGRect{};
+
+ auto rects = DoTextRangeRect(TextRange{0, actual_text_.size()});
+
+ float left = std::numeric_limits<float>::max();
+ float bottom = std::numeric_limits<float>::max();
+ float right = 0;
+ float top = 0;
+
+ for (auto& rect : rects) {
+ if (rect.origin.x < left) left = rect.origin.x;
+ if (rect.origin.y < bottom) bottom = rect.origin.y;
+ if (rect.origin.x + rect.size.width > right)
+ right = rect.origin.x + rect.size.width;
+ if (rect.origin.y + rect.size.height > top)
+ top = rect.origin.y + rect.size.height;
+ }
+
+ return CGRectMake(left, bottom, right - left, top - bottom);
+}
+
+CGRect OsxCTTextLayout::DoGetTextBoundsIncludingEmptyLines(
+ bool includingTrailingSpace) {
+ auto result = includingTrailingSpace ? text_bounds_with_trailing_space_
+ : text_bounds_without_trailing_space_;
+
+ result.size.height += head_empty_line_count_ * font_->GetFontSize();
+ result.size.height += tail_empty_line_count_ * font_->GetFontSize();
+
+ return result;
+}
+
+std::vector<CGRect> OsxCTTextLayout::DoTextRangeRect(
+ const TextRange& text_range) {
+ const auto r =
+ actual_text_.RangeFromCodeUnitToCodePoint(text_range).Normalize();
+
+ std::vector<CGRect> results;
+
+ for (int i = 0; i < line_count_; i++) {
+ auto line = lines_[i];
+ auto line_origin = line_origins_[i];
+
+ Range range = Convert(CTLineGetStringRange(line));
+ range = range.CoerceInto(r.GetStart(), r.GetEnd());
+
+ if (range.count) {
+ CGRect line_rect{line_origin.x, line_origin.y - line_descents_[i], 0,
+ line_heights_[i]};
+ float start_offset =
+ CTLineGetOffsetForStringIndex(line, range.GetStart(), nullptr);
+ float end_offset =
+ CTLineGetOffsetForStringIndex(line, range.GetEnd(), nullptr);
+ line_rect.origin.x += start_offset;
+ line_rect.size.width = end_offset - start_offset;
+ results.push_back(line_rect);
+ }
+ }
+
+ return results;
+}
+
+CGRect OsxCTTextLayout::DoTextSinglePoint(Index position, bool trailing) {
+ Expects(position >= 0 && position <= actual_text_.size());
+
+ if (actual_text_.empty()) return CGRectMake(0, 0, 0, font_->GetFontSize());
+
+ position = actual_text_.IndexFromCodeUnitToCodePoint(position);
+
+ for (int i = 0; i < line_count_; i++) {
+ auto line = lines_[i];
+ auto line_origin = line_origins_[i];
+
+ CFRange range = CTLineGetStringRange(line);
+ if (range.location <= position &&
+ position < range.location + range.length ||
+ i == line_count_ - 1 && position == range.location + range.length) {
+ auto offset = CTLineGetOffsetForStringIndex(line, position, nullptr);
+ return CGRectMake(offset + line_origin.x,
+ line_origin.y - line_descents_[i], 0, line_heights_[i]);
+ }
+ }
+
+ UnreachableCode();
+}
+} // namespace cru::platform::graphics::quartz
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
diff --git a/src/platform/osx/CMakeLists.txt b/src/platform/osx/CMakeLists.txt
new file mode 100644
index 00000000..9768a75c
--- /dev/null
+++ b/src/platform/osx/CMakeLists.txt
@@ -0,0 +1,8 @@
+add_library(CruPlatformBaseOsx SHARED
+ Resource.cpp
+)
+
+find_library(FOUNDATION Foundation REQUIRED)
+find_library(CORE_FOUNDATION CoreFoundation REQUIRED)
+
+target_link_libraries(CruPlatformBaseOsx PUBLIC CruPlatformBase ${FOUNDATION} ${CORE_FOUNDATION})
diff --git a/src/platform/osx/Resource.cpp b/src/platform/osx/Resource.cpp
new file mode 100644
index 00000000..0b098d6f
--- /dev/null
+++ b/src/platform/osx/Resource.cpp
@@ -0,0 +1 @@
+#include "cru/platform/osx/Resource.h"