De-ImGui XCUI standalone text atlas provider
This commit is contained in:
@@ -1,6 +1,21 @@
|
||||
#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h"
|
||||
|
||||
#include <imgui_internal.h>
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <wingdi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -8,65 +23,688 @@ namespace XCUIBackend {
|
||||
|
||||
namespace {
|
||||
|
||||
IXCUITextAtlasProvider::FontHandle MakeFontHandle(const ImFont* font) {
|
||||
constexpr float kDefaultNominalFontSize = 18.0f;
|
||||
constexpr int kAtlasPadding = 1;
|
||||
constexpr int kMaxAtlasDimension = 4096;
|
||||
constexpr std::size_t kInvalidFontIndex = (std::numeric_limits<std::size_t>::max)();
|
||||
constexpr wchar_t kPrimaryFontFace[] = L"Segoe UI";
|
||||
constexpr wchar_t kFallbackFontFace[] = L"Microsoft YaHei";
|
||||
constexpr std::array<int, 5> kSupportedFontSizes = { 13, 14, 16, 18, 20 };
|
||||
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 2> kPrebakedCodepointRanges = {{
|
||||
{ 0x0020u, 0x007Eu },
|
||||
{ 0x00A0u, 0x00FFu },
|
||||
}};
|
||||
|
||||
struct CachedPixelBuffer {
|
||||
std::vector<unsigned char> bytes = {};
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int bytesPerPixel = 0;
|
||||
|
||||
void Clear() {
|
||||
bytes.clear();
|
||||
width = 0;
|
||||
height = 0;
|
||||
bytesPerPixel = 0;
|
||||
}
|
||||
|
||||
bool IsValid() const {
|
||||
return !bytes.empty() && width > 0 && height > 0 && bytesPerPixel > 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct GlyphRecord {
|
||||
int sizeKey = 0;
|
||||
std::uint32_t codepoint = 0u;
|
||||
bool visible = false;
|
||||
bool colored = false;
|
||||
float advanceX = 0.0f;
|
||||
float x0 = 0.0f;
|
||||
float y0 = 0.0f;
|
||||
float x1 = 0.0f;
|
||||
float y1 = 0.0f;
|
||||
int bitmapWidth = 0;
|
||||
int bitmapHeight = 0;
|
||||
int atlasX = 0;
|
||||
int atlasY = 0;
|
||||
std::vector<unsigned char> alpha = {};
|
||||
};
|
||||
|
||||
struct BakedFontRecord {
|
||||
int sizeKey = 0;
|
||||
IXCUITextAtlasProvider::BakedFontInfo metrics = {};
|
||||
std::unordered_map<std::uint32_t, IXCUITextAtlasProvider::GlyphInfo> glyphs = {};
|
||||
};
|
||||
|
||||
IXCUITextAtlasProvider::FontHandle MakeDefaultFontHandle() {
|
||||
IXCUITextAtlasProvider::FontHandle handle = {};
|
||||
handle.value = reinterpret_cast<std::uintptr_t>(font);
|
||||
handle.value = 1u;
|
||||
return handle;
|
||||
}
|
||||
|
||||
ImFont* ResolveFontHandle(IXCUITextAtlasProvider::FontHandle handle) {
|
||||
return reinterpret_cast<ImFont*>(handle.value);
|
||||
bool IsDefaultFontHandle(IXCUITextAtlasProvider::FontHandle handle) {
|
||||
return handle.value == MakeDefaultFontHandle().value;
|
||||
}
|
||||
|
||||
float ResolveNominalFontSize(const ImFont& font) {
|
||||
return font.LegacySize > 0.0f ? font.LegacySize : 16.0f;
|
||||
std::size_t ResolveFontIndex(IXCUITextAtlasProvider::FontHandle handle) {
|
||||
return IsDefaultFontHandle(handle) ? 0u : kInvalidFontIndex;
|
||||
}
|
||||
|
||||
float ResolveRequestedFontSize(const ImFont& font, float requestedFontSize) {
|
||||
return requestedFontSize > 0.0f ? requestedFontSize : ResolveNominalFontSize(font);
|
||||
std::uint64_t MakeGlyphKey(int sizeKey, std::uint32_t codepoint) {
|
||||
return (static_cast<std::uint64_t>(static_cast<std::uint32_t>(sizeKey)) << 32u) |
|
||||
static_cast<std::uint64_t>(codepoint);
|
||||
}
|
||||
|
||||
bool IsFontOwnedByAtlas(const ImFont* font, const ImFontAtlas* atlas) {
|
||||
return font != nullptr && atlas != nullptr && font->OwnerAtlas == atlas;
|
||||
void AppendCodepointRange(
|
||||
std::vector<std::uint32_t>& outCodepoints,
|
||||
std::uint32_t first,
|
||||
std::uint32_t last) {
|
||||
if (first > last) {
|
||||
return;
|
||||
}
|
||||
|
||||
outCodepoints.reserve(outCodepoints.size() + static_cast<std::size_t>(last - first + 1u));
|
||||
for (std::uint32_t codepoint = first; codepoint <= last; ++codepoint) {
|
||||
outCodepoints.push_back(codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
ImFontBaked* ResolveBakedFont(
|
||||
IXCUITextAtlasProvider::FontHandle fontHandle,
|
||||
ImFontAtlas* atlas,
|
||||
float requestedFontSize) {
|
||||
ImFont* font = ResolveFontHandle(fontHandle);
|
||||
if (!IsFontOwnedByAtlas(font, atlas)) {
|
||||
std::vector<std::uint32_t> BuildPrebakedCodepointSet() {
|
||||
std::vector<std::uint32_t> codepoints = {};
|
||||
codepoints.reserve(768u);
|
||||
for (const auto& range : kPrebakedCodepointRanges) {
|
||||
AppendCodepointRange(codepoints, range.first, range.second);
|
||||
}
|
||||
codepoints.push_back(0xFFFDu);
|
||||
std::sort(codepoints.begin(), codepoints.end());
|
||||
codepoints.erase(std::unique(codepoints.begin(), codepoints.end()), codepoints.end());
|
||||
return codepoints;
|
||||
}
|
||||
|
||||
HFONT CreateEditorFont(int pixelHeight, const wchar_t* faceName) {
|
||||
return ::CreateFontW(
|
||||
-pixelHeight,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
FW_NORMAL,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
DEFAULT_CHARSET,
|
||||
OUT_TT_PRECIS,
|
||||
CLIP_DEFAULT_PRECIS,
|
||||
ANTIALIASED_QUALITY,
|
||||
DEFAULT_PITCH | FF_DONTCARE,
|
||||
faceName);
|
||||
}
|
||||
|
||||
bool QueryTextMetrics(HDC dc, HFONT font, TEXTMETRICW& outMetrics) {
|
||||
outMetrics = {};
|
||||
if (dc == nullptr || font == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HGDIOBJ previous = ::SelectObject(dc, font);
|
||||
const BOOL result = ::GetTextMetricsW(dc, &outMetrics);
|
||||
::SelectObject(dc, previous);
|
||||
return result != FALSE;
|
||||
}
|
||||
|
||||
bool FontHasGlyph(HDC dc, HFONT font, std::uint32_t codepoint) {
|
||||
if (dc == nullptr || font == nullptr || codepoint > 0xFFFFu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WORD glyphIndex = 0xFFFFu;
|
||||
WCHAR wideCodepoint = static_cast<WCHAR>(codepoint);
|
||||
HGDIOBJ previous = ::SelectObject(dc, font);
|
||||
const DWORD result = ::GetGlyphIndicesW(
|
||||
dc,
|
||||
&wideCodepoint,
|
||||
1u,
|
||||
&glyphIndex,
|
||||
GGI_MARK_NONEXISTING_GLYPHS);
|
||||
::SelectObject(dc, previous);
|
||||
return result != GDI_ERROR && glyphIndex != 0xFFFFu;
|
||||
}
|
||||
|
||||
bool IsAdvanceOnlyWhitespace(std::uint32_t codepoint) {
|
||||
return codepoint == 0x0020u ||
|
||||
codepoint == 0x00A0u ||
|
||||
(codepoint >= 0x2000u && codepoint <= 0x200Au) ||
|
||||
codepoint == 0x202Fu ||
|
||||
codepoint == 0x205Fu ||
|
||||
codepoint == 0x3000u;
|
||||
}
|
||||
|
||||
TEXTMETRICW MergeTextMetrics(
|
||||
const TEXTMETRICW& primaryMetrics,
|
||||
bool hasPrimaryMetrics,
|
||||
const TEXTMETRICW& fallbackMetrics,
|
||||
bool hasFallbackMetrics) {
|
||||
TEXTMETRICW merged = {};
|
||||
if (hasPrimaryMetrics) {
|
||||
merged = primaryMetrics;
|
||||
}
|
||||
if (!hasFallbackMetrics) {
|
||||
return merged;
|
||||
}
|
||||
if (!hasPrimaryMetrics) {
|
||||
return fallbackMetrics;
|
||||
}
|
||||
|
||||
merged.tmHeight = (std::max)(primaryMetrics.tmHeight, fallbackMetrics.tmHeight);
|
||||
merged.tmAscent = (std::max)(primaryMetrics.tmAscent, fallbackMetrics.tmAscent);
|
||||
merged.tmDescent = (std::max)(primaryMetrics.tmDescent, fallbackMetrics.tmDescent);
|
||||
merged.tmExternalLeading = (std::max)(primaryMetrics.tmExternalLeading, fallbackMetrics.tmExternalLeading);
|
||||
merged.tmAveCharWidth = (std::max)(primaryMetrics.tmAveCharWidth, fallbackMetrics.tmAveCharWidth);
|
||||
merged.tmMaxCharWidth = (std::max)(primaryMetrics.tmMaxCharWidth, fallbackMetrics.tmMaxCharWidth);
|
||||
return merged;
|
||||
}
|
||||
|
||||
class GdiFontContext {
|
||||
public:
|
||||
explicit GdiFontContext(int sizeKey)
|
||||
: m_sizeKey(sizeKey) {
|
||||
m_dc = ::CreateCompatibleDC(nullptr);
|
||||
if (m_dc == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_primaryFont = CreateEditorFont(sizeKey, kPrimaryFontFace);
|
||||
m_fallbackFont = CreateEditorFont(sizeKey, kFallbackFontFace);
|
||||
m_hasPrimaryMetrics = QueryTextMetrics(m_dc, m_primaryFont, m_primaryMetrics);
|
||||
m_hasFallbackMetrics = QueryTextMetrics(m_dc, m_fallbackFont, m_fallbackMetrics);
|
||||
m_lineMetrics = MergeTextMetrics(
|
||||
m_primaryMetrics,
|
||||
m_hasPrimaryMetrics,
|
||||
m_fallbackMetrics,
|
||||
m_hasFallbackMetrics);
|
||||
}
|
||||
|
||||
~GdiFontContext() {
|
||||
if (m_primaryFont != nullptr) {
|
||||
::DeleteObject(m_primaryFont);
|
||||
m_primaryFont = nullptr;
|
||||
}
|
||||
if (m_fallbackFont != nullptr) {
|
||||
::DeleteObject(m_fallbackFont);
|
||||
m_fallbackFont = nullptr;
|
||||
}
|
||||
if (m_dc != nullptr) {
|
||||
::DeleteDC(m_dc);
|
||||
m_dc = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsValid() const {
|
||||
return m_dc != nullptr && (m_hasPrimaryMetrics || m_hasFallbackMetrics);
|
||||
}
|
||||
|
||||
IXCUITextAtlasProvider::BakedFontInfo BuildBakedFontInfo() const {
|
||||
IXCUITextAtlasProvider::BakedFontInfo info = {};
|
||||
info.lineHeight = static_cast<float>(m_lineMetrics.tmHeight);
|
||||
info.ascent = static_cast<float>(m_lineMetrics.tmAscent);
|
||||
info.descent = static_cast<float>(m_lineMetrics.tmDescent);
|
||||
info.rasterizerDensity = 1.0f;
|
||||
return info;
|
||||
}
|
||||
|
||||
bool RasterizeGlyph(std::uint32_t codepoint, GlyphRecord& outRecord) const {
|
||||
outRecord = {};
|
||||
outRecord.sizeKey = m_sizeKey;
|
||||
outRecord.codepoint = codepoint;
|
||||
outRecord.colored = false;
|
||||
|
||||
if (!IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HFONT resolvedFont = ResolveFontForCodepoint(codepoint);
|
||||
if (resolvedFont == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsAdvanceOnlyWhitespace(codepoint)) {
|
||||
HGDIOBJ previous = ::SelectObject(m_dc, resolvedFont);
|
||||
SIZE textExtent = {};
|
||||
WCHAR wideCodepoint = static_cast<WCHAR>(codepoint);
|
||||
const BOOL extentResult = ::GetTextExtentPoint32W(m_dc, &wideCodepoint, 1, &textExtent);
|
||||
::SelectObject(m_dc, previous);
|
||||
outRecord.visible = false;
|
||||
outRecord.advanceX = extentResult != FALSE
|
||||
? static_cast<float>(textExtent.cx)
|
||||
: static_cast<float>(m_lineMetrics.tmAveCharWidth);
|
||||
return true;
|
||||
}
|
||||
|
||||
HGDIOBJ previous = ::SelectObject(m_dc, resolvedFont);
|
||||
MAT2 transform = {};
|
||||
transform.eM11.value = 1;
|
||||
transform.eM22.value = 1;
|
||||
|
||||
GLYPHMETRICS glyphMetrics = {};
|
||||
const DWORD bufferSize = ::GetGlyphOutlineW(
|
||||
m_dc,
|
||||
static_cast<UINT>(codepoint),
|
||||
GGO_GRAY8_BITMAP,
|
||||
&glyphMetrics,
|
||||
0u,
|
||||
nullptr,
|
||||
&transform);
|
||||
::SelectObject(m_dc, previous);
|
||||
|
||||
if (bufferSize == GDI_ERROR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outRecord.visible = glyphMetrics.gmBlackBoxX > 0u && glyphMetrics.gmBlackBoxY > 0u;
|
||||
outRecord.advanceX = static_cast<float>(glyphMetrics.gmCellIncX);
|
||||
outRecord.bitmapWidth = static_cast<int>(glyphMetrics.gmBlackBoxX);
|
||||
outRecord.bitmapHeight = static_cast<int>(glyphMetrics.gmBlackBoxY);
|
||||
outRecord.x0 = static_cast<float>(glyphMetrics.gmptGlyphOrigin.x);
|
||||
outRecord.y0 = static_cast<float>(m_lineMetrics.tmAscent - glyphMetrics.gmptGlyphOrigin.y);
|
||||
outRecord.x1 = outRecord.x0 + static_cast<float>(glyphMetrics.gmBlackBoxX);
|
||||
outRecord.y1 = outRecord.y0 + static_cast<float>(glyphMetrics.gmBlackBoxY);
|
||||
|
||||
if (!outRecord.visible) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> bitmapBuffer(bufferSize);
|
||||
previous = ::SelectObject(m_dc, resolvedFont);
|
||||
const DWORD writeResult = ::GetGlyphOutlineW(
|
||||
m_dc,
|
||||
static_cast<UINT>(codepoint),
|
||||
GGO_GRAY8_BITMAP,
|
||||
&glyphMetrics,
|
||||
bufferSize,
|
||||
bitmapBuffer.data(),
|
||||
&transform);
|
||||
::SelectObject(m_dc, previous);
|
||||
if (writeResult == GDI_ERROR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int sourceStride = (static_cast<int>(glyphMetrics.gmBlackBoxX) + 3) & ~3;
|
||||
const std::size_t requiredBytes =
|
||||
static_cast<std::size_t>(sourceStride) *
|
||||
static_cast<std::size_t>((std::max)(0, outRecord.bitmapHeight));
|
||||
if (bitmapBuffer.size() < requiredBytes) {
|
||||
return false;
|
||||
}
|
||||
outRecord.alpha.resize(
|
||||
static_cast<std::size_t>(outRecord.bitmapWidth) *
|
||||
static_cast<std::size_t>(outRecord.bitmapHeight));
|
||||
for (int y = 0; y < outRecord.bitmapHeight; ++y) {
|
||||
const unsigned char* sourceRow = bitmapBuffer.data() + static_cast<std::size_t>(y * sourceStride);
|
||||
unsigned char* destRow = outRecord.alpha.data() +
|
||||
static_cast<std::size_t>(y * outRecord.bitmapWidth);
|
||||
for (int x = 0; x < outRecord.bitmapWidth; ++x) {
|
||||
const unsigned int value = static_cast<unsigned int>(sourceRow[x]);
|
||||
destRow[x] = static_cast<unsigned char>((value * 255u + 32u) / 64u);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
HFONT ResolveFontForCodepoint(std::uint32_t codepoint) const {
|
||||
if (FontHasGlyph(m_dc, m_primaryFont, codepoint)) {
|
||||
return m_primaryFont;
|
||||
}
|
||||
if (FontHasGlyph(m_dc, m_fallbackFont, codepoint)) {
|
||||
return m_fallbackFont;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const float resolvedFontSize = ResolveRequestedFontSize(*font, requestedFontSize);
|
||||
if (resolvedFontSize <= 0.0f) {
|
||||
return nullptr;
|
||||
int m_sizeKey = 0;
|
||||
HDC m_dc = nullptr;
|
||||
HFONT m_primaryFont = nullptr;
|
||||
HFONT m_fallbackFont = nullptr;
|
||||
TEXTMETRICW m_primaryMetrics = {};
|
||||
TEXTMETRICW m_fallbackMetrics = {};
|
||||
TEXTMETRICW m_lineMetrics = {};
|
||||
bool m_hasPrimaryMetrics = false;
|
||||
bool m_hasFallbackMetrics = false;
|
||||
};
|
||||
|
||||
std::uintptr_t MakePixelDataKey(const CachedPixelBuffer& pixels, std::uint64_t generation) {
|
||||
return reinterpret_cast<std::uintptr_t>(pixels.bytes.data()) ^
|
||||
static_cast<std::uintptr_t>(generation * 1315423911ull);
|
||||
}
|
||||
|
||||
int ResolveSupportedSizeKey(float requestedSize, float nominalSize) {
|
||||
const float targetSize = requestedSize > 0.0f ? requestedSize : nominalSize;
|
||||
auto bestIt = std::min_element(
|
||||
kSupportedFontSizes.begin(),
|
||||
kSupportedFontSizes.end(),
|
||||
[targetSize](int lhs, int rhs) {
|
||||
const float lhsDistance = std::fabs(targetSize - static_cast<float>(lhs));
|
||||
const float rhsDistance = std::fabs(targetSize - static_cast<float>(rhs));
|
||||
return lhsDistance < rhsDistance;
|
||||
});
|
||||
return bestIt != kSupportedFontSizes.end() ? *bestIt : static_cast<int>(std::lround(targetSize));
|
||||
}
|
||||
|
||||
bool PopulateGlyphInfo(
|
||||
const GlyphRecord& glyph,
|
||||
int atlasWidth,
|
||||
int atlasHeight,
|
||||
IXCUITextAtlasProvider::GlyphInfo& outInfo) {
|
||||
outInfo = {};
|
||||
outInfo.requestedCodepoint = glyph.codepoint;
|
||||
outInfo.resolvedCodepoint = glyph.codepoint;
|
||||
outInfo.visible = glyph.visible;
|
||||
outInfo.colored = glyph.colored;
|
||||
outInfo.advanceX = glyph.advanceX;
|
||||
outInfo.x0 = glyph.x0;
|
||||
outInfo.y0 = glyph.y0;
|
||||
outInfo.x1 = glyph.x1;
|
||||
outInfo.y1 = glyph.y1;
|
||||
if (!glyph.visible) {
|
||||
return true;
|
||||
}
|
||||
if (atlasWidth <= 0 || atlasHeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return font->GetFontBaked(resolvedFontSize);
|
||||
outInfo.u0 = static_cast<float>(glyph.atlasX) / static_cast<float>(atlasWidth);
|
||||
outInfo.v0 = static_cast<float>(glyph.atlasY) / static_cast<float>(atlasHeight);
|
||||
outInfo.u1 = static_cast<float>(glyph.atlasX + glyph.bitmapWidth) / static_cast<float>(atlasWidth);
|
||||
outInfo.v1 = static_cast<float>(glyph.atlasY + glyph.bitmapHeight) / static_cast<float>(atlasHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::size_t> BuildVisiblePackOrder(const std::vector<GlyphRecord>& glyphs) {
|
||||
std::vector<std::size_t> order = {};
|
||||
order.reserve(glyphs.size());
|
||||
for (std::size_t index = 0; index < glyphs.size(); ++index) {
|
||||
if (glyphs[index].visible) {
|
||||
order.push_back(index);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(
|
||||
order.begin(),
|
||||
order.end(),
|
||||
[&glyphs](std::size_t lhs, std::size_t rhs) {
|
||||
const GlyphRecord& left = glyphs[lhs];
|
||||
const GlyphRecord& right = glyphs[rhs];
|
||||
if (left.bitmapHeight != right.bitmapHeight) {
|
||||
return left.bitmapHeight > right.bitmapHeight;
|
||||
}
|
||||
if (left.bitmapWidth != right.bitmapWidth) {
|
||||
return left.bitmapWidth > right.bitmapWidth;
|
||||
}
|
||||
if (left.sizeKey != right.sizeKey) {
|
||||
return left.sizeKey < right.sizeKey;
|
||||
}
|
||||
return left.codepoint < right.codepoint;
|
||||
});
|
||||
return order;
|
||||
}
|
||||
|
||||
bool TryPackVisibleGlyphs(
|
||||
std::vector<GlyphRecord>& glyphs,
|
||||
int atlasWidth,
|
||||
int& outAtlasHeight) {
|
||||
outAtlasHeight = 1;
|
||||
if (atlasWidth <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (GlyphRecord& glyph : glyphs) {
|
||||
glyph.atlasX = 0;
|
||||
glyph.atlasY = 0;
|
||||
}
|
||||
|
||||
const std::vector<std::size_t> packOrder = BuildVisiblePackOrder(glyphs);
|
||||
int cursorX = kAtlasPadding;
|
||||
int cursorY = kAtlasPadding;
|
||||
int rowHeight = 0;
|
||||
|
||||
for (std::size_t index : packOrder) {
|
||||
GlyphRecord& glyph = glyphs[index];
|
||||
const int requiredWidth = glyph.bitmapWidth + kAtlasPadding * 2;
|
||||
const int requiredHeight = glyph.bitmapHeight + kAtlasPadding * 2;
|
||||
if (requiredWidth > atlasWidth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cursorX + requiredWidth > atlasWidth) {
|
||||
cursorX = kAtlasPadding;
|
||||
cursorY += rowHeight;
|
||||
rowHeight = 0;
|
||||
}
|
||||
|
||||
glyph.atlasX = cursorX + kAtlasPadding;
|
||||
glyph.atlasY = cursorY + kAtlasPadding;
|
||||
cursorX += requiredWidth;
|
||||
rowHeight = (std::max)(rowHeight, requiredHeight);
|
||||
outAtlasHeight = (std::max)(outAtlasHeight, cursorY + rowHeight + kAtlasPadding);
|
||||
if (outAtlasHeight > kMaxAtlasDimension) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
XCUIStandaloneTextAtlasProvider::XCUIStandaloneTextAtlasProvider() {
|
||||
struct XCUIStandaloneTextAtlasProvider::Impl {
|
||||
bool ready = false;
|
||||
float nominalSize = kDefaultNominalFontSize;
|
||||
CachedPixelBuffer alpha8Pixels = {};
|
||||
CachedPixelBuffer rgba32Pixels = {};
|
||||
std::uint64_t pixelGeneration = 0u;
|
||||
std::unordered_map<int, BakedFontRecord> bakedFonts = {};
|
||||
std::vector<GlyphRecord> glyphRecords = {};
|
||||
std::unordered_map<std::uint64_t, std::size_t> glyphIndices = {};
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
void ResetImpl(XCUIStandaloneTextAtlasProvider::Impl& impl) {
|
||||
impl.ready = false;
|
||||
impl.nominalSize = kDefaultNominalFontSize;
|
||||
impl.alpha8Pixels.Clear();
|
||||
impl.rgba32Pixels.Clear();
|
||||
impl.bakedFonts.clear();
|
||||
impl.glyphRecords.clear();
|
||||
impl.glyphIndices.clear();
|
||||
++impl.pixelGeneration;
|
||||
}
|
||||
|
||||
bool RebuildAtlasPixels(XCUIStandaloneTextAtlasProvider::Impl& impl) {
|
||||
int chosenAtlasWidth = 0;
|
||||
int chosenAtlasHeight = 1;
|
||||
for (const int candidateWidth : { 1024, 2048, 4096 }) {
|
||||
int packedHeight = 1;
|
||||
if (TryPackVisibleGlyphs(impl.glyphRecords, candidateWidth, packedHeight)) {
|
||||
chosenAtlasWidth = candidateWidth;
|
||||
chosenAtlasHeight = packedHeight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (chosenAtlasWidth == 0 || chosenAtlasHeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
impl.alpha8Pixels.width = chosenAtlasWidth;
|
||||
impl.alpha8Pixels.height = chosenAtlasHeight;
|
||||
impl.alpha8Pixels.bytesPerPixel = 1;
|
||||
impl.alpha8Pixels.bytes.assign(
|
||||
static_cast<std::size_t>(chosenAtlasWidth) * static_cast<std::size_t>(chosenAtlasHeight),
|
||||
0u);
|
||||
|
||||
for (const GlyphRecord& glyph : impl.glyphRecords) {
|
||||
if (!glyph.visible || glyph.bitmapWidth <= 0 || glyph.bitmapHeight <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int y = 0; y < glyph.bitmapHeight; ++y) {
|
||||
const unsigned char* sourceRow = glyph.alpha.data() +
|
||||
static_cast<std::size_t>(y * glyph.bitmapWidth);
|
||||
unsigned char* destRow = impl.alpha8Pixels.bytes.data() +
|
||||
static_cast<std::size_t>((glyph.atlasY + y) * impl.alpha8Pixels.width + glyph.atlasX);
|
||||
std::copy(sourceRow, sourceRow + glyph.bitmapWidth, destRow);
|
||||
}
|
||||
}
|
||||
|
||||
impl.rgba32Pixels.width = chosenAtlasWidth;
|
||||
impl.rgba32Pixels.height = chosenAtlasHeight;
|
||||
impl.rgba32Pixels.bytesPerPixel = 4;
|
||||
impl.rgba32Pixels.bytes.assign(
|
||||
static_cast<std::size_t>(chosenAtlasWidth) *
|
||||
static_cast<std::size_t>(chosenAtlasHeight) *
|
||||
4u,
|
||||
0u);
|
||||
for (std::size_t pixelIndex = 0; pixelIndex < impl.alpha8Pixels.bytes.size(); ++pixelIndex) {
|
||||
const unsigned char alpha = impl.alpha8Pixels.bytes[pixelIndex];
|
||||
unsigned char* rgba = impl.rgba32Pixels.bytes.data() + pixelIndex * 4u;
|
||||
rgba[0] = 255u;
|
||||
rgba[1] = 255u;
|
||||
rgba[2] = 255u;
|
||||
rgba[3] = alpha;
|
||||
}
|
||||
|
||||
for (auto& [sizeKey, bakedFont] : impl.bakedFonts) {
|
||||
(void)sizeKey;
|
||||
bakedFont.glyphs.clear();
|
||||
}
|
||||
|
||||
for (const GlyphRecord& glyph : impl.glyphRecords) {
|
||||
IXCUITextAtlasProvider::GlyphInfo glyphInfo = {};
|
||||
if (!PopulateGlyphInfo(
|
||||
glyph,
|
||||
impl.alpha8Pixels.width,
|
||||
impl.alpha8Pixels.height,
|
||||
glyphInfo)) {
|
||||
return false;
|
||||
}
|
||||
impl.bakedFonts[glyph.sizeKey].glyphs.insert_or_assign(glyph.codepoint, glyphInfo);
|
||||
}
|
||||
|
||||
++impl.pixelGeneration;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AddGlyphRecord(
|
||||
XCUIStandaloneTextAtlasProvider::Impl& impl,
|
||||
int sizeKey,
|
||||
std::uint32_t codepoint) {
|
||||
const std::uint64_t glyphKey = MakeGlyphKey(sizeKey, codepoint);
|
||||
if (impl.glyphIndices.find(glyphKey) != impl.glyphIndices.end()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
GdiFontContext context(sizeKey);
|
||||
if (!context.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (impl.bakedFonts.find(sizeKey) == impl.bakedFonts.end()) {
|
||||
BakedFontRecord bakedFont = {};
|
||||
bakedFont.sizeKey = sizeKey;
|
||||
bakedFont.metrics = context.BuildBakedFontInfo();
|
||||
impl.bakedFonts.insert_or_assign(sizeKey, std::move(bakedFont));
|
||||
}
|
||||
|
||||
GlyphRecord glyph = {};
|
||||
if (!context.RasterizeGlyph(codepoint, glyph)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t previousGlyphCount = impl.glyphRecords.size();
|
||||
impl.glyphRecords.push_back(std::move(glyph));
|
||||
impl.glyphIndices.insert_or_assign(glyphKey, previousGlyphCount);
|
||||
if (!RebuildAtlasPixels(impl)) {
|
||||
impl.glyphIndices.erase(glyphKey);
|
||||
impl.glyphRecords.pop_back();
|
||||
RebuildAtlasPixels(impl);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const BakedFontRecord* FindBakedFont(
|
||||
const XCUIStandaloneTextAtlasProvider::Impl& impl,
|
||||
int sizeKey) {
|
||||
const auto it = impl.bakedFonts.find(sizeKey);
|
||||
return it != impl.bakedFonts.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
XCUIStandaloneTextAtlasProvider::XCUIStandaloneTextAtlasProvider()
|
||||
: m_impl(std::make_unique<Impl>()) {
|
||||
RebuildDefaultEditorAtlas();
|
||||
}
|
||||
|
||||
XCUIStandaloneTextAtlasProvider::~XCUIStandaloneTextAtlasProvider() = default;
|
||||
|
||||
void XCUIStandaloneTextAtlasProvider::Reset() {
|
||||
m_atlas.Clear();
|
||||
m_defaultFont = nullptr;
|
||||
m_ready = false;
|
||||
if (m_impl == nullptr) {
|
||||
m_impl = std::make_unique<Impl>();
|
||||
return;
|
||||
}
|
||||
|
||||
ResetImpl(*m_impl);
|
||||
}
|
||||
|
||||
bool XCUIStandaloneTextAtlasProvider::RebuildDefaultEditorAtlas() {
|
||||
Reset();
|
||||
m_ready = BuildDefaultXCUIEditorFontAtlas(m_atlas, m_defaultFont);
|
||||
return m_ready;
|
||||
|
||||
if (m_impl == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<std::uint32_t> prebakedCodepoints = BuildPrebakedCodepointSet();
|
||||
for (const int sizeKey : kSupportedFontSizes) {
|
||||
GdiFontContext context(sizeKey);
|
||||
if (!context.IsValid()) {
|
||||
ResetImpl(*m_impl);
|
||||
return false;
|
||||
}
|
||||
|
||||
BakedFontRecord bakedFont = {};
|
||||
bakedFont.sizeKey = sizeKey;
|
||||
bakedFont.metrics = context.BuildBakedFontInfo();
|
||||
m_impl->bakedFonts.insert_or_assign(sizeKey, std::move(bakedFont));
|
||||
|
||||
for (std::uint32_t codepoint : prebakedCodepoints) {
|
||||
GlyphRecord glyph = {};
|
||||
if (!context.RasterizeGlyph(codepoint, glyph)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::uint64_t glyphKey = MakeGlyphKey(sizeKey, codepoint);
|
||||
m_impl->glyphIndices.insert_or_assign(glyphKey, m_impl->glyphRecords.size());
|
||||
m_impl->glyphRecords.push_back(std::move(glyph));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_impl->bakedFonts.empty() || !RebuildAtlasPixels(*m_impl)) {
|
||||
ResetImpl(*m_impl);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_impl->ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XCUIStandaloneTextAtlasProvider::IsReady() const {
|
||||
return m_ready && ResolveDefaultFont() != nullptr;
|
||||
return m_impl != nullptr &&
|
||||
m_impl->ready &&
|
||||
!m_impl->bakedFonts.empty() &&
|
||||
m_impl->rgba32Pixels.IsValid();
|
||||
}
|
||||
|
||||
bool XCUIStandaloneTextAtlasProvider::GetAtlasTextureView(
|
||||
@@ -74,74 +712,59 @@ bool XCUIStandaloneTextAtlasProvider::GetAtlasTextureView(
|
||||
AtlasTextureView& outView) const {
|
||||
outView = {};
|
||||
|
||||
ImFontAtlas* atlas = ResolveAtlas();
|
||||
if (atlas == nullptr) {
|
||||
if (!IsReady() || m_impl == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned char* pixels = nullptr;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int bytesPerPixel = 0;
|
||||
PixelFormat resolvedFormat = preferredFormat;
|
||||
|
||||
switch (preferredFormat) {
|
||||
case PixelFormat::Alpha8:
|
||||
atlas->GetTexDataAsAlpha8(&pixels, &width, &height, &bytesPerPixel);
|
||||
const CachedPixelBuffer* resolvedPixels = nullptr;
|
||||
PixelFormat resolvedFormat = PixelFormat::Unknown;
|
||||
if (preferredFormat == PixelFormat::Alpha8 && m_impl->alpha8Pixels.IsValid()) {
|
||||
resolvedPixels = &m_impl->alpha8Pixels;
|
||||
resolvedFormat = PixelFormat::Alpha8;
|
||||
break;
|
||||
case PixelFormat::RGBA32:
|
||||
case PixelFormat::Unknown:
|
||||
default:
|
||||
atlas->GetTexDataAsRGBA32(&pixels, &width, &height, &bytesPerPixel);
|
||||
} else if (m_impl->rgba32Pixels.IsValid()) {
|
||||
resolvedPixels = &m_impl->rgba32Pixels;
|
||||
resolvedFormat = PixelFormat::RGBA32;
|
||||
break;
|
||||
} else if (m_impl->alpha8Pixels.IsValid()) {
|
||||
resolvedPixels = &m_impl->alpha8Pixels;
|
||||
resolvedFormat = PixelFormat::Alpha8;
|
||||
}
|
||||
|
||||
if (pixels == nullptr || width <= 0 || height <= 0 || bytesPerPixel <= 0) {
|
||||
if (resolvedPixels == nullptr || !resolvedPixels->IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outView.pixels = pixels;
|
||||
outView.width = width;
|
||||
outView.height = height;
|
||||
outView.stride = width * bytesPerPixel;
|
||||
outView.bytesPerPixel = bytesPerPixel;
|
||||
outView.pixels = resolvedPixels->bytes.data();
|
||||
outView.width = resolvedPixels->width;
|
||||
outView.height = resolvedPixels->height;
|
||||
outView.stride = resolvedPixels->width * resolvedPixels->bytesPerPixel;
|
||||
outView.bytesPerPixel = resolvedPixels->bytesPerPixel;
|
||||
outView.format = resolvedFormat;
|
||||
outView.atlasStorageKey = reinterpret_cast<std::uintptr_t>(atlas);
|
||||
outView.pixelDataKey = reinterpret_cast<std::uintptr_t>(pixels);
|
||||
outView.atlasStorageKey = reinterpret_cast<std::uintptr_t>(m_impl.get());
|
||||
outView.pixelDataKey = MakePixelDataKey(*resolvedPixels, m_impl->pixelGeneration);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t XCUIStandaloneTextAtlasProvider::GetFontCount() const {
|
||||
const ImFontAtlas* atlas = ResolveAtlas();
|
||||
return atlas != nullptr ? static_cast<std::size_t>(atlas->Fonts.Size) : 0u;
|
||||
return IsReady() ? 1u : 0u;
|
||||
}
|
||||
|
||||
IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetFont(std::size_t index) const {
|
||||
const ImFontAtlas* atlas = ResolveAtlas();
|
||||
if (atlas == nullptr || index >= static_cast<std::size_t>(atlas->Fonts.Size)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return MakeFontHandle(atlas->Fonts[static_cast<int>(index)]);
|
||||
return IsReady() && index == 0u ? MakeDefaultFontHandle() : FontHandle{};
|
||||
}
|
||||
|
||||
IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetDefaultFont() const {
|
||||
return MakeFontHandle(ResolveDefaultFont());
|
||||
return IsReady() ? MakeDefaultFontHandle() : FontHandle{};
|
||||
}
|
||||
|
||||
bool XCUIStandaloneTextAtlasProvider::GetFontInfo(FontHandle fontHandle, FontInfo& outInfo) const {
|
||||
outInfo = {};
|
||||
|
||||
ImFontAtlas* atlas = ResolveAtlas();
|
||||
ImFont* font = ResolveFontHandle(fontHandle);
|
||||
if (!IsFontOwnedByAtlas(font, atlas)) {
|
||||
if (!IsReady() || ResolveFontIndex(fontHandle) == kInvalidFontIndex || m_impl == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outInfo.handle = fontHandle;
|
||||
outInfo.nominalSize = ResolveNominalFontSize(*font);
|
||||
outInfo.nominalSize = m_impl->nominalSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -151,15 +774,17 @@ bool XCUIStandaloneTextAtlasProvider::GetBakedFontInfo(
|
||||
BakedFontInfo& outInfo) const {
|
||||
outInfo = {};
|
||||
|
||||
ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, ResolveAtlas(), fontSize);
|
||||
if (!IsReady() || ResolveFontIndex(fontHandle) == kInvalidFontIndex || m_impl == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int sizeKey = ResolveSupportedSizeKey(fontSize, m_impl->nominalSize);
|
||||
const BakedFontRecord* bakedFont = FindBakedFont(*m_impl, sizeKey);
|
||||
if (bakedFont == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outInfo.lineHeight = bakedFont->Size;
|
||||
outInfo.ascent = bakedFont->Ascent;
|
||||
outInfo.descent = bakedFont->Descent;
|
||||
outInfo.rasterizerDensity = bakedFont->RasterizerDensity;
|
||||
outInfo = bakedFont->metrics;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -170,53 +795,48 @@ bool XCUIStandaloneTextAtlasProvider::FindGlyph(
|
||||
GlyphInfo& outInfo) const {
|
||||
outInfo = {};
|
||||
|
||||
if (codepoint > IM_UNICODE_CODEPOINT_MAX) {
|
||||
if (!IsReady() || ResolveFontIndex(fontHandle) == kInvalidFontIndex || m_impl == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, ResolveAtlas(), fontSize);
|
||||
const int sizeKey = ResolveSupportedSizeKey(fontSize, m_impl->nominalSize);
|
||||
const BakedFontRecord* bakedFont = FindBakedFont(*m_impl, sizeKey);
|
||||
if (bakedFont == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ImFontGlyph* glyph = bakedFont->FindGlyph(static_cast<ImWchar>(codepoint));
|
||||
if (glyph == nullptr) {
|
||||
const auto glyphIt = bakedFont->glyphs.find(codepoint);
|
||||
if (glyphIt != bakedFont->glyphs.end()) {
|
||||
outInfo = glyphIt->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (codepoint <= 0xFFFFu && AddGlyphRecord(*m_impl, sizeKey, codepoint)) {
|
||||
const BakedFontRecord* refreshedBakedFont = FindBakedFont(*m_impl, sizeKey);
|
||||
if (refreshedBakedFont != nullptr) {
|
||||
const auto refreshedGlyphIt = refreshedBakedFont->glyphs.find(codepoint);
|
||||
if (refreshedGlyphIt != refreshedBakedFont->glyphs.end()) {
|
||||
outInfo = refreshedGlyphIt->second;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BakedFontRecord* fallbackBakedFont = FindBakedFont(*m_impl, sizeKey);
|
||||
if (fallbackBakedFont == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto fallbackGlyphIt = fallbackBakedFont->glyphs.find(static_cast<std::uint32_t>('?'));
|
||||
if (fallbackGlyphIt == fallbackBakedFont->glyphs.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outInfo = fallbackGlyphIt->second;
|
||||
outInfo.requestedCodepoint = codepoint;
|
||||
outInfo.resolvedCodepoint = glyph->Codepoint;
|
||||
outInfo.visible = glyph->Visible != 0;
|
||||
outInfo.colored = glyph->Colored != 0;
|
||||
outInfo.advanceX = glyph->AdvanceX;
|
||||
outInfo.x0 = glyph->X0;
|
||||
outInfo.y0 = glyph->Y0;
|
||||
outInfo.x1 = glyph->X1;
|
||||
outInfo.y1 = glyph->Y1;
|
||||
outInfo.u0 = glyph->U0;
|
||||
outInfo.v0 = glyph->V0;
|
||||
outInfo.u1 = glyph->U1;
|
||||
outInfo.v1 = glyph->V1;
|
||||
return true;
|
||||
}
|
||||
|
||||
::ImFontAtlas* XCUIStandaloneTextAtlasProvider::ResolveAtlas() const {
|
||||
return m_ready ? &m_atlas : nullptr;
|
||||
}
|
||||
|
||||
::ImFont* XCUIStandaloneTextAtlasProvider::ResolveDefaultFont() const {
|
||||
ImFontAtlas* atlas = ResolveAtlas();
|
||||
if (atlas == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (IsFontOwnedByAtlas(m_defaultFont, atlas)) {
|
||||
return m_defaultFont;
|
||||
}
|
||||
|
||||
return atlas->Fonts.empty() ? nullptr : atlas->Fonts[0];
|
||||
}
|
||||
|
||||
} // namespace XCUIBackend
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "IXCUITextAtlasProvider.h"
|
||||
#include "XCUIEditorFontSetup.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <memory>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -12,6 +12,7 @@ namespace XCUIBackend {
|
||||
class XCUIStandaloneTextAtlasProvider final : public IXCUITextAtlasProvider {
|
||||
public:
|
||||
XCUIStandaloneTextAtlasProvider();
|
||||
~XCUIStandaloneTextAtlasProvider();
|
||||
|
||||
XCUIStandaloneTextAtlasProvider(const XCUIStandaloneTextAtlasProvider&) = delete;
|
||||
XCUIStandaloneTextAtlasProvider& operator=(const XCUIStandaloneTextAtlasProvider&) = delete;
|
||||
@@ -28,13 +29,10 @@ public:
|
||||
bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override;
|
||||
bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override;
|
||||
|
||||
private:
|
||||
::ImFontAtlas* ResolveAtlas() const;
|
||||
::ImFont* ResolveDefaultFont() const;
|
||||
struct Impl;
|
||||
|
||||
mutable ::ImFontAtlas m_atlas = {};
|
||||
::ImFont* m_defaultFont = nullptr;
|
||||
bool m_ready = false;
|
||||
private:
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
|
||||
} // namespace XCUIBackend
|
||||
|
||||
Reference in New Issue
Block a user