aboutsummaryrefslogtreecommitdiff
path: root/src/platform/graphics/quartz
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/graphics/quartz
parent9e0c9d3499bc50c3534b4dc500d8b5d0b5f22752 (diff)
downloadcru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.tar.gz
cru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.tar.bz2
cru-8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6.zip
...
Diffstat (limited to 'src/platform/graphics/quartz')
-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
11 files changed, 1124 insertions, 0 deletions
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