diff options
author | crupest <crupest@outlook.com> | 2022-05-15 14:08:06 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2022-05-15 14:08:06 +0800 |
commit | 8ad2966933957ac5d6ff8dcd5e732736fd5e4dc6 (patch) | |
tree | 77e41cc14264060517c0f7ed95837012afb8342e /src/platform/graphics/quartz | |
parent | 9e0c9d3499bc50c3534b4dc500d8b5d0b5f22752 (diff) | |
download | cru-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.cpp | 36 | ||||
-rw-r--r-- | src/platform/graphics/quartz/CMakeLists.txt | 19 | ||||
-rw-r--r-- | src/platform/graphics/quartz/Convert.cpp | 63 | ||||
-rw-r--r-- | src/platform/graphics/quartz/Factory.cpp | 43 | ||||
-rw-r--r-- | src/platform/graphics/quartz/Font.cpp | 30 | ||||
-rw-r--r-- | src/platform/graphics/quartz/Geometry.cpp | 79 | ||||
-rw-r--r-- | src/platform/graphics/quartz/Image.cpp | 58 | ||||
-rw-r--r-- | src/platform/graphics/quartz/ImageFactory.cpp | 109 | ||||
-rw-r--r-- | src/platform/graphics/quartz/Painter.cpp | 230 | ||||
-rw-r--r-- | src/platform/graphics/quartz/Resource.cpp | 1 | ||||
-rw-r--r-- | src/platform/graphics/quartz/TextLayout.cpp | 456 |
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 |