diff options
Diffstat (limited to 'win32/SurfaceD2D.cxx')
-rw-r--r-- | win32/SurfaceD2D.cxx | 1738 |
1 files changed, 1738 insertions, 0 deletions
diff --git a/win32/SurfaceD2D.cxx b/win32/SurfaceD2D.cxx new file mode 100644 index 000000000..5ebf2191a --- /dev/null +++ b/win32/SurfaceD2D.cxx @@ -0,0 +1,1738 @@ +// Scintilla source code edit control +/** @file SurfaceD2D.cxx + ** Implementation of drawing to Direct2D on Windows. + **/ +// Copyright 1998-2003 by Neil Hodgson <neilh@scintilla.org> +// The License.txt file describes the conditions under which this software may be distributed. + +#include <cstddef> +#include <cstdlib> +#include <cstdint> +#include <cstring> +#include <cstdio> +#include <cstdarg> +#include <ctime> +#include <cmath> +#include <climits> + +#include <string_view> +#include <vector> +#include <map> +#include <optional> +#include <algorithm> +#include <iterator> +#include <memory> +#include <mutex> + +// Want to use std::min and std::max so don't want Windows.h version of min and max +#if !defined(NOMINMAX) +#define NOMINMAX +#endif +#undef _WIN32_WINNT +#define _WIN32_WINNT 0x0A00 +#undef WINVER +#define WINVER 0x0A00 +#define WIN32_LEAN_AND_MEAN 1 +#include <windows.h> +#include <commctrl.h> +#include <richedit.h> +#include <windowsx.h> +#include <shellscalingapi.h> + +#include <wrl.h> +using Microsoft::WRL::ComPtr; + +#if !defined(DISABLE_D2D) +#define USE_D2D 1 +#endif + +#if defined(USE_D2D) +#include <d2d1_1.h> +#include <d3d11_1.h> +#include <dwrite_1.h> +#endif + +#include "ScintillaTypes.h" + +#include "Debugging.h" +#include "Geometry.h" +#include "Platform.h" +#include "XPM.h" +#include "UniConversion.h" +#include "DBCS.h" + +#include "WinTypes.h" +#include "PlatWin.h" +#include "SurfaceGDI.h" +#if defined(USE_D2D) +#include "SurfaceD2D.h" +#endif + +// __uuidof is a Microsoft extension but makes COM code neater, so disable warning +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wlanguage-extension-token" +#endif + +using namespace Scintilla; +using namespace Scintilla::Internal; + +namespace { + +#if defined(USE_D2D) + +D2D1_DRAW_TEXT_OPTIONS d2dDrawTextOptions = D2D1_DRAW_TEXT_OPTIONS_NONE; + +HMODULE hDLLD2D{}; +HMODULE hDLLD3D{}; +HMODULE hDLLDWrite{}; + +PFN_D3D11_CREATE_DEVICE fnDCD{}; + +void LoadD2DOnce() noexcept { + DWORD loadLibraryFlags = 0; + HMODULE kernel32 = ::GetModuleHandleW(L"kernel32.dll"); + if (kernel32) { + if (::GetProcAddress(kernel32, "SetDefaultDllDirectories")) { + // Availability of SetDefaultDllDirectories implies Windows 8+ or + // that KB2533623 has been installed so LoadLibraryEx can be called + // with LOAD_LIBRARY_SEARCH_SYSTEM32. + loadLibraryFlags = LOAD_LIBRARY_SEARCH_SYSTEM32; + } + } + + using D2D1CFSig = HRESULT(WINAPI *)(D2D1_FACTORY_TYPE factoryType, REFIID riid, + CONST D2D1_FACTORY_OPTIONS *pFactoryOptions, IUnknown **factory); + using DWriteCFSig = HRESULT(WINAPI *)(DWRITE_FACTORY_TYPE factoryType, REFIID iid, + IUnknown **factory); + + hDLLD2D = ::LoadLibraryEx(TEXT("D2D1.DLL"), {}, loadLibraryFlags); + D2D1CFSig fnD2DCF = DLLFunction<D2D1CFSig>(hDLLD2D, "D2D1CreateFactory"); + if (fnD2DCF) { + const D2D1_FACTORY_OPTIONS options{}; + // A multi threaded factory in case Scintilla is used with multiple GUI threads + fnD2DCF(D2D1_FACTORY_TYPE_MULTI_THREADED, + __uuidof(ID2D1Factory1), + &options, + reinterpret_cast<IUnknown **>(&pD2DFactory)); + } + hDLLDWrite = ::LoadLibraryEx(TEXT("DWRITE.DLL"), {}, loadLibraryFlags); + DWriteCFSig fnDWCF = DLLFunction<DWriteCFSig>(hDLLDWrite, "DWriteCreateFactory"); + if (fnDWCF) { + const GUID IID_IDWriteFactory2 = // 0439fc60-ca44-4994-8dee-3a9af7b732ec + { 0x0439fc60, 0xca44, 0x4994, { 0x8d, 0xee, 0x3a, 0x9a, 0xf7, 0xb7, 0x32, 0xec } }; + + const HRESULT hr = fnDWCF(DWRITE_FACTORY_TYPE_SHARED, + IID_IDWriteFactory2, + reinterpret_cast<IUnknown **>(&pIDWriteFactory)); + if (SUCCEEDED(hr)) { + // D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT + d2dDrawTextOptions = static_cast<D2D1_DRAW_TEXT_OPTIONS>(0x00000004); + } else { + fnDWCF(DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory1), + reinterpret_cast<IUnknown **>(&pIDWriteFactory)); + } + } + + hDLLD3D = ::LoadLibraryEx(TEXT("D3D11.DLL"), {}, loadLibraryFlags); + if (!hDLLD3D) { + Platform::DebugPrintf("Direct3D not loaded\n"); + } + fnDCD = DLLFunction<PFN_D3D11_CREATE_DEVICE>(hDLLD3D, "D3D11CreateDevice"); + if (!fnDCD) { + Platform::DebugPrintf("Direct3D does not have D3D11CreateDevice\n"); + } +} + +constexpr D2D1_TEXT_ANTIALIAS_MODE DWriteMapFontQuality(FontQuality extraFontFlag) noexcept { + switch (extraFontFlag & FontQuality::QualityMask) { + + case FontQuality::QualityNonAntialiased: + return D2D1_TEXT_ANTIALIAS_MODE_ALIASED; + + case FontQuality::QualityAntialiased: + return D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE; + + case FontQuality::QualityLcdOptimized: + return D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE; + + default: + return D2D1_TEXT_ANTIALIAS_MODE_DEFAULT; + } +} + +struct FontDirectWrite : public FontWin { + ComPtr<IDWriteTextFormat> pTextFormat; + FontQuality extraFontFlag = FontQuality::QualityDefault; + CharacterSet characterSet = CharacterSet::Ansi; + FLOAT yAscent = 2.0f; + FLOAT yDescent = 1.0f; + FLOAT yInternalLeading = 0.0f; + + explicit FontDirectWrite(const FontParameters &fp) : + extraFontFlag(fp.extraFontFlag), + characterSet(fp.characterSet) { + const std::wstring wsFace = WStringFromUTF8(fp.faceName); + const std::wstring wsLocale = WStringFromUTF8(fp.localeName); + const FLOAT fHeight = static_cast<FLOAT>(fp.size); + const DWRITE_FONT_STYLE style = fp.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL; + HRESULT hr = pIDWriteFactory->CreateTextFormat(wsFace.c_str(), nullptr, + static_cast<DWRITE_FONT_WEIGHT>(fp.weight), + style, + static_cast<DWRITE_FONT_STRETCH>(fp.stretch), + fHeight, wsLocale.c_str(), pTextFormat.GetAddressOf()); + if (hr == E_INVALIDARG) { + // Possibly a bad locale name like "/" so try "en-us". + hr = pIDWriteFactory->CreateTextFormat(wsFace.c_str(), nullptr, + static_cast<DWRITE_FONT_WEIGHT>(fp.weight), + style, + static_cast<DWRITE_FONT_STRETCH>(fp.stretch), + fHeight, L"en-us", pTextFormat.ReleaseAndGetAddressOf()); + } + if (SUCCEEDED(hr)) { + pTextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); + + if (TextLayout pTextLayout = LayoutCreate(L"X", pTextFormat.Get())) { + constexpr int maxLines = 2; + DWRITE_LINE_METRICS lineMetrics[maxLines]{}; + UINT32 lineCount = 0; + hr = pTextLayout->GetLineMetrics(lineMetrics, maxLines, &lineCount); + if (SUCCEEDED(hr)) { + yAscent = lineMetrics[0].baseline; + yDescent = lineMetrics[0].height - lineMetrics[0].baseline; + + FLOAT emHeight; + hr = pTextLayout->GetFontSize(0, &emHeight); + if (SUCCEEDED(hr)) { + yInternalLeading = lineMetrics[0].height - emHeight; + } + } + pTextFormat->SetLineSpacing(DWRITE_LINE_SPACING_METHOD_UNIFORM, lineMetrics[0].height, lineMetrics[0].baseline); + } + } + } + // Allow copy constructor. Has to explicitly copy each field since can't use =default as deleted in Font. + FontDirectWrite(const FontDirectWrite &other) noexcept { + pTextFormat = other.pTextFormat; + extraFontFlag = other.extraFontFlag; + characterSet = other.characterSet; + yAscent = other.yAscent; + yDescent = other.yDescent; + yInternalLeading = other.yInternalLeading; + } + // Deleted so FontDirectWrite objects can not be copied. + FontDirectWrite(FontDirectWrite &&) = delete; + FontDirectWrite &operator=(const FontDirectWrite &) = delete; + FontDirectWrite &operator=(FontDirectWrite &&) = delete; + ~FontDirectWrite() noexcept override = default; + [[nodiscard]] HFONT HFont() const noexcept override { + LOGFONTW lf = {}; + const HRESULT hr = pTextFormat->GetFontFamilyName(lf.lfFaceName, LF_FACESIZE); + if (!SUCCEEDED(hr)) { + return {}; + } + lf.lfWeight = pTextFormat->GetFontWeight(); + lf.lfItalic = pTextFormat->GetFontStyle() == DWRITE_FONT_STYLE_ITALIC; + lf.lfHeight = -static_cast<int>(pTextFormat->GetFontSize()); + return ::CreateFontIndirectW(&lf); + } + + [[nodiscard]] int CodePageText(int codePage) const noexcept { + if (!(codePage == CpUtf8) && (characterSet != CharacterSet::Ansi)) { + codePage = CodePageFromCharSet(characterSet, codePage); + } + return codePage; + } + + static const FontDirectWrite *Cast(const Font *font_) { + const FontDirectWrite *pfm = dynamic_cast<const FontDirectWrite *>(font_); + PLATFORM_ASSERT(pfm); + if (!pfm) { + throw std::runtime_error("SurfaceD2D::SetFont: wrong Font type."); + } + return pfm; + } + + [[nodiscard]] std::unique_ptr<FontWin> Duplicate() const override { + return std::make_unique<FontDirectWrite>(*this); + } + + [[nodiscard]] CharacterSet GetCharacterSet() const noexcept override { + return characterSet; + } +}; + +constexpr D2D1_RECT_F RectangleFromPRectangle(PRectangle rc) noexcept { + return { + static_cast<FLOAT>(rc.left), + static_cast<FLOAT>(rc.top), + static_cast<FLOAT>(rc.right), + static_cast<FLOAT>(rc.bottom) + }; +} + +constexpr D2D1_POINT_2F DPointFromPoint(Point point) noexcept { + return { static_cast<FLOAT>(point.x), static_cast<FLOAT>(point.y) }; +} + +constexpr Supports SupportsD2D[] = { + Supports::LineDrawsFinal, + Supports::FractionalStrokeWidth, + Supports::TranslucentStroke, + Supports::PixelModification, + Supports::ThreadSafeMeasureWidths, +}; + +constexpr D2D1_RECT_F RectangleInset(D2D1_RECT_F rect, FLOAT inset) noexcept { + return D2D1_RECT_F{ + rect.left + inset, + rect.top + inset, + rect.right - inset, + rect.bottom - inset }; +} + +constexpr D2D_COLOR_F ColorFromColourAlpha(ColourRGBA colour) noexcept { + return D2D_COLOR_F{ + colour.GetRedComponent(), + colour.GetGreenComponent(), + colour.GetBlueComponent(), + colour.GetAlphaComponent() + }; +} + +class BlobInline; + +class SurfaceD2D : public Surface, public ISetRenderingParams { + SurfaceMode mode; + + // Text measuring surface: both pRenderTarget and pBitmapRenderTarget are null. + // Window surface: pRenderTarget is valid but not pBitmapRenderTarget. + // Bitmap drawing surface: both pRenderTarget and pBitmapRenderTarget are valid and the same. + ComPtr<ID2D1RenderTarget> pRenderTarget; + ComPtr<ID2D1BitmapRenderTarget> pBitmapRenderTarget; + int clipsActive = 0; + + BrushSolid pBrush = nullptr; + + static constexpr FontQuality invalidFontQuality = FontQuality::QualityMask; + FontQuality fontQuality = invalidFontQuality; + int logPixelsY = USER_DEFAULT_SCREEN_DPI; + int deviceScaleFactor = 1; + std::shared_ptr<RenderingParams> renderingParams; + + void Clear() noexcept; + void SetFontQuality(FontQuality extraFontFlag); + HRESULT GetBitmap(ID2D1Bitmap **ppBitmap); + void SetDeviceScaleFactor(const ID2D1RenderTarget *const pD2D1RenderTarget) noexcept; + +public: + SurfaceD2D() noexcept = default; + SurfaceD2D(ID2D1RenderTarget *pRenderTargetCompatible, int width, int height, SurfaceMode mode_, int logPixelsY_) noexcept; + // Deleted so SurfaceD2D objects can not be copied. + SurfaceD2D(const SurfaceD2D &) = delete; + SurfaceD2D(SurfaceD2D &&) = delete; + SurfaceD2D &operator=(const SurfaceD2D &) = delete; + SurfaceD2D &operator=(SurfaceD2D &&) = delete; + ~SurfaceD2D() noexcept override; + + void SetScale(WindowID wid) noexcept; + void Init(WindowID wid) override; + void Init(SurfaceID sid, WindowID wid) override; + std::unique_ptr<Surface> AllocatePixMap(int width, int height) override; + + void SetMode(SurfaceMode mode_) override; + + void Release() noexcept override; + int SupportsFeature(Supports feature) noexcept override; + bool Initialised() override; + + void D2DPenColourAlpha(ColourRGBA fore) noexcept; + int LogPixelsY() override; + int PixelDivisions() override; + int DeviceHeightFont(int points) override; + void LineDraw(Point start, Point end, Stroke stroke) override; + static Geometry GeometricFigure(const Point *pts, size_t npts, D2D1_FIGURE_BEGIN figureBegin) noexcept; + void PolyLine(const Point *pts, size_t npts, Stroke stroke) override; + void Polygon(const Point *pts, size_t npts, FillStroke fillStroke) override; + void RectangleDraw(PRectangle rc, FillStroke fillStroke) override; + void RectangleFrame(PRectangle rc, Stroke stroke) override; + void FillRectangle(PRectangle rc, Fill fill) override; + void FillRectangleAligned(PRectangle rc, Fill fill) override; + void FillRectangle(PRectangle rc, Surface &surfacePattern) override; + void RoundedRectangle(PRectangle rc, FillStroke fillStroke) override; + void AlphaRectangle(PRectangle rc, XYPOSITION cornerSize, FillStroke fillStroke) override; + void GradientRectangle(PRectangle rc, const std::vector<ColourStop> &stops, GradientOptions options) override; + void DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) override; + void Ellipse(PRectangle rc, FillStroke fillStroke) override; + void Stadium(PRectangle rc, FillStroke fillStroke, Ends ends) override; + void Copy(PRectangle rc, Point from, Surface &surfaceSource) override; + + std::unique_ptr<IScreenLineLayout> Layout(const IScreenLine *screenLine) override; + + void DrawTextCommon(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, int codePageOverride, UINT fuOptions); + + void DrawTextNoClip(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; + void DrawTextClipped(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; + void DrawTextTransparent(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) override; + void MeasureWidths(const Font *font_, std::string_view text, XYPOSITION *positions) override; + XYPOSITION WidthText(const Font *font_, std::string_view text) override; + + void DrawTextNoClipUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; + void DrawTextClippedUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; + void DrawTextTransparentUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) override; + void MeasureWidthsUTF8(const Font *font_, std::string_view text, XYPOSITION *positions) override; + XYPOSITION WidthTextUTF8(const Font *font_, std::string_view text) override; + + XYPOSITION Ascent(const Font *font_) override; + XYPOSITION Descent(const Font *font_) override; + XYPOSITION InternalLeading(const Font *font_) override; + XYPOSITION Height(const Font *font_) override; + XYPOSITION AverageCharWidth(const Font *font_) override; + + void SetClip(PRectangle rc) override; + void PopClip() override; + void FlushCachedState() override; + void FlushDrawing() override; + + void SetRenderingParams(std::shared_ptr<RenderingParams> renderingParams_) override; +}; + +SurfaceD2D::SurfaceD2D(ID2D1RenderTarget *pRenderTargetCompatible, int width, int height, SurfaceMode mode_, int logPixelsY_) noexcept { + const D2D1_SIZE_F desiredSize = D2D1::SizeF(static_cast<float>(width), static_cast<float>(height)); + D2D1_PIXEL_FORMAT desiredFormat; +#ifdef __MINGW32__ + desiredFormat.format = DXGI_FORMAT_UNKNOWN; +#else + desiredFormat = pRenderTargetCompatible->GetPixelFormat(); +#endif + desiredFormat.alphaMode = D2D1_ALPHA_MODE_IGNORE; + const HRESULT hr = pRenderTargetCompatible->CreateCompatibleRenderTarget( + &desiredSize, nullptr, &desiredFormat, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, pBitmapRenderTarget.GetAddressOf()); + if (SUCCEEDED(hr)) { + pRenderTarget = pBitmapRenderTarget; + SetDeviceScaleFactor(pRenderTarget.Get()); + pRenderTarget->BeginDraw(); + } + mode = mode_; + logPixelsY = logPixelsY_; +} + +SurfaceD2D::~SurfaceD2D() noexcept { + Clear(); +} + +void SurfaceD2D::Clear() noexcept { + pBrush = nullptr; + if (pRenderTarget) { + while (clipsActive) { + pRenderTarget->PopAxisAlignedClip(); + clipsActive--; + } + if (pBitmapRenderTarget) { + pRenderTarget->EndDraw(); + } + } + pRenderTarget = nullptr; + pBitmapRenderTarget = nullptr; +} + +void SurfaceD2D::Release() noexcept { + Clear(); +} + +void SurfaceD2D::SetScale(WindowID wid) noexcept { + fontQuality = invalidFontQuality; + logPixelsY = DpiForWindow(wid); +} + +int SurfaceD2D::SupportsFeature(Supports feature) noexcept { + for (const Supports f : SupportsD2D) { + if (f == feature) + return 1; + } + return 0; +} + +bool SurfaceD2D::Initialised() { + return pRenderTarget; +} + +void SurfaceD2D::Init(WindowID wid) { + Release(); + SetScale(wid); +} + +void SurfaceD2D::Init(SurfaceID sid, WindowID wid) { + Release(); + SetScale(wid); + pRenderTarget = static_cast<ID2D1RenderTarget *>(sid); + SetDeviceScaleFactor(pRenderTarget.Get()); +} + +std::unique_ptr<Surface> SurfaceD2D::AllocatePixMap(int width, int height) { + std::unique_ptr<SurfaceD2D> surf = std::make_unique<SurfaceD2D>(pRenderTarget.Get(), width, height, mode, logPixelsY); + surf->SetRenderingParams(renderingParams); + return surf; +} + +void SurfaceD2D::SetMode(SurfaceMode mode_) { + mode = mode_; +} + +HRESULT SurfaceD2D::GetBitmap(ID2D1Bitmap **ppBitmap) { + PLATFORM_ASSERT(pBitmapRenderTarget); + return pBitmapRenderTarget->GetBitmap(ppBitmap); +} + +void SurfaceD2D::D2DPenColourAlpha(ColourRGBA fore) noexcept { + if (pRenderTarget) { + const D2D_COLOR_F col = ColorFromColourAlpha(fore); + if (pBrush) { + pBrush->SetColor(col); + } else { + const HRESULT hr = pRenderTarget->CreateSolidColorBrush(col, &pBrush); + if (!SUCCEEDED(hr)) { + pBrush = nullptr; + } + } + } +} + +void SurfaceD2D::SetFontQuality(FontQuality extraFontFlag) { + if ((fontQuality != extraFontFlag) && renderingParams) { + fontQuality = extraFontFlag; + const D2D1_TEXT_ANTIALIAS_MODE aaMode = DWriteMapFontQuality(extraFontFlag); + if (aaMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE && renderingParams->customRenderingParams) { + pRenderTarget->SetTextRenderingParams(renderingParams->customRenderingParams.Get()); + } else if (renderingParams->defaultRenderingParams) { + pRenderTarget->SetTextRenderingParams(renderingParams->defaultRenderingParams.Get()); + } + pRenderTarget->SetTextAntialiasMode(aaMode); + } +} + +int SurfaceD2D::LogPixelsY() { + return logPixelsY; +} + +void SurfaceD2D::SetDeviceScaleFactor(const ID2D1RenderTarget *const pD2D1RenderTarget) noexcept { + FLOAT dpiX = 0.f; + FLOAT dpiY = 0.f; + pD2D1RenderTarget->GetDpi(&dpiX, &dpiY); + deviceScaleFactor = static_cast<int>(dpiX / dpiDefault); +} + +int SurfaceD2D::PixelDivisions() { + return deviceScaleFactor; +} + +int SurfaceD2D::DeviceHeightFont(int points) { + return ::MulDiv(points, LogPixelsY(), 72); +} + +void SurfaceD2D::LineDraw(Point start, Point end, Stroke stroke) { + D2DPenColourAlpha(stroke.colour); + + D2D1_STROKE_STYLE_PROPERTIES strokeProps {}; + strokeProps.startCap = D2D1_CAP_STYLE_SQUARE; + strokeProps.endCap = D2D1_CAP_STYLE_SQUARE; + strokeProps.dashCap = D2D1_CAP_STYLE_FLAT; + strokeProps.lineJoin = D2D1_LINE_JOIN_MITER; + strokeProps.miterLimit = 4.0f; + strokeProps.dashStyle = D2D1_DASH_STYLE_SOLID; + strokeProps.dashOffset = 0; + + // get the stroke style to apply + if (const StrokeStyle pStrokeStyle = StrokeStyleCreate(strokeProps)) { + pRenderTarget->DrawLine( + DPointFromPoint(start), + DPointFromPoint(end), pBrush.Get(), stroke.WidthF(), pStrokeStyle.Get()); + } +} + +Geometry SurfaceD2D::GeometricFigure(const Point *pts, size_t npts, D2D1_FIGURE_BEGIN figureBegin) noexcept { + Geometry geometry = GeometryCreate(); + if (geometry) { + if (const GeometrySink sink = GeometrySinkCreate(geometry.Get())) { + sink->BeginFigure(DPointFromPoint(pts[0]), figureBegin); + for (size_t i = 1; i < npts; i++) { + sink->AddLine(DPointFromPoint(pts[i])); + } + sink->EndFigure((figureBegin == D2D1_FIGURE_BEGIN_FILLED) ? + D2D1_FIGURE_END_CLOSED : D2D1_FIGURE_END_OPEN); + sink->Close(); + } + } + return geometry; +} + +void SurfaceD2D::PolyLine(const Point *pts, size_t npts, Stroke stroke) { + PLATFORM_ASSERT(pRenderTarget && (npts > 1)); + if (!pRenderTarget || (npts <= 1)) { + return; + } + + const Geometry geometry = GeometricFigure(pts, npts, D2D1_FIGURE_BEGIN_HOLLOW); + PLATFORM_ASSERT(geometry); + if (!geometry) { + return; + } + + D2DPenColourAlpha(stroke.colour); + D2D1_STROKE_STYLE_PROPERTIES strokeProps {}; + strokeProps.startCap = D2D1_CAP_STYLE_ROUND; + strokeProps.endCap = D2D1_CAP_STYLE_ROUND; + strokeProps.dashCap = D2D1_CAP_STYLE_FLAT; + strokeProps.lineJoin = D2D1_LINE_JOIN_MITER; + strokeProps.miterLimit = 4.0f; + strokeProps.dashStyle = D2D1_DASH_STYLE_SOLID; + strokeProps.dashOffset = 0; + + // get the stroke style to apply + if (const StrokeStyle pStrokeStyle = StrokeStyleCreate(strokeProps)) { + pRenderTarget->DrawGeometry(geometry.Get(), pBrush.Get(), stroke.WidthF(), pStrokeStyle.Get()); + } +} + +void SurfaceD2D::Polygon(const Point *pts, size_t npts, FillStroke fillStroke) { + PLATFORM_ASSERT(pRenderTarget && (npts > 2)); + if (pRenderTarget) { + const Geometry geometry = GeometricFigure(pts, npts, D2D1_FIGURE_BEGIN_FILLED); + PLATFORM_ASSERT(geometry); + if (geometry) { + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillGeometry(geometry.Get(), pBrush.Get()); + D2DPenColourAlpha(fillStroke.stroke.colour); + pRenderTarget->DrawGeometry(geometry.Get(), pBrush.Get(), fillStroke.stroke.WidthF()); + } + } +} + +void SurfaceD2D::RectangleDraw(PRectangle rc, FillStroke fillStroke) { + if (!pRenderTarget) + return; + const D2D1_RECT_F rect = RectangleFromPRectangle(rc); + const D2D1_RECT_F rectFill = RectangleInset(rect, fillStroke.stroke.WidthF()); + const float halfStroke = fillStroke.stroke.WidthF() / 2.0f; + const D2D1_RECT_F rectOutline = RectangleInset(rect, halfStroke); + + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillRectangle(&rectFill, pBrush.Get()); + D2DPenColourAlpha(fillStroke.stroke.colour); + pRenderTarget->DrawRectangle(&rectOutline, pBrush.Get(), fillStroke.stroke.WidthF()); +} + +void SurfaceD2D::RectangleFrame(PRectangle rc, Stroke stroke) { + if (pRenderTarget) { + const XYPOSITION halfStroke = stroke.width / 2.0f; + const D2D1_RECT_F rectangle1 = RectangleFromPRectangle(rc.Inset(halfStroke)); + D2DPenColourAlpha(stroke.colour); + pRenderTarget->DrawRectangle(&rectangle1, pBrush.Get(), stroke.WidthF()); + } +} + +void SurfaceD2D::FillRectangle(PRectangle rc, Fill fill) { + if (pRenderTarget) { + D2DPenColourAlpha(fill.colour); + const D2D1_RECT_F rectangle = RectangleFromPRectangle(rc); + pRenderTarget->FillRectangle(&rectangle, pBrush.Get()); + } +} + +void SurfaceD2D::FillRectangleAligned(PRectangle rc, Fill fill) { + FillRectangle(PixelAlign(rc, PixelDivisions()), fill); +} + +void SurfaceD2D::FillRectangle(PRectangle rc, Surface &surfacePattern) { + SurfaceD2D *psurfOther = dynamic_cast<SurfaceD2D *>(&surfacePattern); + PLATFORM_ASSERT(psurfOther); + if (!psurfOther) { + throw std::runtime_error("SurfaceD2D::FillRectangle: wrong Surface type."); + } + ComPtr<ID2D1Bitmap> pBitmap; + HRESULT hr = psurfOther->GetBitmap(pBitmap.GetAddressOf()); + if (SUCCEEDED(hr) && pBitmap) { + ComPtr<ID2D1BitmapBrush> pBitmapBrush; + const D2D1_BITMAP_BRUSH_PROPERTIES brushProperties = + D2D1::BitmapBrushProperties(D2D1_EXTEND_MODE_WRAP, D2D1_EXTEND_MODE_WRAP, + D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR); + // Create the bitmap brush. + hr = pRenderTarget->CreateBitmapBrush(pBitmap.Get(), brushProperties, pBitmapBrush.GetAddressOf()); + if (SUCCEEDED(hr) && pBitmapBrush) { + pRenderTarget->FillRectangle( + RectangleFromPRectangle(rc), + pBitmapBrush.Get()); + } + } +} + +void SurfaceD2D::RoundedRectangle(PRectangle rc, FillStroke fillStroke) { + if (pRenderTarget) { + const FLOAT minDimension = static_cast<FLOAT>(std::min(rc.Width(), rc.Height())) / 2.0f; + const FLOAT radius = std::min(4.0f, minDimension); + if (fillStroke.fill.colour == fillStroke.stroke.colour) { + const D2D1_ROUNDED_RECT roundedRectFill = { + RectangleFromPRectangle(rc), + radius, radius }; + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillRoundedRectangle(roundedRectFill, pBrush.Get()); + } else { + const D2D1_ROUNDED_RECT roundedRectFill = { + RectangleFromPRectangle(rc.Inset(1.0)), + radius-1, radius-1 }; + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillRoundedRectangle(roundedRectFill, pBrush.Get()); + + const D2D1_ROUNDED_RECT roundedRect = { + RectangleFromPRectangle(rc.Inset(0.5)), + radius, radius }; + D2DPenColourAlpha(fillStroke.stroke.colour); + pRenderTarget->DrawRoundedRectangle(roundedRect, pBrush.Get(), fillStroke.stroke.WidthF()); + } + } +} + +void SurfaceD2D::AlphaRectangle(PRectangle rc, XYPOSITION cornerSize, FillStroke fillStroke) { + const D2D1_RECT_F rect = RectangleFromPRectangle(rc); + const D2D1_RECT_F rectFill = RectangleInset(rect, fillStroke.stroke.WidthF()); + const float halfStroke = fillStroke.stroke.WidthF() / 2.0f; + const D2D1_RECT_F rectOutline = RectangleInset(rect, halfStroke); + if (pRenderTarget) { + if (cornerSize == 0) { + // When corner size is zero, draw square rectangle to prevent blurry pixels at corners + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillRectangle(rectFill, pBrush.Get()); + + D2DPenColourAlpha(fillStroke.stroke.colour); + pRenderTarget->DrawRectangle(rectOutline, pBrush.Get(), fillStroke.stroke.WidthF()); + } else { + const float cornerSizeF = static_cast<float>(cornerSize); + const D2D1_ROUNDED_RECT roundedRectFill = { + rectFill, cornerSizeF - 1.0f, cornerSizeF - 1.0f }; + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillRoundedRectangle(roundedRectFill, pBrush.Get()); + + const D2D1_ROUNDED_RECT roundedRect = { + rectOutline, cornerSizeF, cornerSizeF}; + D2DPenColourAlpha(fillStroke.stroke.colour); + pRenderTarget->DrawRoundedRectangle(roundedRect, pBrush.Get(), fillStroke.stroke.WidthF()); + } + } +} + +void SurfaceD2D::GradientRectangle(PRectangle rc, const std::vector<ColourStop> &stops, GradientOptions options) { + if (pRenderTarget) { + D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES lgbp { + DPointFromPoint(Point(rc.left, rc.top)), {} + }; + switch (options) { + case GradientOptions::leftToRight: + lgbp.endPoint = DPointFromPoint(Point(rc.right, rc.top)); + break; + case GradientOptions::topToBottom: + default: + lgbp.endPoint = DPointFromPoint(Point(rc.left, rc.bottom)); + break; + } + + std::vector<D2D1_GRADIENT_STOP> gradientStops; + for (const ColourStop &stop : stops) { + gradientStops.push_back({ static_cast<FLOAT>(stop.position), ColorFromColourAlpha(stop.colour) }); + } + ComPtr<ID2D1GradientStopCollection> pGradientStops; + HRESULT hr = pRenderTarget->CreateGradientStopCollection( + gradientStops.data(), static_cast<UINT32>(gradientStops.size()), pGradientStops.GetAddressOf()); + if (FAILED(hr) || !pGradientStops) { + return; + } + ComPtr<ID2D1LinearGradientBrush> pBrushLinear; + hr = pRenderTarget->CreateLinearGradientBrush( + lgbp, pGradientStops.Get(), pBrushLinear.GetAddressOf()); + if (SUCCEEDED(hr) && pBrushLinear) { + const D2D1_RECT_F rectangle = RectangleFromPRectangle(PRectangle( + std::round(rc.left), rc.top, std::round(rc.right), rc.bottom)); + pRenderTarget->FillRectangle(&rectangle, pBrushLinear.Get()); + } + } +} + +void SurfaceD2D::DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) { + if (pRenderTarget) { + if (rc.Width() > width) + rc.left += std::floor((rc.Width() - width) / 2); + rc.right = rc.left + width; + if (rc.Height() > height) + rc.top += std::floor((rc.Height() - height) / 2); + rc.bottom = rc.top + height; + + std::vector<unsigned char> image(RGBAImage::bytesPerPixel * height * width); + RGBAImage::BGRAFromRGBA(image.data(), pixelsImage, static_cast<ptrdiff_t>(height) * width); + + ComPtr<ID2D1Bitmap> bitmap; + const D2D1_SIZE_U size = D2D1::SizeU(width, height); + const D2D1_BITMAP_PROPERTIES props = {{DXGI_FORMAT_B8G8R8A8_UNORM, + D2D1_ALPHA_MODE_PREMULTIPLIED}, 72.0, 72.0}; + const HRESULT hr = pRenderTarget->CreateBitmap(size, image.data(), + width * 4, &props, bitmap.GetAddressOf()); + if (SUCCEEDED(hr)) { + const D2D1_RECT_F rcDestination = RectangleFromPRectangle(rc); + pRenderTarget->DrawBitmap(bitmap.Get(), rcDestination); + } + } +} + +void SurfaceD2D::Ellipse(PRectangle rc, FillStroke fillStroke) { + if (!pRenderTarget) + return; + const D2D1_POINT_2F centre = DPointFromPoint(rc.Centre()); + + const FLOAT radiusFill = static_cast<FLOAT>(rc.Width() / 2.0f - fillStroke.stroke.width); + const D2D1_ELLIPSE ellipseFill = { centre, radiusFill, radiusFill }; + + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillEllipse(ellipseFill, pBrush.Get()); + + const FLOAT radiusOutline = static_cast<FLOAT>(rc.Width() / 2.0f - fillStroke.stroke.width / 2.0f); + const D2D1_ELLIPSE ellipseOutline = { centre, radiusOutline, radiusOutline }; + + D2DPenColourAlpha(fillStroke.stroke.colour); + pRenderTarget->DrawEllipse(ellipseOutline, pBrush.Get(), fillStroke.stroke.WidthF()); +} + +void SurfaceD2D::Stadium(PRectangle rc, FillStroke fillStroke, Ends ends) { + if (!pRenderTarget) + return; + if (rc.Width() < rc.Height()) { + // Can't draw nice ends so just draw a rectangle + RectangleDraw(rc, fillStroke); + return; + } + const FLOAT radius = static_cast<FLOAT>(rc.Height() / 2.0); + const FLOAT radiusFill = radius - fillStroke.stroke.WidthF(); + const FLOAT halfStroke = fillStroke.stroke.WidthF() / 2.0f; + if (ends == Surface::Ends::semiCircles) { + const D2D1_RECT_F rect = RectangleFromPRectangle(rc); + const D2D1_ROUNDED_RECT roundedRectFill = { RectangleInset(rect, fillStroke.stroke.WidthF()), + radiusFill, radiusFill }; + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillRoundedRectangle(roundedRectFill, pBrush.Get()); + + const D2D1_ROUNDED_RECT roundedRect = { RectangleInset(rect, halfStroke), + radius, radius }; + D2DPenColourAlpha(fillStroke.stroke.colour); + pRenderTarget->DrawRoundedRectangle(roundedRect, pBrush.Get(), fillStroke.stroke.WidthF()); + } else { + const Ends leftSide = static_cast<Ends>(static_cast<int>(ends) & 0xf); + const Ends rightSide = static_cast<Ends>(static_cast<int>(ends) & 0xf0); + PRectangle rcInner = rc; + rcInner.left += radius; + rcInner.right -= radius; + const Geometry pathGeometry = GeometryCreate(); + if (!pathGeometry) + return; + if (const GeometrySink pSink = GeometrySinkCreate(pathGeometry.Get())) { + switch (leftSide) { + case Ends::leftFlat: + pSink->BeginFigure(DPointFromPoint(Point(rc.left + halfStroke, rc.top + halfStroke)), D2D1_FIGURE_BEGIN_FILLED); + pSink->AddLine(DPointFromPoint(Point(rc.left + halfStroke, rc.bottom - halfStroke))); + break; + case Ends::leftAngle: + pSink->BeginFigure(DPointFromPoint(Point(rcInner.left + halfStroke, rc.top + halfStroke)), D2D1_FIGURE_BEGIN_FILLED); + pSink->AddLine(DPointFromPoint(Point(rc.left + halfStroke, rc.Centre().y))); + pSink->AddLine(DPointFromPoint(Point(rcInner.left + halfStroke, rc.bottom - halfStroke))); + break; + case Ends::semiCircles: + default: { + pSink->BeginFigure(DPointFromPoint(Point(rcInner.left + halfStroke, rc.top + halfStroke)), D2D1_FIGURE_BEGIN_FILLED); + D2D1_ARC_SEGMENT segment{}; + segment.point = DPointFromPoint(Point(rcInner.left + halfStroke, rc.bottom - halfStroke)); + segment.size = D2D1::SizeF(radiusFill, radiusFill); + segment.rotationAngle = 0.0f; + segment.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE; + segment.arcSize = D2D1_ARC_SIZE_SMALL; + pSink->AddArc(segment); + } + break; + } + + switch (rightSide) { + case Ends::rightFlat: + pSink->AddLine(DPointFromPoint(Point(rc.right - halfStroke, rc.bottom - halfStroke))); + pSink->AddLine(DPointFromPoint(Point(rc.right - halfStroke, rc.top + halfStroke))); + break; + case Ends::rightAngle: + pSink->AddLine(DPointFromPoint(Point(rcInner.right - halfStroke, rc.bottom - halfStroke))); + pSink->AddLine(DPointFromPoint(Point(rc.right - halfStroke, rc.Centre().y))); + pSink->AddLine(DPointFromPoint(Point(rcInner.right - halfStroke, rc.top + halfStroke))); + break; + case Ends::semiCircles: + default: { + pSink->AddLine(DPointFromPoint(Point(rcInner.right - halfStroke, rc.bottom - halfStroke))); + D2D1_ARC_SEGMENT segment{}; + segment.point = DPointFromPoint(Point(rcInner.right - halfStroke, rc.top + halfStroke)); + segment.size = D2D1::SizeF(radiusFill, radiusFill); + segment.rotationAngle = 0.0f; + segment.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE; + segment.arcSize = D2D1_ARC_SIZE_SMALL; + pSink->AddArc(segment); + } + break; + } + + pSink->EndFigure(D2D1_FIGURE_END_CLOSED); + + pSink->Close(); + } + D2DPenColourAlpha(fillStroke.fill.colour); + pRenderTarget->FillGeometry(pathGeometry.Get(), pBrush.Get()); + D2DPenColourAlpha(fillStroke.stroke.colour); + pRenderTarget->DrawGeometry(pathGeometry.Get(), pBrush.Get(), fillStroke.stroke.WidthF()); + } +} + +void SurfaceD2D::Copy(PRectangle rc, Point from, Surface &surfaceSource) { + SurfaceD2D &surfOther = dynamic_cast<SurfaceD2D &>(surfaceSource); + ComPtr<ID2D1Bitmap> pBitmap; + const HRESULT hr = surfOther.GetBitmap(pBitmap.GetAddressOf()); + if (SUCCEEDED(hr) && pBitmap) { + const D2D1_RECT_F rcDestination = RectangleFromPRectangle(rc); + const D2D1_RECT_F rcSource = RectangleFromPRectangle(PRectangle( + from.x, from.y, from.x + rc.Width(), from.y + rc.Height())); + pRenderTarget->DrawBitmap(pBitmap.Get(), rcDestination, 1.0f, + D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, rcSource); + } +} + +class BlobInline final : public IDWriteInlineObject { + XYPOSITION width; + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, PVOID *ppv) override; + STDMETHODIMP_(ULONG)AddRef() override; + STDMETHODIMP_(ULONG)Release() override; + + // IDWriteInlineObject + COM_DECLSPEC_NOTHROW STDMETHODIMP Draw( + void *clientDrawingContext, + IDWriteTextRenderer *renderer, + FLOAT originX, + FLOAT originY, + BOOL isSideways, + BOOL isRightToLeft, + IUnknown *clientDrawingEffect + ) override; + COM_DECLSPEC_NOTHROW STDMETHODIMP GetMetrics(DWRITE_INLINE_OBJECT_METRICS *metrics) override; + COM_DECLSPEC_NOTHROW STDMETHODIMP GetOverhangMetrics(DWRITE_OVERHANG_METRICS *overhangs) override; + COM_DECLSPEC_NOTHROW STDMETHODIMP GetBreakConditions( + DWRITE_BREAK_CONDITION *breakConditionBefore, + DWRITE_BREAK_CONDITION *breakConditionAfter) override; +public: + explicit BlobInline(XYPOSITION width_=0.0f) noexcept : width(width_) { + } +}; + +/// Implement IUnknown +STDMETHODIMP BlobInline::QueryInterface(REFIID riid, PVOID *ppv) { + if (!ppv) + return E_POINTER; + // Never called so not checked. + *ppv = nullptr; + if (riid == IID_IUnknown) + *ppv = this; + if (riid == __uuidof(IDWriteInlineObject)) + *ppv = this; + if (!*ppv) + return E_NOINTERFACE; + return S_OK; +} + +STDMETHODIMP_(ULONG) BlobInline::AddRef() { + // Lifetime tied to Platform methods so ignore any reference operations. + return 1; +} + +STDMETHODIMP_(ULONG) BlobInline::Release() { + // Lifetime tied to Platform methods so ignore any reference operations. + return 1; +} + +/// Implement IDWriteInlineObject +COM_DECLSPEC_NOTHROW HRESULT STDMETHODCALLTYPE BlobInline::Draw( + void*, + IDWriteTextRenderer*, + FLOAT, + FLOAT, + BOOL, + BOOL, + IUnknown*) { + // Since not performing drawing, not necessary to implement + // Could be implemented by calling back into platform-independent code. + // This would allow more of the drawing to be mediated through DirectWrite. + return S_OK; +} + +COM_DECLSPEC_NOTHROW HRESULT STDMETHODCALLTYPE BlobInline::GetMetrics( + DWRITE_INLINE_OBJECT_METRICS *metrics +) { + if (!metrics) + return E_POINTER; + metrics->width = static_cast<FLOAT>(width); + metrics->height = 2; + metrics->baseline = 1; + metrics->supportsSideways = FALSE; + return S_OK; +} + +COM_DECLSPEC_NOTHROW HRESULT STDMETHODCALLTYPE BlobInline::GetOverhangMetrics( + DWRITE_OVERHANG_METRICS *overhangs +) { + if (!overhangs) + return E_POINTER; + overhangs->left = 0; + overhangs->top = 0; + overhangs->right = 0; + overhangs->bottom = 0; + return S_OK; +} + +COM_DECLSPEC_NOTHROW HRESULT STDMETHODCALLTYPE BlobInline::GetBreakConditions( + DWRITE_BREAK_CONDITION *breakConditionBefore, + DWRITE_BREAK_CONDITION *breakConditionAfter +) { + if (!breakConditionBefore || !breakConditionAfter) + return E_POINTER; + // Since not performing 2D layout, not necessary to implement + *breakConditionBefore = DWRITE_BREAK_CONDITION_NEUTRAL; + *breakConditionAfter = DWRITE_BREAK_CONDITION_NEUTRAL; + return S_OK; +} + +class ScreenLineLayout : public IScreenLineLayout { + TextLayout textLayout; + std::string text; + std::wstring buffer; + std::vector<BlobInline> blobs; + static void FillTextLayoutFormats(const IScreenLine *screenLine, IDWriteTextLayout *textLayout, std::vector<BlobInline> &blobs); + static std::wstring ReplaceRepresentation(std::string_view text); + static size_t GetPositionInLayout(std::string_view text, size_t position); +public: + explicit ScreenLineLayout(const IScreenLine *screenLine); + // Deleted so ScreenLineLayout objects can not be copied + ScreenLineLayout(const ScreenLineLayout &) = delete; + ScreenLineLayout(ScreenLineLayout &&) = delete; + ScreenLineLayout &operator=(const ScreenLineLayout &) = delete; + ScreenLineLayout &operator=(ScreenLineLayout &&) = delete; + ~ScreenLineLayout() noexcept override = default; + size_t PositionFromX(XYPOSITION xDistance, bool charPosition) override; + XYPOSITION XFromPosition(size_t caretPosition) override; + std::vector<Interval> FindRangeIntervals(size_t start, size_t end) override; +}; + +// Each char can have its own style, so we fill the textLayout with the textFormat of each char + +void ScreenLineLayout::FillTextLayoutFormats(const IScreenLine *screenLine, IDWriteTextLayout *textLayout, std::vector<BlobInline> &blobs) { + // Reserve enough entries up front so they are not moved and the pointers handed + // to textLayout remain valid. + const ptrdiff_t numRepresentations = screenLine->RepresentationCount(); + std::string_view text = screenLine->Text(); + const ptrdiff_t numTabs = std::count(std::begin(text), std::end(text), '\t'); + blobs.reserve(numRepresentations + numTabs); + + UINT32 layoutPosition = 0; + + for (size_t bytePosition = 0; bytePosition < screenLine->Length();) { + const unsigned char uch = screenLine->Text()[bytePosition]; + const unsigned int byteCount = UTF8BytesOfLead[uch]; + const UINT32 codeUnits = UTF16LengthFromUTF8ByteCount(byteCount); + const DWRITE_TEXT_RANGE textRange = { layoutPosition, codeUnits }; + + XYPOSITION representationWidth = screenLine->RepresentationWidth(bytePosition); + if ((representationWidth == 0.0f) && (screenLine->Text()[bytePosition] == '\t')) { + D2D1_POINT_2F realPt {}; + DWRITE_HIT_TEST_METRICS realCaretMetrics {}; + textLayout->HitTestTextPosition( + layoutPosition, + false, // trailing if false, else leading edge + &realPt.x, + &realPt.y, + &realCaretMetrics + ); + + const XYPOSITION nextTab = screenLine->TabPositionAfter(realPt.x); + representationWidth = nextTab - realPt.x; + } + if (representationWidth > 0.0f) { + blobs.push_back(BlobInline(representationWidth)); + textLayout->SetInlineObject(&blobs.back(), textRange); + }; + + const FontDirectWrite *pfm = + dynamic_cast<const FontDirectWrite *>(screenLine->FontOfPosition(bytePosition)); + if (!pfm) { + throw std::runtime_error("FillTextLayoutFormats: wrong Font type."); + } + + const unsigned int fontFamilyNameSize = pfm->pTextFormat->GetFontFamilyNameLength(); + std::wstring fontFamilyName(fontFamilyNameSize, 0); + const HRESULT hrFamily = pfm->pTextFormat->GetFontFamilyName(fontFamilyName.data(), fontFamilyNameSize + 1); + if (SUCCEEDED(hrFamily)) { + textLayout->SetFontFamilyName(fontFamilyName.c_str(), textRange); + } + + textLayout->SetFontSize(pfm->pTextFormat->GetFontSize(), textRange); + textLayout->SetFontWeight(pfm->pTextFormat->GetFontWeight(), textRange); + textLayout->SetFontStyle(pfm->pTextFormat->GetFontStyle(), textRange); + + const unsigned int localeNameSize = pfm->pTextFormat->GetLocaleNameLength(); + std::wstring localeName(localeNameSize, 0); + const HRESULT hrLocale = pfm->pTextFormat->GetLocaleName(localeName.data(), localeNameSize + 1); + if (SUCCEEDED(hrLocale)) { + textLayout->SetLocaleName(localeName.c_str(), textRange); + } + + textLayout->SetFontStretch(pfm->pTextFormat->GetFontStretch(), textRange); + + IDWriteFontCollection *fontCollection = nullptr; + if (SUCCEEDED(pfm->pTextFormat->GetFontCollection(&fontCollection))) { + textLayout->SetFontCollection(fontCollection, textRange); + } + + bytePosition += byteCount; + layoutPosition += codeUnits; + } + +} + +/* Convert to a wide character string and replace tabs with X to stop DirectWrite tab expansion */ + +std::wstring ScreenLineLayout::ReplaceRepresentation(std::string_view text) { + const TextWide wideText(text, CpUtf8); + std::wstring ws(wideText.buffer, wideText.tlen); + std::replace(ws.begin(), ws.end(), L'\t', L'X'); + return ws; +} + +// Finds the position in the wide character version of the text. + +size_t ScreenLineLayout::GetPositionInLayout(std::string_view text, size_t position) { + const std::string_view textUptoPosition = text.substr(0, position); + return UTF16Length(textUptoPosition); +} + +ScreenLineLayout::ScreenLineLayout(const IScreenLine *screenLine) { + // If the text is empty, then no need to go through this function + if (!screenLine || !screenLine->Length()) + return; + + text = screenLine->Text(); + + // Get textFormat + const FontDirectWrite *pfm = FontDirectWrite::Cast(screenLine->FontOfPosition(0)); + if (!pfm->pTextFormat) { + return; + } + + // Convert the string to wstring and replace the original control characters with their representative chars. + buffer = ReplaceRepresentation(screenLine->Text()); + + // Create a text layout + textLayout = LayoutCreate( + buffer, + pfm->pTextFormat.Get(), + static_cast<FLOAT>(screenLine->Width()), + static_cast<FLOAT>(screenLine->Height())); + if (!textLayout) { + return; + } + + // Fill the textLayout chars with their own formats + FillTextLayoutFormats(screenLine, textLayout.Get(), blobs); +} + +// Get the position from the provided x + +size_t ScreenLineLayout::PositionFromX(XYPOSITION xDistance, bool charPosition) { + if (!textLayout) { + return 0; + } + + // Returns the text position corresponding to the mouse x,y. + // If hitting the trailing side of a cluster, return the + // leading edge of the following text position. + + BOOL isTrailingHit = FALSE; + BOOL isInside = FALSE; + DWRITE_HIT_TEST_METRICS caretMetrics {}; + + textLayout->HitTestPoint( + static_cast<FLOAT>(xDistance), + 0.0f, + &isTrailingHit, + &isInside, + &caretMetrics + ); + + DWRITE_HIT_TEST_METRICS hitTestMetrics {}; + if (isTrailingHit) { + FLOAT caretX = 0.0f; + FLOAT caretY = 0.0f; + + // Uses hit-testing to align the current caret position to a whole cluster, + // rather than residing in the middle of a base character + diacritic, + // surrogate pair, or character + UVS. + + // Align the caret to the nearest whole cluster. + textLayout->HitTestTextPosition( + caretMetrics.textPosition, + false, + &caretX, + &caretY, + &hitTestMetrics + ); + } + + size_t pos; + if (charPosition) { + pos = isTrailingHit ? hitTestMetrics.textPosition : caretMetrics.textPosition; + } else { + pos = isTrailingHit ? static_cast<size_t>(hitTestMetrics.textPosition) + hitTestMetrics.length : caretMetrics.textPosition; + } + + // Get the character position in original string + return UTF8PositionFromUTF16Position(text, pos); +} + +// Finds the point of the caret position + +XYPOSITION ScreenLineLayout::XFromPosition(size_t caretPosition) { + if (!textLayout) { + return 0.0; + } + // Convert byte positions to wchar_t positions + const size_t position = GetPositionInLayout(text, caretPosition); + + // Translate text character offset to point x,y. + DWRITE_HIT_TEST_METRICS caretMetrics {}; + D2D1_POINT_2F pt {}; + + textLayout->HitTestTextPosition( + static_cast<UINT32>(position), + false, // trailing if false, else leading edge + &pt.x, + &pt.y, + &caretMetrics + ); + + return pt.x; +} + +// Find the selection range rectangles + +std::vector<Interval> ScreenLineLayout::FindRangeIntervals(size_t start, size_t end) { + std::vector<Interval> ret; + + if (!textLayout || (start == end)) { + return ret; + } + + // Convert byte positions to wchar_t positions + const size_t startPos = GetPositionInLayout(text, start); + const size_t endPos = GetPositionInLayout(text, end); + + // Find selection range length + const size_t rangeLength = (endPos > startPos) ? (endPos - startPos) : (startPos - endPos); + + // Determine actual number of hit-test ranges + UINT32 actualHitTestCount = 0; + + // First try with 2 elements and if more needed, allocate. + std::vector<DWRITE_HIT_TEST_METRICS> hitTestMetrics(2); + textLayout->HitTestTextRange( + static_cast<UINT32>(startPos), + static_cast<UINT32>(rangeLength), + 0, // x + 0, // y + hitTestMetrics.data(), + static_cast<UINT32>(hitTestMetrics.size()), + &actualHitTestCount + ); + + if (actualHitTestCount == 0) { + return ret; + } + + if (hitTestMetrics.size() < actualHitTestCount) { + // Allocate enough room to return all hit-test metrics. + hitTestMetrics.resize(actualHitTestCount); + textLayout->HitTestTextRange( + static_cast<UINT32>(startPos), + static_cast<UINT32>(rangeLength), + 0, // x + 0, // y + hitTestMetrics.data(), + static_cast<UINT32>(hitTestMetrics.size()), + &actualHitTestCount + ); + } + + // Get the selection ranges behind the text. + + for (size_t i = 0; i < actualHitTestCount; ++i) { + // Store selection rectangle + const DWRITE_HIT_TEST_METRICS &htm = hitTestMetrics[i]; + const Interval selectionInterval { htm.left, htm.left + htm.width }; + ret.push_back(selectionInterval); + } + + return ret; +} + +std::unique_ptr<IScreenLineLayout> SurfaceD2D::Layout(const IScreenLine *screenLine) { + return std::make_unique<ScreenLineLayout>(screenLine); +} + +void SurfaceD2D::DrawTextCommon(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, int codePageOverride, UINT fuOptions) { + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + if (pfm->pTextFormat && pRenderTarget && pBrush) { + // Use Unicode calls + const int codePageDraw = codePageOverride ? codePageOverride : pfm->CodePageText(mode.codePage); + const TextWide tbuf(text, codePageDraw); + + SetFontQuality(pfm->extraFontFlag); + if (fuOptions & ETO_CLIPPED) { + const D2D1_RECT_F rcClip = RectangleFromPRectangle(rc); + pRenderTarget->PushAxisAlignedClip(rcClip, D2D1_ANTIALIAS_MODE_ALIASED); + } + + // Explicitly creating a text layout appears a little faster + TextLayout pTextLayout = LayoutCreate( + tbuf.AsView(), + pfm->pTextFormat.Get(), + static_cast<FLOAT>(rc.Width()), + static_cast<FLOAT>(rc.Height())); + if (pTextLayout) { + const D2D1_POINT_2F origin = DPointFromPoint(Point(rc.left, ybase - pfm->yAscent)); + pRenderTarget->DrawTextLayout(origin, pTextLayout.Get(), pBrush.Get(), d2dDrawTextOptions); + } + + if (fuOptions & ETO_CLIPPED) { + pRenderTarget->PopAxisAlignedClip(); + } + } +} + +void SurfaceD2D::DrawTextNoClip(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, + ColourRGBA fore, ColourRGBA back) { + if (pRenderTarget) { + FillRectangleAligned(rc, back); + D2DPenColourAlpha(fore); + DrawTextCommon(rc, font_, ybase, text, 0, ETO_OPAQUE); + } +} + +void SurfaceD2D::DrawTextClipped(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, + ColourRGBA fore, ColourRGBA back) { + if (pRenderTarget) { + FillRectangleAligned(rc, back); + D2DPenColourAlpha(fore); + DrawTextCommon(rc, font_, ybase, text, 0, ETO_OPAQUE | ETO_CLIPPED); + } +} + +void SurfaceD2D::DrawTextTransparent(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, + ColourRGBA fore) { + // Avoid drawing spaces in transparent mode + for (const char ch : text) { + if (ch != ' ') { + if (pRenderTarget) { + D2DPenColourAlpha(fore); + DrawTextCommon(rc, font_, ybase, text, 0, 0); + } + return; + } + } +} + +HRESULT MeasurePositions(TextPositions &poses, const TextWide &tbuf, IDWriteTextFormat *pTextFormat) { + if (!pTextFormat) { + // Unexpected failure like no access to DirectWrite so give up. + return E_FAIL; + } + + // Initialize poses for safety. + std::fill(poses.buffer, poses.buffer + tbuf.tlen, 0.0f); + // Create a layout + TextLayout pTextLayout = LayoutCreate(tbuf.AsView(), pTextFormat); + if (!pTextLayout) { + return E_FAIL; + } + VarBuffer<DWRITE_CLUSTER_METRICS, stackBufferLength> cm(tbuf.tlen); + UINT32 count = 0; + const HRESULT hrGetCluster = pTextLayout->GetClusterMetrics(cm.buffer, tbuf.tlen, &count); + if (!SUCCEEDED(hrGetCluster)) { + return hrGetCluster; + } + const DWRITE_CLUSTER_METRICS * const clusterMetrics = cm.buffer; + // A cluster may be more than one WCHAR, such as for "ffi" which is a ligature in the Candara font + XYPOSITION position = 0.0; + int ti=0; + for (unsigned int ci=0; ci<count; ci++) { + for (unsigned int inCluster=0; inCluster<clusterMetrics[ci].length; inCluster++) { + poses.buffer[ti++] = position + clusterMetrics[ci].width * (inCluster + 1) / clusterMetrics[ci].length; + } + position += clusterMetrics[ci].width; + } + PLATFORM_ASSERT(ti == tbuf.tlen); + return S_OK; +} + +void SurfaceD2D::MeasureWidths(const Font *font_, std::string_view text, XYPOSITION *positions) { + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + const int codePageText = pfm->CodePageText(mode.codePage); + const TextWide tbuf(text, codePageText); + TextPositions poses(tbuf.tlen); + if (FAILED(MeasurePositions(poses, tbuf, pfm->pTextFormat.Get()))) { + return; + } + if (codePageText == CpUtf8) { + // Map the widths given for UTF-16 characters back onto the UTF-8 input string + size_t i = 0; + for (int ui = 0; ui < tbuf.tlen; ui++) { + const unsigned char uch = text[i]; + const unsigned int byteCount = UTF8BytesOfLead[uch]; + if (byteCount == 4) { // Non-BMP + ui++; + } + for (unsigned int bytePos=0; (bytePos<byteCount) && (i<text.length()) && (ui<tbuf.tlen); bytePos++) { + positions[i++] = poses.buffer[ui]; + } + } + const XYPOSITION lastPos = (i > 0) ? positions[i - 1] : 0.0; + while (i<text.length()) { + positions[i++] = lastPos; + } + } else if (!IsDBCSCodePage(codePageText)) { + + // One char per position + PLATFORM_ASSERT(text.length() == static_cast<size_t>(tbuf.tlen)); + for (int kk=0; kk<tbuf.tlen; kk++) { + positions[kk] = poses.buffer[kk]; + } + + } else { + + // May be one or two bytes per position + int ui = 0; + for (size_t i=0; i<text.length() && ui<tbuf.tlen;) { + positions[i] = poses.buffer[ui]; + if (DBCSIsLeadByte(codePageText, text[i])) { + positions[i+1] = poses.buffer[ui]; + i += 2; + } else { + i++; + } + + ui++; + } + } +} + +XYPOSITION SurfaceD2D::WidthText(const Font *font_, std::string_view text) { + FLOAT width = 1.0; + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + if (pfm->pTextFormat) { + const TextWide tbuf(text, pfm->CodePageText(mode.codePage)); + // Create a layout + if (TextLayout pTextLayout = LayoutCreate(tbuf.AsView(), pfm->pTextFormat.Get())) { + DWRITE_TEXT_METRICS textMetrics; + if (SUCCEEDED(pTextLayout->GetMetrics(&textMetrics))) + width = textMetrics.widthIncludingTrailingWhitespace; + } + } + return width; +} + +void SurfaceD2D::DrawTextNoClipUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, + ColourRGBA fore, ColourRGBA back) { + if (pRenderTarget) { + FillRectangleAligned(rc, back); + D2DPenColourAlpha(fore); + DrawTextCommon(rc, font_, ybase, text, CpUtf8, ETO_OPAQUE); + } +} + +void SurfaceD2D::DrawTextClippedUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, + ColourRGBA fore, ColourRGBA back) { + if (pRenderTarget) { + FillRectangleAligned(rc, back); + D2DPenColourAlpha(fore); + DrawTextCommon(rc, font_, ybase, text, CpUtf8, ETO_OPAQUE | ETO_CLIPPED); + } +} + +void SurfaceD2D::DrawTextTransparentUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, + ColourRGBA fore) { + // Avoid drawing spaces in transparent mode + for (const char ch : text) { + if (ch != ' ') { + if (pRenderTarget) { + D2DPenColourAlpha(fore); + DrawTextCommon(rc, font_, ybase, text, CpUtf8, 0); + } + return; + } + } +} + +void SurfaceD2D::MeasureWidthsUTF8(const Font *font_, std::string_view text, XYPOSITION *positions) { + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + const TextWide tbuf(text, CpUtf8); + TextPositions poses(tbuf.tlen); + if (FAILED(MeasurePositions(poses, tbuf, pfm->pTextFormat.Get()))) { + return; + } + // Map the widths given for UTF-16 characters back onto the UTF-8 input string + size_t i = 0; + for (int ui = 0; ui < tbuf.tlen; ui++) { + const unsigned char uch = text[i]; + const unsigned int byteCount = UTF8BytesOfLead[uch]; + if (byteCount == 4) { // Non-BMP + ui++; + PLATFORM_ASSERT(ui < tbuf.tlen); + } + for (unsigned int bytePos=0; (bytePos<byteCount) && (i<text.length()) && (ui < tbuf.tlen); bytePos++) { + positions[i++] = poses.buffer[ui]; + } + } + const XYPOSITION lastPos = (i > 0) ? positions[i - 1] : 0.0; + while (i < text.length()) { + positions[i++] = lastPos; + } +} + +XYPOSITION SurfaceD2D::WidthTextUTF8(const Font * font_, std::string_view text) { + FLOAT width = 1.0; + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + if (pfm->pTextFormat) { + const TextWide tbuf(text, CpUtf8); + // Create a layout + if (TextLayout pTextLayout = LayoutCreate(tbuf.AsView(), pfm->pTextFormat.Get())) { + DWRITE_TEXT_METRICS textMetrics; + if (SUCCEEDED(pTextLayout->GetMetrics(&textMetrics))) + width = textMetrics.widthIncludingTrailingWhitespace; + } + } + return width; +} + +XYPOSITION SurfaceD2D::Ascent(const Font *font_) { + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + return std::ceil(pfm->yAscent); +} + +XYPOSITION SurfaceD2D::Descent(const Font *font_) { + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + return std::ceil(pfm->yDescent); +} + +XYPOSITION SurfaceD2D::InternalLeading(const Font *font_) { + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + return std::floor(pfm->yInternalLeading); +} + +XYPOSITION SurfaceD2D::Height(const Font *font_) { + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + return std::ceil(pfm->yAscent) + std::ceil(pfm->yDescent); +} + +XYPOSITION SurfaceD2D::AverageCharWidth(const Font *font_) { + FLOAT width = 1.0; + const FontDirectWrite *pfm = FontDirectWrite::Cast(font_); + if (pfm->pTextFormat) { + // Create a layout + static constexpr std::wstring_view wsvAllAlpha = L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if (TextLayout pTextLayout = LayoutCreate(wsvAllAlpha, pfm->pTextFormat.Get())) { + DWRITE_TEXT_METRICS textMetrics; + if (SUCCEEDED(pTextLayout->GetMetrics(&textMetrics))) + width = textMetrics.width / wsvAllAlpha.length(); + } + } + return width; +} + +void SurfaceD2D::SetClip(PRectangle rc) { + if (pRenderTarget) { + const D2D1_RECT_F rcClip = RectangleFromPRectangle(rc); + pRenderTarget->PushAxisAlignedClip(rcClip, D2D1_ANTIALIAS_MODE_ALIASED); + clipsActive++; + } +} + +void SurfaceD2D::PopClip() { + if (pRenderTarget) { + PLATFORM_ASSERT(clipsActive > 0); + pRenderTarget->PopAxisAlignedClip(); + clipsActive--; + } +} + +void SurfaceD2D::FlushCachedState() { +} + +void SurfaceD2D::FlushDrawing() { + if (pRenderTarget) { + pRenderTarget->Flush(); + } +} + +void SurfaceD2D::SetRenderingParams(std::shared_ptr<RenderingParams> renderingParams_) { + renderingParams = std::move(renderingParams_); +} + +#endif + +} + +namespace Scintilla::Internal { + +#if defined(USE_D2D) +IDWriteFactory1 *pIDWriteFactory = nullptr; +ID2D1Factory1 *pD2DFactory = nullptr; + +HRESULT CreateD3D(D3D11Device &device) noexcept { + device = nullptr; + if (!fnDCD) { + return E_FAIL; + } + + const D3D_FEATURE_LEVEL featureLevels[] = { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1 + }; + + // Create device. + // Try for a hardware device but, if that fails, fall back to the Warp software rasterizer. + ComPtr<ID3D11Device> upDevice; + HRESULT hr = S_OK; + const D3D_DRIVER_TYPE typesToTry[] = { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }; + for (const D3D_DRIVER_TYPE type : typesToTry) { + hr = fnDCD(nullptr, + type, + {}, + D3D11_CREATE_DEVICE_BGRA_SUPPORT, + featureLevels, + ARRAYSIZE(featureLevels), + D3D11_SDK_VERSION, + upDevice.GetAddressOf(), + nullptr, + nullptr); + if (SUCCEEDED(hr)) + break; + } + if (FAILED(hr)) { + Platform::DebugPrintf("Failed to create D3D11 device 0x%lx\n", hr); + return hr; + } + + // Convert from D3D11 to D3D11.1 + hr = upDevice.As(&device); + if (FAILED(hr)) { + Platform::DebugPrintf("Failed to create D3D11.1 device 0x%lx\n", hr); + } + return hr; +} + +bool LoadD2D() noexcept { + static std::once_flag once; + try { + std::call_once(once, LoadD2DOnce); + } + catch (...) { + // ignore + } + return pIDWriteFactory && pD2DFactory; +} + +void ReleaseD2D() noexcept { + ReleaseUnknown(pIDWriteFactory); + ReleaseUnknown(pD2DFactory); + ReleaseLibrary(hDLLDWrite); + ReleaseLibrary(hDLLD3D); + ReleaseLibrary(hDLLD2D); +} + +HRESULT CreateDCRenderTarget(const D2D1_RENDER_TARGET_PROPERTIES *renderTargetProperties, DCRenderTarget &dcRT) noexcept { + return pD2DFactory->CreateDCRenderTarget(renderTargetProperties, dcRT.ReleaseAndGetAddressOf()); +} + +BrushSolid BrushSolidCreate(ID2D1RenderTarget *pTarget, COLORREF colour) noexcept { + BrushSolid brush; + const D2D_COLOR_F col = ColorFromColourAlpha(ColourRGBA::FromRGB(colour)); + if (FAILED(pTarget->CreateSolidColorBrush(col, brush.GetAddressOf()))) { + return {}; + } + return brush; +} + +Geometry GeometryCreate() noexcept { + Geometry geometry; + if (FAILED(pD2DFactory->CreatePathGeometry(geometry.GetAddressOf()))) { + return {}; + } + return geometry; +} + +GeometrySink GeometrySinkCreate(ID2D1PathGeometry *geometry) noexcept { + GeometrySink sink; + if (FAILED(geometry->Open(sink.GetAddressOf()))) { + return {}; + } + return sink; +} + +StrokeStyle StrokeStyleCreate(const D2D1_STROKE_STYLE_PROPERTIES &strokeStyleProperties) noexcept { + StrokeStyle strokeStyle; + const HRESULT hr = pD2DFactory->CreateStrokeStyle( + strokeStyleProperties, nullptr, 0, strokeStyle.GetAddressOf()); + if (FAILED(hr)) { + return {}; + } + return strokeStyle; +} + +TextLayout LayoutCreate(std::wstring_view wsv, IDWriteTextFormat *pTextFormat, FLOAT maxWidth, FLOAT maxHeight) noexcept { + TextLayout layout; + const HRESULT hr = pIDWriteFactory->CreateTextLayout(wsv.data(), static_cast<UINT32>(wsv.length()), + pTextFormat, maxWidth, maxHeight, layout.GetAddressOf()); + if (FAILED(hr)) { + return {}; + } + return layout; +} + +#endif + +std::shared_ptr<Font> Font::Allocate(const FontParameters &fp) { +#if defined(USE_D2D) + if (fp.technology != Technology::Default) { + return std::make_shared<FontDirectWrite>(fp); + } +#endif + return FontGDI_Allocate(fp); +} + +std::unique_ptr<Surface> Surface::Allocate([[maybe_unused]] Technology technology) { +#if defined(USE_D2D) + if (technology != Technology::Default) { + return std::make_unique<SurfaceD2D>(); + } +#endif + return SurfaceGDI_Allocate(); +} + +} |