Refine XCEditor docking and DPI rendering

This commit is contained in:
2026-04-11 17:07:37 +08:00
parent 35d3d6328b
commit 2958dcc491
46 changed files with 4839 additions and 471 deletions

View File

@@ -8,9 +8,8 @@ namespace XCEngine::UI::Editor::Host {
namespace {
D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect) {
return D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
}
constexpr float kBaseDpi = 96.0f;
constexpr float kDefaultFontSize = 16.0f;
std::string HrToString(const char* operation, HRESULT hr) {
char buffer[128] = {};
@@ -18,6 +17,27 @@ std::string HrToString(const char* operation, HRESULT 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);
}
} // namespace
bool NativeRenderer::Initialize(HWND hwnd) {
@@ -64,6 +84,17 @@ void NativeRenderer::Shutdown() {
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;
@@ -104,6 +135,52 @@ const std::string& NativeRenderer::GetLastRenderError() const {
return m_lastRenderError;
}
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<IDWriteTextLayout> textLayout;
HRESULT hr = m_dwriteFactory->CreateTextLayout(
text.c_str(),
static_cast<UINT32>(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,
@@ -146,7 +223,9 @@ bool NativeRenderer::CaptureToPng(
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties(
D2D1_RENDER_TARGET_TYPE_DEFAULT,
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED));
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
kBaseDpi,
kBaseDpi);
Microsoft::WRL::ComPtr<ID2D1RenderTarget> offscreenRenderTarget;
hr = m_d2dFactory->CreateWicBitmapRenderTarget(
@@ -301,7 +380,11 @@ bool NativeRenderer::CreateDeviceResources() {
const UINT width = static_cast<UINT>((std::max)(clientRect.right - clientRect.left, 1L));
const UINT height = static_cast<UINT>((std::max)(clientRect.bottom - clientRect.top, 1L));
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties();
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));
@@ -324,6 +407,7 @@ bool NativeRenderer::CreateDeviceResources() {
return false;
}
m_renderTarget->SetDpi(kBaseDpi, kBaseDpi);
m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
m_lastRenderError.clear();
return true;
@@ -333,6 +417,7 @@ 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));
@@ -358,13 +443,15 @@ void NativeRenderer::RenderCommand(
const ::XCEngine::UI::UIDrawCommand& command,
std::vector<D2D1_RECT_F>& 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);
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, command.rounding, command.rounding),
D2D1::RoundedRect(rect, rounding, rounding),
&solidBrush);
} else {
renderTarget.FillRectangle(rect, &solidBrush);
@@ -372,11 +459,12 @@ void NativeRenderer::RenderCommand(
break;
}
case ::XCEngine::UI::UIDrawCommandType::RectOutline: {
const D2D1_RECT_F rect = ToD2DRect(command.rect);
const float thickness = command.thickness > 0.0f ? command.thickness : 1.0f;
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, command.rounding, command.rounding),
D2D1::RoundedRect(rect, rounding, rounding),
&solidBrush,
thickness);
} else {
@@ -389,8 +477,9 @@ void NativeRenderer::RenderCommand(
break;
}
const float fontSize = command.fontSize > 0.0f ? command.fontSize : 16.0f;
IDWriteTextFormat* textFormat = GetTextFormat(fontSize);
const float fontSize = ResolveFontSize(command.fontSize);
const float scaledFontSize = fontSize * dpiScale;
IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize);
if (textFormat == nullptr) {
break;
}
@@ -401,11 +490,14 @@ void NativeRenderer::RenderCommand(
}
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(
command.position.x,
command.position.y,
originX,
originY,
targetSize.width,
command.position.y + fontSize * 1.8f);
originY + lineHeight);
renderTarget.DrawTextW(
text.c_str(),
static_cast<UINT32>(text.size()),
@@ -413,7 +505,7 @@ void NativeRenderer::RenderCommand(
layoutRect,
&solidBrush,
D2D1_DRAW_TEXT_OPTIONS_CLIP,
DWRITE_MEASURING_MODE_NATURAL);
DWRITE_MEASURING_MODE_GDI_NATURAL);
break;
}
case ::XCEngine::UI::UIDrawCommandType::Image: {
@@ -421,12 +513,12 @@ void NativeRenderer::RenderCommand(
break;
}
const D2D1_RECT_F rect = ToD2DRect(command.rect);
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
break;
}
case ::XCEngine::UI::UIDrawCommandType::PushClipRect: {
const D2D1_RECT_F rect = ToD2DRect(command.rect);
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
clipStack.push_back(rect);
break;
@@ -443,12 +535,13 @@ void NativeRenderer::RenderCommand(
}
}
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const {
if (!m_dwriteFactory) {
return nullptr;
}
const int key = static_cast<int>(std::lround(fontSize * 10.0f));
const float resolvedFontSize = ResolveFontSize(fontSize);
const int key = static_cast<int>(std::lround(resolvedFontSize * 10.0f));
const auto found = m_textFormats.find(key);
if (found != m_textFormats.end()) {
return found->second.Get();
@@ -461,7 +554,7 @@ IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
DWRITE_FONT_WEIGHT_REGULAR,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
fontSize,
resolvedFontSize,
L"",
textFormat.ReleaseAndGetAddressOf());
if (FAILED(hr)) {