关键节点
This commit is contained in:
929
editor/app/Rendering/D3D12/D3D12UiTextSystem.cpp
Normal file
929
editor/app/Rendering/D3D12/D3D12UiTextSystem.cpp
Normal file
@@ -0,0 +1,929 @@
|
||||
#include "D3D12UiTextSystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace XCEngine::UI::Editor::Host {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kDefaultFontSize = 16.0f;
|
||||
constexpr float kTextVisualScaleCompensation = 1.08f;
|
||||
|
||||
std::string HrToString(const char* operation, HRESULT hr) {
|
||||
char buffer[128] = {};
|
||||
sprintf_s(buffer, "%s failed with hr=0x%08X.", operation, static_cast<unsigned int>(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 ResolveDisplayFontSize(float fontSize) {
|
||||
return ResolveFontSize(fontSize) * kTextVisualScaleCompensation;
|
||||
}
|
||||
|
||||
struct GlyphRunRecord {
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace> fontFace = {};
|
||||
std::vector<UINT16> glyphIndices = {};
|
||||
std::vector<FLOAT> glyphAdvances = {};
|
||||
std::vector<DWRITE_GLYPH_OFFSET> glyphOffsets = {};
|
||||
FLOAT fontEmSize = 0.0f;
|
||||
FLOAT baselineOriginX = 0.0f;
|
||||
FLOAT baselineOriginY = 0.0f;
|
||||
DWRITE_MEASURING_MODE measuringMode = DWRITE_MEASURING_MODE_NATURAL;
|
||||
BOOL isSideways = FALSE;
|
||||
UINT32 bidiLevel = 0u;
|
||||
};
|
||||
|
||||
class GlyphRunCollector final : public IDWriteTextRenderer {
|
||||
public:
|
||||
explicit GlyphRunCollector(float pixelsPerDip)
|
||||
: m_pixelsPerDip(ClampDpiScale(pixelsPerDip)) {
|
||||
}
|
||||
|
||||
const std::vector<GlyphRunRecord>& GetGlyphRuns() const {
|
||||
return m_glyphRuns;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** object) override {
|
||||
if (object == nullptr) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
*object = nullptr;
|
||||
if (riid == __uuidof(IUnknown) ||
|
||||
riid == __uuidof(IDWritePixelSnapping) ||
|
||||
riid == __uuidof(IDWriteTextRenderer)) {
|
||||
*object = static_cast<IDWriteTextRenderer*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||
return ++m_refCount;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const ULONG refCount = --m_refCount;
|
||||
if (refCount == 0u) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refCount;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE IsPixelSnappingDisabled(
|
||||
void*,
|
||||
BOOL* isDisabled) override {
|
||||
if (isDisabled == nullptr) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
*isDisabled = FALSE;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetCurrentTransform(
|
||||
void*,
|
||||
DWRITE_MATRIX* transform) override {
|
||||
if (transform == nullptr) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
*transform = DWRITE_MATRIX{ 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f };
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetPixelsPerDip(
|
||||
void*,
|
||||
FLOAT* pixelsPerDip) override {
|
||||
if (pixelsPerDip == nullptr) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
*pixelsPerDip = m_pixelsPerDip;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DrawGlyphRun(
|
||||
void*,
|
||||
FLOAT baselineOriginX,
|
||||
FLOAT baselineOriginY,
|
||||
DWRITE_MEASURING_MODE measuringMode,
|
||||
const DWRITE_GLYPH_RUN* glyphRun,
|
||||
const DWRITE_GLYPH_RUN_DESCRIPTION*,
|
||||
IUnknown*) override {
|
||||
if (glyphRun == nullptr ||
|
||||
glyphRun->fontFace == nullptr ||
|
||||
glyphRun->glyphCount == 0u ||
|
||||
glyphRun->glyphIndices == nullptr) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
GlyphRunRecord record = {};
|
||||
record.fontFace = glyphRun->fontFace;
|
||||
record.glyphIndices.assign(
|
||||
glyphRun->glyphIndices,
|
||||
glyphRun->glyphIndices + glyphRun->glyphCount);
|
||||
record.glyphAdvances.resize(glyphRun->glyphCount, 0.0f);
|
||||
record.glyphOffsets.resize(glyphRun->glyphCount, DWRITE_GLYPH_OFFSET{});
|
||||
if (glyphRun->glyphAdvances != nullptr) {
|
||||
std::copy(
|
||||
glyphRun->glyphAdvances,
|
||||
glyphRun->glyphAdvances + glyphRun->glyphCount,
|
||||
record.glyphAdvances.begin());
|
||||
}
|
||||
if (glyphRun->glyphOffsets != nullptr) {
|
||||
std::copy(
|
||||
glyphRun->glyphOffsets,
|
||||
glyphRun->glyphOffsets + glyphRun->glyphCount,
|
||||
record.glyphOffsets.begin());
|
||||
}
|
||||
|
||||
record.fontEmSize = glyphRun->fontEmSize;
|
||||
record.baselineOriginX = baselineOriginX;
|
||||
record.baselineOriginY = baselineOriginY;
|
||||
record.measuringMode = measuringMode;
|
||||
record.isSideways = glyphRun->isSideways;
|
||||
record.bidiLevel = glyphRun->bidiLevel;
|
||||
m_glyphRuns.push_back(std::move(record));
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DrawUnderline(
|
||||
void*,
|
||||
FLOAT,
|
||||
FLOAT,
|
||||
const DWRITE_UNDERLINE*,
|
||||
IUnknown*) override {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DrawStrikethrough(
|
||||
void*,
|
||||
FLOAT,
|
||||
FLOAT,
|
||||
const DWRITE_STRIKETHROUGH*,
|
||||
IUnknown*) override {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DrawInlineObject(
|
||||
void*,
|
||||
FLOAT,
|
||||
FLOAT,
|
||||
IDWriteInlineObject*,
|
||||
BOOL,
|
||||
BOOL,
|
||||
IUnknown*) override {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> m_refCount = 1u;
|
||||
float m_pixelsPerDip = 1.0f;
|
||||
std::vector<GlyphRunRecord> m_glyphRuns = {};
|
||||
};
|
||||
|
||||
void CompositeCoverage(
|
||||
std::vector<std::uint8_t>& rgbaPixels,
|
||||
UINT targetWidth,
|
||||
UINT targetHeight,
|
||||
const RECT& bounds,
|
||||
const std::uint8_t* alphaTexture,
|
||||
std::size_t textureStride,
|
||||
std::size_t channelCount) {
|
||||
if (alphaTexture == nullptr || channelCount == 0u) {
|
||||
return;
|
||||
}
|
||||
|
||||
const LONG width = bounds.right - bounds.left;
|
||||
const LONG height = bounds.bottom - bounds.top;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (LONG row = 0; row < height; ++row) {
|
||||
const LONG dstY = bounds.top + row;
|
||||
if (dstY < 0 || dstY >= static_cast<LONG>(targetHeight)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (LONG column = 0; column < width; ++column) {
|
||||
const LONG dstX = bounds.left + column;
|
||||
if (dstX < 0 || dstX >= static_cast<LONG>(targetWidth)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t srcOffset =
|
||||
static_cast<std::size_t>(row) * textureStride +
|
||||
static_cast<std::size_t>(column) * channelCount;
|
||||
std::uint32_t coverageSum = 0u;
|
||||
for (std::size_t channel = 0u; channel < channelCount; ++channel) {
|
||||
coverageSum += static_cast<std::uint32_t>(alphaTexture[srcOffset + channel]);
|
||||
}
|
||||
const std::uint8_t coverage = static_cast<std::uint8_t>(
|
||||
(coverageSum + static_cast<std::uint32_t>(channelCount / 2u)) /
|
||||
static_cast<std::uint32_t>(channelCount));
|
||||
|
||||
if (coverage == 0u) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t dstOffset =
|
||||
(static_cast<std::size_t>(dstY) * static_cast<std::size_t>(targetWidth) +
|
||||
static_cast<std::size_t>(dstX)) *
|
||||
4u;
|
||||
std::uint8_t& dstAlpha = rgbaPixels[dstOffset + 3u];
|
||||
const std::uint32_t blendedAlpha =
|
||||
static_cast<std::uint32_t>(dstAlpha) +
|
||||
((255u - static_cast<std::uint32_t>(dstAlpha)) *
|
||||
static_cast<std::uint32_t>(coverage) +
|
||||
127u) /
|
||||
255u;
|
||||
dstAlpha = static_cast<std::uint8_t>((std::min)(blendedAlpha, 255u));
|
||||
rgbaPixels[dstOffset + 0u] = 255u;
|
||||
rgbaPixels[dstOffset + 1u] = 255u;
|
||||
rgbaPixels[dstOffset + 2u] = 255u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CreateGlyphRunFromRecord(
|
||||
const GlyphRunRecord& record,
|
||||
DWRITE_GLYPH_RUN& outGlyphRun) {
|
||||
if (record.fontFace == nullptr || record.glyphIndices.empty()) {
|
||||
outGlyphRun = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
outGlyphRun = {};
|
||||
outGlyphRun.fontFace = record.fontFace.Get();
|
||||
outGlyphRun.fontEmSize = record.fontEmSize;
|
||||
outGlyphRun.glyphCount = static_cast<UINT32>(record.glyphIndices.size());
|
||||
outGlyphRun.glyphIndices = record.glyphIndices.data();
|
||||
outGlyphRun.glyphAdvances = record.glyphAdvances.data();
|
||||
outGlyphRun.glyphOffsets =
|
||||
record.glyphOffsets.empty() ? nullptr : record.glyphOffsets.data();
|
||||
outGlyphRun.isSideways = record.isSideways;
|
||||
outGlyphRun.bidiLevel = record.bidiLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AnalyzeGlyphRun(
|
||||
IDWriteFactory& dwriteFactory,
|
||||
const GlyphRunRecord& record,
|
||||
float dpiScale,
|
||||
Microsoft::WRL::ComPtr<IDWriteGlyphRunAnalysis>& outAnalysis,
|
||||
RECT& outBounds,
|
||||
std::string& outError) {
|
||||
outAnalysis.Reset();
|
||||
outBounds = {};
|
||||
outError.clear();
|
||||
|
||||
DWRITE_GLYPH_RUN glyphRun = {};
|
||||
if (!CreateGlyphRunFromRecord(record, glyphRun)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
HRESULT hr = dwriteFactory.CreateGlyphRunAnalysis(
|
||||
&glyphRun,
|
||||
dpiScale,
|
||||
nullptr,
|
||||
DWRITE_RENDERING_MODE_NATURAL,
|
||||
record.measuringMode,
|
||||
record.baselineOriginX,
|
||||
record.baselineOriginY,
|
||||
outAnalysis.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || outAnalysis == nullptr) {
|
||||
outError = HrToString("IDWriteFactory::CreateGlyphRunAnalysis", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = outAnalysis->GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1, &outBounds);
|
||||
if (FAILED(hr)) {
|
||||
outError = HrToString("IDWriteGlyphRunAnalysis::GetAlphaTextureBounds", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HasArea(const RECT& rect) {
|
||||
return rect.right > rect.left && rect.bottom > rect.top;
|
||||
}
|
||||
|
||||
RECT UnionBounds(const RECT& lhs, const RECT& rhs) {
|
||||
RECT result = {};
|
||||
result.left = (std::min)(lhs.left, rhs.left);
|
||||
result.top = (std::min)(lhs.top, rhs.top);
|
||||
result.right = (std::max)(lhs.right, rhs.right);
|
||||
result.bottom = (std::max)(lhs.bottom, rhs.bottom);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool D3D12UiTextSystem::Initialize() {
|
||||
Shutdown();
|
||||
|
||||
const HRESULT hr = DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE_SHARED,
|
||||
__uuidof(IDWriteFactory),
|
||||
reinterpret_cast<IUnknown**>(m_dwriteFactory.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr)) {
|
||||
const std::string error = HrToString("DWriteCreateFactory", hr);
|
||||
Shutdown();
|
||||
m_lastError = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_lastError.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3D12UiTextSystem::Shutdown() {
|
||||
m_textFormats.clear();
|
||||
m_dwriteFactory.Reset();
|
||||
m_lastError.clear();
|
||||
}
|
||||
|
||||
void D3D12UiTextSystem::SetDpiScale(float dpiScale) {
|
||||
m_dpiScale = ClampDpiScale(dpiScale);
|
||||
}
|
||||
|
||||
float D3D12UiTextSystem::GetDpiScale() const {
|
||||
return m_dpiScale;
|
||||
}
|
||||
|
||||
const std::string& D3D12UiTextSystem::GetLastError() const {
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
bool D3D12UiTextSystem::ShapeTextRun(
|
||||
std::string_view text,
|
||||
float fontSize,
|
||||
ShapedTextRun& outRun,
|
||||
std::string& outError) const {
|
||||
outRun = {};
|
||||
outError.clear();
|
||||
|
||||
if (!m_dwriteFactory) {
|
||||
outError = "ShapeTextRun requires an initialized DirectWrite factory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::wstring wideText = Utf8ToWide(text);
|
||||
if (wideText.empty()) {
|
||||
outError = "ShapeTextRun could not convert UTF-8 text to UTF-16.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const float dpiScale = ClampDpiScale(m_dpiScale);
|
||||
const float resolvedFontSize = ResolveDisplayFontSize(fontSize);
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(resolvedFontSize);
|
||||
if (textFormat == nullptr) {
|
||||
outError = "ShapeTextRun could not resolve the DirectWrite text format.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteTextLayout> textLayout = {};
|
||||
HRESULT hr = m_dwriteFactory->CreateTextLayout(
|
||||
wideText.c_str(),
|
||||
static_cast<UINT32>(wideText.size()),
|
||||
textFormat,
|
||||
4096.0f,
|
||||
resolvedFontSize * 2.0f,
|
||||
textLayout.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || textLayout == nullptr) {
|
||||
outError = HrToString("IDWriteFactory::CreateTextLayout", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
DWRITE_TEXT_METRICS textMetrics = {};
|
||||
hr = textLayout->GetMetrics(&textMetrics);
|
||||
if (FAILED(hr)) {
|
||||
outError = HrToString("IDWriteTextLayout::GetMetrics", hr);
|
||||
return false;
|
||||
}
|
||||
outRun.width = textMetrics.widthIncludingTrailingWhitespace;
|
||||
outRun.height = (std::max)(textMetrics.height, resolvedFontSize * 1.6f);
|
||||
|
||||
GlyphRunCollector* collector = new GlyphRunCollector(dpiScale);
|
||||
hr = textLayout->Draw(
|
||||
nullptr,
|
||||
collector,
|
||||
0.0f,
|
||||
0.0f);
|
||||
const std::vector<GlyphRunRecord> glyphRuns = collector->GetGlyphRuns();
|
||||
collector->Release();
|
||||
if (FAILED(hr)) {
|
||||
outError = HrToString("IDWriteTextLayout::Draw", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const GlyphRunRecord& record : glyphRuns) {
|
||||
if (record.fontFace == nullptr || record.glyphIndices.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float advanceCursor = 0.0f;
|
||||
for (std::size_t glyphIndex = 0u; glyphIndex < record.glyphIndices.size(); ++glyphIndex) {
|
||||
const DWRITE_GLYPH_OFFSET glyphOffset =
|
||||
glyphIndex < record.glyphOffsets.size()
|
||||
? record.glyphOffsets[glyphIndex]
|
||||
: DWRITE_GLYPH_OFFSET{};
|
||||
ShapedGlyph shapedGlyph = {};
|
||||
shapedGlyph.fontFace = record.fontFace;
|
||||
shapedGlyph.glyphIndex = record.glyphIndices[glyphIndex];
|
||||
shapedGlyph.fontEmSize = record.fontEmSize;
|
||||
shapedGlyph.originX =
|
||||
record.baselineOriginX +
|
||||
advanceCursor +
|
||||
glyphOffset.advanceOffset;
|
||||
shapedGlyph.originY =
|
||||
record.baselineOriginY +
|
||||
glyphOffset.ascenderOffset;
|
||||
shapedGlyph.measuringMode = record.measuringMode;
|
||||
shapedGlyph.isSideways = record.isSideways != FALSE;
|
||||
outRun.glyphs.push_back(std::move(shapedGlyph));
|
||||
|
||||
if (glyphIndex < record.glyphAdvances.size()) {
|
||||
advanceCursor += record.glyphAdvances[glyphIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool D3D12UiTextSystem::RasterizeGlyph(
|
||||
const ShapedGlyph& glyph,
|
||||
RasterizedGlyph& outGlyph,
|
||||
std::string& outError) const {
|
||||
outGlyph = {};
|
||||
outError.clear();
|
||||
|
||||
if (!m_dwriteFactory) {
|
||||
outError = "RasterizeGlyph requires an initialized DirectWrite factory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (glyph.fontFace == nullptr) {
|
||||
outError = "RasterizeGlyph requires a valid font face.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const UINT16 glyphIndices[] = { glyph.glyphIndex };
|
||||
const FLOAT glyphAdvances[] = { 0.0f };
|
||||
const DWRITE_GLYPH_OFFSET glyphOffsets[] = { DWRITE_GLYPH_OFFSET{} };
|
||||
|
||||
DWRITE_GLYPH_RUN glyphRun = {};
|
||||
glyphRun.fontFace = glyph.fontFace.Get();
|
||||
glyphRun.fontEmSize = glyph.fontEmSize;
|
||||
glyphRun.glyphCount = 1u;
|
||||
glyphRun.glyphIndices = glyphIndices;
|
||||
glyphRun.glyphAdvances = glyphAdvances;
|
||||
glyphRun.glyphOffsets = glyphOffsets;
|
||||
glyphRun.isSideways = glyph.isSideways ? TRUE : FALSE;
|
||||
glyphRun.bidiLevel = 0u;
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteGlyphRunAnalysis> analysis = {};
|
||||
const float dpiScale = ClampDpiScale(m_dpiScale);
|
||||
HRESULT hr = m_dwriteFactory->CreateGlyphRunAnalysis(
|
||||
&glyphRun,
|
||||
dpiScale,
|
||||
nullptr,
|
||||
DWRITE_RENDERING_MODE_NATURAL,
|
||||
glyph.measuringMode,
|
||||
0.0f,
|
||||
0.0f,
|
||||
analysis.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || analysis == nullptr) {
|
||||
outError = HrToString("IDWriteFactory::CreateGlyphRunAnalysis", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT bounds = {};
|
||||
hr = analysis->GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1, &bounds);
|
||||
if (FAILED(hr)) {
|
||||
outError = HrToString("IDWriteGlyphRunAnalysis::GetAlphaTextureBounds", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
outGlyph.boundsLeft = bounds.left;
|
||||
outGlyph.boundsTop = bounds.top;
|
||||
outGlyph.boundsRight = bounds.right;
|
||||
outGlyph.boundsBottom = bounds.bottom;
|
||||
|
||||
const LONG boundsWidth = bounds.right - bounds.left;
|
||||
const LONG boundsHeight = bounds.bottom - bounds.top;
|
||||
if (boundsWidth <= 0 || boundsHeight <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> alphaTexture(
|
||||
static_cast<std::size_t>(boundsWidth) * static_cast<std::size_t>(boundsHeight) * 3u);
|
||||
hr = analysis->CreateAlphaTexture(
|
||||
DWRITE_TEXTURE_CLEARTYPE_3x1,
|
||||
&bounds,
|
||||
alphaTexture.data(),
|
||||
static_cast<UINT32>(alphaTexture.size()));
|
||||
if (FAILED(hr)) {
|
||||
outError = HrToString("IDWriteGlyphRunAnalysis::CreateAlphaTexture", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
outGlyph.width = static_cast<UINT>(boundsWidth);
|
||||
outGlyph.height = static_cast<UINT>(boundsHeight);
|
||||
outGlyph.rgbaPixels.assign(
|
||||
static_cast<std::size_t>(outGlyph.width) * static_cast<std::size_t>(outGlyph.height) * 4u,
|
||||
0u);
|
||||
CompositeCoverage(
|
||||
outGlyph.rgbaPixels,
|
||||
outGlyph.width,
|
||||
outGlyph.height,
|
||||
RECT{ 0, 0, boundsWidth, boundsHeight },
|
||||
alphaTexture.data(),
|
||||
static_cast<std::size_t>(boundsWidth) * 3u,
|
||||
3u);
|
||||
return true;
|
||||
}
|
||||
|
||||
float D3D12UiTextSystem::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 resolvedFontSize = ResolveDisplayFontSize(request.fontSize);
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(resolvedFontSize);
|
||||
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,
|
||||
resolvedFontSize * 2.0f,
|
||||
textLayout.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || textLayout == nullptr) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
DWRITE_TEXT_METRICS textMetrics = {};
|
||||
hr = textLayout->GetMetrics(&textMetrics);
|
||||
if (FAILED(hr)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
GlyphRunCollector* collector = new GlyphRunCollector(dpiScale);
|
||||
hr = textLayout->Draw(
|
||||
nullptr,
|
||||
collector,
|
||||
0.0f,
|
||||
0.0f);
|
||||
const std::vector<GlyphRunRecord> glyphRuns = collector->GetGlyphRuns();
|
||||
collector->Release();
|
||||
if (FAILED(hr)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
bool hasUnionBounds = false;
|
||||
RECT unionBounds = {};
|
||||
for (const GlyphRunRecord& record : glyphRuns) {
|
||||
Microsoft::WRL::ComPtr<IDWriteGlyphRunAnalysis> analysis = {};
|
||||
RECT bounds = {};
|
||||
std::string error = {};
|
||||
if (!AnalyzeGlyphRun(
|
||||
*m_dwriteFactory.Get(),
|
||||
record,
|
||||
dpiScale,
|
||||
analysis,
|
||||
bounds,
|
||||
error)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
if (!HasArea(bounds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unionBounds = hasUnionBounds ? UnionBounds(unionBounds, bounds) : bounds;
|
||||
hasUnionBounds = true;
|
||||
}
|
||||
|
||||
if (hasUnionBounds) {
|
||||
return static_cast<float>(unionBounds.right - unionBounds.left) / dpiScale;
|
||||
}
|
||||
|
||||
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) / dpiScale;
|
||||
}
|
||||
|
||||
float D3D12UiTextSystem::MeasureTextAdvance(
|
||||
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 resolvedFontSize = ResolveDisplayFontSize(request.fontSize);
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(resolvedFontSize);
|
||||
if (textFormat == nullptr) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteTextLayout> textLayout = {};
|
||||
const HRESULT hr = m_dwriteFactory->CreateTextLayout(
|
||||
text.c_str(),
|
||||
static_cast<UINT32>(text.size()),
|
||||
textFormat,
|
||||
4096.0f,
|
||||
resolvedFontSize * 2.0f,
|
||||
textLayout.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || textLayout == nullptr) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
DWRITE_TEXT_METRICS textMetrics = {};
|
||||
if (FAILED(textLayout->GetMetrics(&textMetrics))) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return std::ceil(textMetrics.widthIncludingTrailingWhitespace * dpiScale) / dpiScale;
|
||||
}
|
||||
|
||||
bool D3D12UiTextSystem::RasterizeTextMask(
|
||||
std::string_view text,
|
||||
float fontSize,
|
||||
RasterizedTextRun& outRun,
|
||||
std::string& outError) {
|
||||
outRun = {};
|
||||
outError.clear();
|
||||
|
||||
if (!m_dwriteFactory) {
|
||||
outError = "RasterizeTextMask requires an initialized DirectWrite factory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.empty()) {
|
||||
outError = "RasterizeTextMask rejected an empty text payload.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring wideText = Utf8ToWide(text);
|
||||
if (wideText.empty()) {
|
||||
outError = "RasterizeTextMask could not convert UTF-8 text to UTF-16.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const float dpiScale = ClampDpiScale(m_dpiScale);
|
||||
const float resolvedFontSize = ResolveDisplayFontSize(fontSize);
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(resolvedFontSize);
|
||||
if (textFormat == nullptr) {
|
||||
outError = "RasterizeTextMask could not resolve the DirectWrite text format.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteTextLayout> textLayout = {};
|
||||
HRESULT hr = m_dwriteFactory->CreateTextLayout(
|
||||
wideText.c_str(),
|
||||
static_cast<UINT32>(wideText.size()),
|
||||
textFormat,
|
||||
4096.0f,
|
||||
resolvedFontSize * 2.0f,
|
||||
textLayout.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || textLayout == nullptr) {
|
||||
outError = HrToString("IDWriteFactory::CreateTextLayout", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
DWRITE_TEXT_METRICS textMetrics = {};
|
||||
hr = textLayout->GetMetrics(&textMetrics);
|
||||
if (FAILED(hr)) {
|
||||
outError = HrToString("IDWriteTextLayout::GetMetrics", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
GlyphRunCollector* collector = new GlyphRunCollector(dpiScale);
|
||||
hr = textLayout->Draw(
|
||||
nullptr,
|
||||
collector,
|
||||
0.0f,
|
||||
0.0f);
|
||||
const std::vector<GlyphRunRecord> glyphRuns = collector->GetGlyphRuns();
|
||||
collector->Release();
|
||||
if (FAILED(hr)) {
|
||||
outError = HrToString("IDWriteTextLayout::Draw", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasUnionBounds = false;
|
||||
RECT unionBounds = {};
|
||||
for (const GlyphRunRecord& record : glyphRuns) {
|
||||
Microsoft::WRL::ComPtr<IDWriteGlyphRunAnalysis> analysis = {};
|
||||
RECT bounds = {};
|
||||
if (!AnalyzeGlyphRun(
|
||||
*m_dwriteFactory.Get(),
|
||||
record,
|
||||
dpiScale,
|
||||
analysis,
|
||||
bounds,
|
||||
outError)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!HasArea(bounds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unionBounds = hasUnionBounds ? UnionBounds(unionBounds, bounds) : bounds;
|
||||
hasUnionBounds = true;
|
||||
}
|
||||
|
||||
if (!hasUnionBounds) {
|
||||
DWRITE_OVERHANG_METRICS overhangMetrics = {};
|
||||
textLayout->GetOverhangMetrics(&overhangMetrics);
|
||||
|
||||
const float leftPad = (std::max)(overhangMetrics.left, 0.0f);
|
||||
const float topPad = (std::max)(overhangMetrics.top, 0.0f);
|
||||
const float rightPad = (std::max)(overhangMetrics.right, 0.0f);
|
||||
const float bottomPad = (std::max)(overhangMetrics.bottom, 0.0f);
|
||||
outRun.width = (std::max)(
|
||||
1u,
|
||||
static_cast<UINT>(std::ceil(
|
||||
(textMetrics.widthIncludingTrailingWhitespace + leftPad + rightPad) * dpiScale)));
|
||||
outRun.height = (std::max)(
|
||||
1u,
|
||||
static_cast<UINT>(std::ceil(
|
||||
((std::max)(textMetrics.height, resolvedFontSize * 1.6f) + topPad + bottomPad) *
|
||||
dpiScale)));
|
||||
outRun.offsetX = -leftPad * dpiScale;
|
||||
outRun.offsetY = -topPad * dpiScale;
|
||||
outRun.rgbaPixels.assign(
|
||||
static_cast<std::size_t>(outRun.width) * static_cast<std::size_t>(outRun.height) * 4u,
|
||||
0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
outRun.width = static_cast<UINT>(unionBounds.right - unionBounds.left);
|
||||
outRun.height = static_cast<UINT>(unionBounds.bottom - unionBounds.top);
|
||||
outRun.offsetX = static_cast<float>(unionBounds.left);
|
||||
outRun.offsetY = static_cast<float>(unionBounds.top);
|
||||
outRun.rgbaPixels.assign(
|
||||
static_cast<std::size_t>(outRun.width) * static_cast<std::size_t>(outRun.height) * 4u,
|
||||
0u);
|
||||
|
||||
for (const GlyphRunRecord& record : glyphRuns) {
|
||||
if (record.fontFace == nullptr || record.glyphIndices.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteGlyphRunAnalysis> analysis = {};
|
||||
RECT bounds = {};
|
||||
if (!AnalyzeGlyphRun(
|
||||
*m_dwriteFactory.Get(),
|
||||
record,
|
||||
dpiScale,
|
||||
analysis,
|
||||
bounds,
|
||||
outError)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const LONG boundsWidth = bounds.right - bounds.left;
|
||||
const LONG boundsHeight = bounds.bottom - bounds.top;
|
||||
if (boundsWidth <= 0 || boundsHeight <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> alphaTexture(
|
||||
static_cast<std::size_t>(boundsWidth) * static_cast<std::size_t>(boundsHeight) * 3u);
|
||||
hr = analysis->CreateAlphaTexture(
|
||||
DWRITE_TEXTURE_CLEARTYPE_3x1,
|
||||
&bounds,
|
||||
alphaTexture.data(),
|
||||
static_cast<UINT32>(alphaTexture.size()));
|
||||
if (FAILED(hr)) {
|
||||
outError = HrToString("IDWriteGlyphRunAnalysis::CreateAlphaTexture", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT localBounds = {};
|
||||
localBounds.left = bounds.left - unionBounds.left;
|
||||
localBounds.top = bounds.top - unionBounds.top;
|
||||
localBounds.right = bounds.right - unionBounds.left;
|
||||
localBounds.bottom = bounds.bottom - unionBounds.top;
|
||||
|
||||
CompositeCoverage(
|
||||
outRun.rgbaPixels,
|
||||
outRun.width,
|
||||
outRun.height,
|
||||
localBounds,
|
||||
alphaTexture.data(),
|
||||
static_cast<std::size_t>(boundsWidth) * 3u,
|
||||
3u);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IDWriteTextFormat* D3D12UiTextSystem::GetTextFormat(float fontSize) const {
|
||||
if (!m_dwriteFactory) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteTextFormat> 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) || textFormat == nullptr) {
|
||||
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;
|
||||
}
|
||||
|
||||
std::wstring D3D12UiTextSystem::Utf8ToWide(std::string_view text) {
|
||||
if (text.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int sizeNeeded = MultiByteToWideChar(
|
||||
CP_UTF8,
|
||||
0,
|
||||
text.data(),
|
||||
static_cast<int>(text.size()),
|
||||
nullptr,
|
||||
0);
|
||||
if (sizeNeeded <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring wideText(static_cast<std::size_t>(sizeNeeded), L'\0');
|
||||
MultiByteToWideChar(
|
||||
CP_UTF8,
|
||||
0,
|
||||
text.data(),
|
||||
static_cast<int>(text.size()),
|
||||
wideText.data(),
|
||||
sizeNeeded);
|
||||
return wideText;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Host
|
||||
Reference in New Issue
Block a user