#include "NativeRenderer.h" #include #include #include #include namespace XCEngine::UI::Editor::Host { namespace { constexpr float kBaseDpi = 96.0f; constexpr float kDefaultFontSize = 16.0f; std::string HrToString(const char* operation, HRESULT hr) { char buffer[128] = {}; sprintf_s(buffer, "%s failed with hr=0x%08X.", operation, static_cast(hr)); return buffer; } float ClampDpiScale(float dpiScale) { return dpiScale > 0.0f ? dpiScale : 1.0f; } float ResolveFontSize(float fontSize) { return fontSize > 0.0f ? fontSize : kDefaultFontSize; } float SnapToPixel(float value, float dpiScale) { const float scale = ClampDpiScale(dpiScale); return std::round(value * scale); } D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect, float dpiScale) { const float left = SnapToPixel(rect.x, dpiScale); const float top = SnapToPixel(rect.y, dpiScale); const float right = SnapToPixel(rect.x + rect.width, dpiScale); const float bottom = SnapToPixel(rect.y + rect.height, dpiScale); return D2D1::RectF(left, top, right, bottom); } D2D1_POINT_2F ToD2DPoint( const ::XCEngine::UI::UIPoint& point, float dpiScale, float pixelOffset = 0.0f) { return D2D1::Point2F( SnapToPixel(point.x, dpiScale) + pixelOffset, SnapToPixel(point.y, dpiScale) + pixelOffset); } float ResolveStrokePixelOffset(float thickness) { const float roundedThickness = std::round(thickness); return std::fmod(roundedThickness, 2.0f) == 1.0f ? 0.5f : 0.0f; } } // namespace bool NativeRenderer::Initialize(HWND hwnd) { Shutdown(); if (hwnd == nullptr) { m_lastRenderError = "Initialize rejected a null hwnd."; return false; } m_hwnd = hwnd; HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, m_d2dFactory.ReleaseAndGetAddressOf()); if (FAILED(hr)) { m_lastRenderError = HrToString("D2D1CreateFactory", hr); Shutdown(); return false; } hr = DWriteCreateFactory( DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(m_dwriteFactory.ReleaseAndGetAddressOf())); if (FAILED(hr)) { m_lastRenderError = HrToString("DWriteCreateFactory", hr); Shutdown(); return false; } m_lastRenderError.clear(); return EnsureRenderTarget(); } void NativeRenderer::Shutdown() { while (!m_liveTextures.empty()) { auto it = m_liveTextures.begin(); delete *it; m_liveTextures.erase(it); } m_textFormats.clear(); m_solidBrush.Reset(); m_renderTarget.Reset(); m_wicFactory.Reset(); m_dwriteFactory.Reset(); m_d2dFactory.Reset(); if (m_wicComInitialized) { CoUninitialize(); m_wicComInitialized = false; } m_hwnd = nullptr; } void NativeRenderer::SetDpiScale(float dpiScale) { m_dpiScale = ClampDpiScale(dpiScale); if (m_renderTarget) { m_renderTarget->SetDpi(kBaseDpi, kBaseDpi); } } float NativeRenderer::GetDpiScale() const { return m_dpiScale; } void NativeRenderer::Resize(UINT width, UINT height) { if (!m_renderTarget || width == 0 || height == 0) { return; } const HRESULT hr = m_renderTarget->Resize(D2D1::SizeU(width, height)); if (hr == D2DERR_RECREATE_TARGET) { DiscardRenderTarget(); } } bool NativeRenderer::Render(const ::XCEngine::UI::UIDrawData& drawData) { if (!EnsureRenderTarget()) { if (m_lastRenderError.empty()) { m_lastRenderError = "EnsureRenderTarget failed."; } return false; } const bool rendered = RenderToTarget(*m_renderTarget.Get(), *m_solidBrush.Get(), drawData); const HRESULT hr = m_renderTarget->EndDraw(); if (hr == D2DERR_RECREATE_TARGET) { m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr); DiscardRenderTarget(); return false; } if (!rendered || FAILED(hr)) { m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr); return false; } m_lastRenderError.clear(); return true; } const std::string& NativeRenderer::GetLastRenderError() const { return m_lastRenderError; } bool NativeRenderer::LoadTextureFromFile( const std::filesystem::path& path, ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) { outError.clear(); ReleaseTexture(outTexture); auto texture = std::make_unique(); if (!DecodeTextureFile(path, *texture, outError)) { outTexture = {}; return false; } outTexture.nativeHandle = reinterpret_cast(texture.get()); outTexture.width = texture->width; outTexture.height = texture->height; outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle; m_liveTextures.insert(texture.get()); texture.release(); return true; } void NativeRenderer::ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) { if (!texture.IsValid()) { texture = {}; return; } auto* resource = reinterpret_cast(texture.nativeHandle); if (resource != nullptr) { const auto found = m_liveTextures.find(resource); if (found != m_liveTextures.end()) { m_liveTextures.erase(found); delete resource; } } texture = {}; } float NativeRenderer::MeasureTextWidth( const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const { if (!m_dwriteFactory || request.text.empty()) { return 0.0f; } const std::wstring text = Utf8ToWide(request.text); if (text.empty()) { return 0.0f; } const float dpiScale = ClampDpiScale(m_dpiScale); const float scaledFontSize = ResolveFontSize(request.fontSize) * dpiScale; IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); if (textFormat == nullptr) { return 0.0f; } Microsoft::WRL::ComPtr textLayout; HRESULT hr = m_dwriteFactory->CreateTextLayout( text.c_str(), static_cast(text.size()), textFormat, 4096.0f, scaledFontSize * 2.0f, textLayout.ReleaseAndGetAddressOf()); if (FAILED(hr) || !textLayout) { return 0.0f; } DWRITE_TEXT_METRICS textMetrics = {}; hr = textLayout->GetMetrics(&textMetrics); if (FAILED(hr)) { return 0.0f; } DWRITE_OVERHANG_METRICS overhangMetrics = {}; float width = textMetrics.widthIncludingTrailingWhitespace; if (SUCCEEDED(textLayout->GetOverhangMetrics(&overhangMetrics))) { width += (std::max)(overhangMetrics.left, 0.0f); width += (std::max)(overhangMetrics.right, 0.0f); } return std::ceil(width) / dpiScale; } bool NativeRenderer::CaptureToPng( const ::XCEngine::UI::UIDrawData& drawData, UINT width, UINT height, const std::filesystem::path& outputPath, std::string& outError) { outError.clear(); if (width == 0 || height == 0) { outError = "CaptureToPng rejected an empty render size."; return false; } if (!m_d2dFactory || !m_dwriteFactory) { outError = "CaptureToPng requires an initialized NativeRenderer."; return false; } if (!EnsureWicFactory(outError)) { return false; } std::error_code errorCode = {}; std::filesystem::create_directories(outputPath.parent_path(), errorCode); if (errorCode) { outError = "Failed to create screenshot directory: " + outputPath.parent_path().string(); return false; } Microsoft::WRL::ComPtr bitmap; HRESULT hr = m_wicFactory->CreateBitmap( width, height, GUID_WICPixelFormat32bppPBGRA, WICBitmapCacheOnLoad, bitmap.ReleaseAndGetAddressOf()); if (FAILED(hr)) { outError = HrToString("IWICImagingFactory::CreateBitmap", hr); return false; } const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties( D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), kBaseDpi, kBaseDpi); Microsoft::WRL::ComPtr offscreenRenderTarget; hr = m_d2dFactory->CreateWicBitmapRenderTarget( bitmap.Get(), renderTargetProperties, offscreenRenderTarget.ReleaseAndGetAddressOf()); if (FAILED(hr)) { outError = HrToString("ID2D1Factory::CreateWicBitmapRenderTarget", hr); return false; } Microsoft::WRL::ComPtr offscreenBrush; hr = offscreenRenderTarget->CreateSolidColorBrush( D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), offscreenBrush.ReleaseAndGetAddressOf()); if (FAILED(hr)) { outError = HrToString("ID2D1RenderTarget::CreateSolidColorBrush", hr); return false; } const bool rendered = RenderToTarget(*offscreenRenderTarget.Get(), *offscreenBrush.Get(), drawData); hr = offscreenRenderTarget->EndDraw(); if (!rendered || FAILED(hr)) { outError = HrToString("ID2D1RenderTarget::EndDraw", hr); return false; } const std::wstring wideOutputPath = outputPath.wstring(); Microsoft::WRL::ComPtr stream; hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf()); if (FAILED(hr)) { outError = HrToString("IWICImagingFactory::CreateStream", hr); return false; } hr = stream->InitializeFromFilename(wideOutputPath.c_str(), GENERIC_WRITE); if (FAILED(hr)) { outError = HrToString("IWICStream::InitializeFromFilename", hr); return false; } Microsoft::WRL::ComPtr encoder; hr = m_wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.ReleaseAndGetAddressOf()); if (FAILED(hr)) { outError = HrToString("IWICImagingFactory::CreateEncoder", hr); return false; } hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); if (FAILED(hr)) { outError = HrToString("IWICBitmapEncoder::Initialize", hr); return false; } Microsoft::WRL::ComPtr frame; Microsoft::WRL::ComPtr propertyBag; hr = encoder->CreateNewFrame(frame.ReleaseAndGetAddressOf(), propertyBag.ReleaseAndGetAddressOf()); if (FAILED(hr)) { outError = HrToString("IWICBitmapEncoder::CreateNewFrame", hr); return false; } hr = frame->Initialize(propertyBag.Get()); if (FAILED(hr)) { outError = HrToString("IWICBitmapFrameEncode::Initialize", hr); return false; } hr = frame->SetSize(width, height); if (FAILED(hr)) { outError = HrToString("IWICBitmapFrameEncode::SetSize", hr); return false; } WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppPBGRA; hr = frame->SetPixelFormat(&pixelFormat); if (FAILED(hr)) { outError = HrToString("IWICBitmapFrameEncode::SetPixelFormat", hr); return false; } hr = frame->WriteSource(bitmap.Get(), nullptr); if (FAILED(hr)) { outError = HrToString("IWICBitmapFrameEncode::WriteSource", hr); return false; } hr = frame->Commit(); if (FAILED(hr)) { outError = HrToString("IWICBitmapFrameEncode::Commit", hr); return false; } hr = encoder->Commit(); if (FAILED(hr)) { outError = HrToString("IWICBitmapEncoder::Commit", hr); return false; } return true; } bool NativeRenderer::EnsureRenderTarget() { if (!m_hwnd || !m_d2dFactory || !m_dwriteFactory) { m_lastRenderError = "EnsureRenderTarget requires hwnd, D2D factory, and DWrite factory."; return false; } return CreateDeviceResources(); } bool NativeRenderer::EnsureWicFactory(std::string& outError) { outError.clear(); if (m_wicFactory) { return true; } const HRESULT initHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) { outError = HrToString("CoInitializeEx", initHr); return false; } if (SUCCEEDED(initHr)) { m_wicComInitialized = true; } const HRESULT factoryHr = CoCreateInstance( CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(m_wicFactory.ReleaseAndGetAddressOf())); if (FAILED(factoryHr)) { outError = HrToString("CoCreateInstance(CLSID_WICImagingFactory)", factoryHr); return false; } return true; } void NativeRenderer::DiscardRenderTarget() { InvalidateCachedTextureBitmaps(m_renderTarget.Get()); m_solidBrush.Reset(); m_renderTarget.Reset(); } bool NativeRenderer::CreateDeviceResources() { if (m_renderTarget) { return true; } RECT clientRect = {}; GetClientRect(m_hwnd, &clientRect); const UINT width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); const UINT height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties( D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), kBaseDpi, kBaseDpi); const D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties( m_hwnd, D2D1::SizeU(width, height)); const HRESULT renderTargetHr = m_d2dFactory->CreateHwndRenderTarget( renderTargetProps, hwndProps, m_renderTarget.ReleaseAndGetAddressOf()); if (FAILED(renderTargetHr)) { m_lastRenderError = HrToString("ID2D1Factory::CreateHwndRenderTarget", renderTargetHr); return false; } const HRESULT brushHr = m_renderTarget->CreateSolidColorBrush( D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), m_solidBrush.ReleaseAndGetAddressOf()); if (FAILED(brushHr)) { m_lastRenderError = HrToString("ID2D1HwndRenderTarget::CreateSolidColorBrush", brushHr); DiscardRenderTarget(); return false; } m_renderTarget->SetDpi(kBaseDpi, kBaseDpi); m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); m_lastRenderError.clear(); return true; } void NativeRenderer::InvalidateCachedTextureBitmaps(const ID2D1RenderTarget* renderTarget) { for (NativeTextureResource* texture : m_liveTextures) { if (texture == nullptr) { continue; } if (renderTarget == nullptr || texture->cachedTarget == renderTarget) { texture->cachedBitmap.Reset(); texture->cachedTarget = nullptr; } } } bool NativeRenderer::DecodeTextureFile( const std::filesystem::path& path, NativeTextureResource& outTexture, std::string& outError) { outError.clear(); if (!EnsureWicFactory(outError)) { return false; } const std::wstring widePath = path.wstring(); Microsoft::WRL::ComPtr decoder; HRESULT hr = m_wicFactory->CreateDecoderFromFilename( widePath.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnLoad, decoder.ReleaseAndGetAddressOf()); if (FAILED(hr) || !decoder) { outError = HrToString("IWICImagingFactory::CreateDecoderFromFilename", hr); return false; } Microsoft::WRL::ComPtr frame; hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf()); if (FAILED(hr) || !frame) { outError = HrToString("IWICBitmapDecoder::GetFrame", hr); return false; } Microsoft::WRL::ComPtr converter; hr = m_wicFactory->CreateFormatConverter(converter.ReleaseAndGetAddressOf()); if (FAILED(hr) || !converter) { outError = HrToString("IWICImagingFactory::CreateFormatConverter", hr); return false; } hr = converter->Initialize( frame.Get(), GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, nullptr, 0.0f, WICBitmapPaletteTypeCustom); if (FAILED(hr)) { outError = HrToString("IWICFormatConverter::Initialize", hr); return false; } UINT width = 0u; UINT height = 0u; hr = converter->GetSize(&width, &height); if (FAILED(hr) || width == 0u || height == 0u) { outError = HrToString("IWICBitmapSource::GetSize", hr); return false; } std::vector pixels( static_cast(width) * static_cast(height) * 4u); hr = converter->CopyPixels( nullptr, width * 4u, static_cast(pixels.size()), pixels.data()); if (FAILED(hr)) { outError = HrToString("IWICBitmapSource::CopyPixels", hr); return false; } outTexture.pixels = std::move(pixels); outTexture.width = width; outTexture.height = height; outTexture.cachedBitmap.Reset(); outTexture.cachedTarget = nullptr; return true; } bool NativeRenderer::ResolveTextureBitmap( ID2D1RenderTarget& renderTarget, NativeTextureResource& texture, Microsoft::WRL::ComPtr& outBitmap) { outBitmap.Reset(); if (texture.width == 0u || texture.height == 0u || texture.pixels.empty()) { return false; } if (texture.cachedBitmap && texture.cachedTarget == &renderTarget) { outBitmap = texture.cachedBitmap; return true; } Microsoft::WRL::ComPtr bitmap; const D2D1_BITMAP_PROPERTIES properties = D2D1::BitmapProperties( D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), kBaseDpi, kBaseDpi); const HRESULT hr = renderTarget.CreateBitmap( D2D1::SizeU(texture.width, texture.height), texture.pixels.data(), texture.width * 4u, &properties, bitmap.ReleaseAndGetAddressOf()); if (FAILED(hr) || !bitmap) { return false; } if (&renderTarget == m_renderTarget.Get()) { texture.cachedBitmap = bitmap; texture.cachedTarget = &renderTarget; } outBitmap = std::move(bitmap); return true; } bool NativeRenderer::RenderToTarget( ID2D1RenderTarget& renderTarget, ID2D1SolidColorBrush& solidBrush, const ::XCEngine::UI::UIDrawData& drawData) { renderTarget.SetDpi(kBaseDpi, kBaseDpi); renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); renderTarget.BeginDraw(); renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f)); std::vector clipStack = {}; for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { for (const ::XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) { RenderCommand(renderTarget, solidBrush, command, clipStack); } } while (!clipStack.empty()) { renderTarget.PopAxisAlignedClip(); clipStack.pop_back(); } return true; } void NativeRenderer::RenderCommand( ID2D1RenderTarget& renderTarget, ID2D1SolidColorBrush& solidBrush, const ::XCEngine::UI::UIDrawCommand& command, std::vector& clipStack) { solidBrush.SetColor(ToD2DColor(command.color)); const float dpiScale = ClampDpiScale(m_dpiScale); switch (command.type) { case ::XCEngine::UI::UIDrawCommandType::FilledRect: { const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; if (command.rounding > 0.0f) { renderTarget.FillRoundedRectangle( D2D1::RoundedRect(rect, rounding, rounding), &solidBrush); } else { renderTarget.FillRectangle(rect, &solidBrush); } break; } case ::XCEngine::UI::UIDrawCommandType::FilledRectLinearGradient: { const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; const D2D1_GRADIENT_STOP stops[2] = { D2D1::GradientStop(0.0f, ToD2DColor(command.color)), D2D1::GradientStop(1.0f, ToD2DColor(command.secondaryColor)) }; Microsoft::WRL::ComPtr stopCollection; HRESULT hr = renderTarget.CreateGradientStopCollection( stops, 2u, stopCollection.ReleaseAndGetAddressOf()); if (FAILED(hr) || !stopCollection) { break; } const D2D1_POINT_2F startPoint = command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical ? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.top) : D2D1::Point2F(rect.left, (rect.top + rect.bottom) * 0.5f); const D2D1_POINT_2F endPoint = command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical ? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.bottom) : D2D1::Point2F(rect.right, (rect.top + rect.bottom) * 0.5f); Microsoft::WRL::ComPtr gradientBrush; hr = renderTarget.CreateLinearGradientBrush( D2D1::LinearGradientBrushProperties(startPoint, endPoint), stopCollection.Get(), gradientBrush.ReleaseAndGetAddressOf()); if (FAILED(hr) || !gradientBrush) { break; } if (command.rounding > 0.0f) { renderTarget.FillRoundedRectangle( D2D1::RoundedRect(rect, rounding, rounding), gradientBrush.Get()); } else { renderTarget.FillRectangle(rect, gradientBrush.Get()); } break; } case ::XCEngine::UI::UIDrawCommandType::RectOutline: { const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; if (command.rounding > 0.0f) { renderTarget.DrawRoundedRectangle( D2D1::RoundedRect(rect, rounding, rounding), &solidBrush, thickness); } else { renderTarget.DrawRectangle(rect, &solidBrush, thickness); } break; } case ::XCEngine::UI::UIDrawCommandType::Line: { const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; const float pixelOffset = ResolveStrokePixelOffset(thickness); const D2D1_POINT_2F start = ToD2DPoint(command.position, dpiScale, pixelOffset); const D2D1_POINT_2F end = ToD2DPoint(command.uvMin, dpiScale, pixelOffset); renderTarget.DrawLine(start, end, &solidBrush, thickness); break; } case ::XCEngine::UI::UIDrawCommandType::FilledTriangle: { Microsoft::WRL::ComPtr geometry; HRESULT hr = m_d2dFactory->CreatePathGeometry(geometry.ReleaseAndGetAddressOf()); if (FAILED(hr) || !geometry) { break; } Microsoft::WRL::ComPtr sink; hr = geometry->Open(sink.ReleaseAndGetAddressOf()); if (FAILED(hr) || !sink) { break; } const D2D1_POINT_2F a = ToD2DPoint(command.position, dpiScale); const D2D1_POINT_2F b = ToD2DPoint(command.uvMin, dpiScale); const D2D1_POINT_2F c = ToD2DPoint(command.uvMax, dpiScale); const D2D1_POINT_2F points[2] = { b, c }; sink->BeginFigure(a, D2D1_FIGURE_BEGIN_FILLED); sink->AddLines(points, 2u); sink->EndFigure(D2D1_FIGURE_END_CLOSED); hr = sink->Close(); if (FAILED(hr)) { break; } renderTarget.FillGeometry(geometry.Get(), &solidBrush); break; } case ::XCEngine::UI::UIDrawCommandType::FilledCircle: { const float radius = command.radius * dpiScale; renderTarget.FillEllipse( D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius), &solidBrush); break; } case ::XCEngine::UI::UIDrawCommandType::CircleOutline: { const float radius = command.radius * dpiScale; const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; renderTarget.DrawEllipse( D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius), &solidBrush, thickness); break; } case ::XCEngine::UI::UIDrawCommandType::Text: { if (command.text.empty()) { break; } const float fontSize = ResolveFontSize(command.fontSize); const float scaledFontSize = fontSize * dpiScale; IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); if (textFormat == nullptr) { break; } const std::wstring text = Utf8ToWide(command.text); if (text.empty()) { break; } const D2D1_SIZE_F targetSize = renderTarget.GetSize(); const float originX = SnapToPixel(command.position.x, dpiScale); const float originY = SnapToPixel(command.position.y, dpiScale); const float lineHeight = std::ceil(scaledFontSize * 1.6f); const D2D1_RECT_F layoutRect = D2D1::RectF( originX, originY, targetSize.width, originY + lineHeight); renderTarget.DrawTextW( text.c_str(), static_cast(text.size()), textFormat, layoutRect, &solidBrush, D2D1_DRAW_TEXT_OPTIONS_CLIP, DWRITE_MEASURING_MODE_GDI_NATURAL); break; } case ::XCEngine::UI::UIDrawCommandType::Image: { if (!command.texture.IsValid()) { break; } auto* texture = reinterpret_cast(command.texture.nativeHandle); if (texture == nullptr || m_liveTextures.find(texture) == m_liveTextures.end()) { const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); renderTarget.DrawRectangle(rect, &solidBrush, 1.0f); break; } Microsoft::WRL::ComPtr bitmap; if (!ResolveTextureBitmap(renderTarget, *texture, bitmap) || !bitmap) { break; } const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); const float sourceLeft = static_cast(texture->width) * std::clamp(command.uvMin.x, 0.0f, 1.0f); const float sourceTop = static_cast(texture->height) * std::clamp(command.uvMin.y, 0.0f, 1.0f); const float sourceRight = static_cast(texture->width) * std::clamp(command.uvMax.x, 0.0f, 1.0f); const float sourceBottom = static_cast(texture->height) * std::clamp(command.uvMax.y, 0.0f, 1.0f); renderTarget.DrawBitmap( bitmap.Get(), rect, std::clamp(command.color.a, 0.0f, 1.0f), D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, D2D1::RectF(sourceLeft, sourceTop, sourceRight, sourceBottom)); break; } case ::XCEngine::UI::UIDrawCommandType::PushClipRect: { const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); clipStack.push_back(rect); break; } case ::XCEngine::UI::UIDrawCommandType::PopClipRect: { if (!clipStack.empty()) { renderTarget.PopAxisAlignedClip(); clipStack.pop_back(); } break; } default: break; } } IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const { if (!m_dwriteFactory) { return nullptr; } const float resolvedFontSize = ResolveFontSize(fontSize); const int key = static_cast(std::lround(resolvedFontSize * 10.0f)); const auto found = m_textFormats.find(key); if (found != m_textFormats.end()) { return found->second.Get(); } Microsoft::WRL::ComPtr textFormat; const HRESULT hr = m_dwriteFactory->CreateTextFormat( L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_REGULAR, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, resolvedFontSize, L"", textFormat.ReleaseAndGetAddressOf()); if (FAILED(hr)) { return nullptr; } textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); IDWriteTextFormat* result = textFormat.Get(); m_textFormats.emplace(key, std::move(textFormat)); return result; } D2D1_COLOR_F NativeRenderer::ToD2DColor(const ::XCEngine::UI::UIColor& color) { return D2D1::ColorF(color.r, color.g, color.b, color.a); } std::wstring NativeRenderer::Utf8ToWide(std::string_view text) { if (text.empty()) { return {}; } const int sizeNeeded = MultiByteToWideChar( CP_UTF8, 0, text.data(), static_cast(text.size()), nullptr, 0); if (sizeNeeded <= 0) { return {}; } std::wstring wideText(static_cast(sizeNeeded), L'\0'); MultiByteToWideChar( CP_UTF8, 0, text.data(), static_cast(text.size()), wideText.data(), sizeNeeded); return wideText; } } // namespace XCEngine::UI::Editor::Host