De-ImGui XCUI standalone text atlas provider
This commit is contained in:
@@ -16,7 +16,7 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
|||||||
- `LayoutLab` continues as the editor widget proving ground for tree/list/property-section style controls
|
- `LayoutLab` continues as the editor widget proving ground for tree/list/property-section style controls
|
||||||
- the demo sandbox and editor bridge APIs were tightened again without touching the old editor replacement scope
|
- the demo sandbox and editor bridge APIs were tightened again without touching the old editor replacement scope
|
||||||
- `new_editor` now has an explicit two-step window compositor seam through `IWindowUICompositor` / `ImGuiWindowUICompositor`, layered on top of `IEditorHostCompositor` / `ImGuiHostCompositor`
|
- `new_editor` now has an explicit two-step window compositor seam through `IWindowUICompositor` / `ImGuiWindowUICompositor`, layered on top of `IEditorHostCompositor` / `ImGuiHostCompositor`
|
||||||
- The native-host follow-up is now landed in `new_editor`:
|
- The native-host / hosted-preview publication follow-up is now landed in `new_editor`:
|
||||||
- `NativeWindowUICompositor` is now buildable alongside the legacy ImGui compositor
|
- `NativeWindowUICompositor` is now buildable alongside the legacy ImGui compositor
|
||||||
- `Application` now defaults to a native XCUI host path instead of creating the ImGui shell by default
|
- `Application` now defaults to a native XCUI host path instead of creating the ImGui shell by default
|
||||||
- the native default path now drives `XCUIDemoRuntime` and `XCUILayoutLabRuntime` directly, composes their `UIDrawData`, and submits one swapchain packet through the native compositor
|
- the native default path now drives `XCUIDemoRuntime` and `XCUILayoutLabRuntime` directly, composes their `UIDrawData`, and submits one swapchain packet through the native compositor
|
||||||
@@ -24,6 +24,9 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
|||||||
- the native shell now begins the hosted-preview queue/registry lifecycle each frame, queues native preview frames, drains them during the native render pass, and consumes published hosted-surface images directly in panel cards with live/warming placeholder states
|
- the native shell now begins the hosted-preview queue/registry lifecycle each frame, queues native preview frames, drains them during the native render pass, and consumes published hosted-surface images directly in panel cards with live/warming placeholder states
|
||||||
- the old ImGui shell path remains present as an explicit compatibility host instead of the default host
|
- the old ImGui shell path remains present as an explicit compatibility host instead of the default host
|
||||||
- `XCUIInputBridge.h` no longer drags `imgui.h` through the public XCUI input seam
|
- `XCUIInputBridge.h` no longer drags `imgui.h` through the public XCUI input seam
|
||||||
|
- The default native text path is now also de-ImGuiized inside `new_editor`:
|
||||||
|
- `XCUIStandaloneTextAtlasProvider` now builds and owns its atlas through a Windows/GDI raster path instead of using ImGui font-atlas internals
|
||||||
|
- the standalone atlas provider now exposes both `RGBA32` and `Alpha8` views, supports non-nominal size resolution, lazily adds non-prebaked BMP glyphs, and falls back to `?` when a glyph cannot be rasterized
|
||||||
- Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`.
|
- Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`.
|
||||||
|
|
||||||
## Three-Layer Status
|
## Three-Layer Status
|
||||||
@@ -50,7 +53,7 @@ Current gap:
|
|||||||
|
|
||||||
- Minimal schema self-definition support is landed, including consistency checks for enum/document-only schema metadata, but schema-driven validation for `.xcui` / `.xctheme` instances is still not implemented.
|
- Minimal schema self-definition support is landed, including consistency checks for enum/document-only schema metadata, but schema-driven validation for `.xcui` / `.xctheme` instances is still not implemented.
|
||||||
- Shared widget/runtime instantiation is still thin and mostly editor-side.
|
- Shared widget/runtime instantiation is still thin and mostly editor-side.
|
||||||
- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, multi-selection/focus-traversal/virtualized collection state on top of the new editor-primitive helpers, and a fully native text/font-atlas path that no longer leans on ImGui font internals.
|
- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, multi-selection/focus-traversal/virtualized collection state on top of the new editor-primitive helpers, shared scroll/navigation-scope/caret-layout helpers, and promotion of the current native text-atlas path into a shared/cross-platform text subsystem.
|
||||||
|
|
||||||
### 2. Runtime/Game Layer
|
### 2. Runtime/Game Layer
|
||||||
|
|
||||||
@@ -118,9 +121,10 @@ Current gap:
|
|||||||
Current gap:
|
Current gap:
|
||||||
|
|
||||||
- The default shell host is now native, but the legacy ImGui shell and panel path still exists as a compatibility host and is still compiled into `new_editor`.
|
- The default shell host is now native, but the legacy ImGui shell and panel path still exists as a compatibility host and is still compiled into `new_editor`.
|
||||||
|
- The default native shell path is still compiled through an `Application` translation unit that directly includes legacy ImGui host/presenter/compositor code, and `new_editor/CMakeLists.txt` still treats those legacy ImGui sources and include paths as target-global defaults.
|
||||||
- The native shell currently proves direct runtime composition, but its shell chrome is still a bespoke `Application`-side layout rather than a fully shared XCUI-authored editor shell document.
|
- The native shell currently proves direct runtime composition, but its shell chrome is still a bespoke `Application`-side layout rather than a fully shared XCUI-authored editor shell document.
|
||||||
- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/focus traversal, toolbar/menu chrome, menu interaction widgets, and icon-atlas widgets are not yet extracted into reusable XCUI modules.
|
- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/focus traversal, toolbar/menu chrome, menu interaction widgets, and icon-atlas widgets are not yet extracted into reusable XCUI modules.
|
||||||
- The default native path still depends on ImGui font-atlas internals through `XCUIStandaloneTextAtlasProvider`, so text rendering is not yet cleanly de-ImGuiized even though the shell/panel composition path is native by default.
|
- The default native text path now uses a standalone Windows/GDI atlas through `XCUIStandaloneTextAtlasProvider`, but that provider still lives inside `new_editor` and is not yet promoted into a shared/cross-platform text subsystem.
|
||||||
|
|
||||||
## Validated This Phase
|
## Validated This Phase
|
||||||
|
|
||||||
@@ -131,17 +135,19 @@ Current gap:
|
|||||||
- `new_editor_xcui_layout_lab_runtime_tests`: `12/12`
|
- `new_editor_xcui_layout_lab_runtime_tests`: `12/12`
|
||||||
- `new_editor_xcui_rhi_command_compiler_tests`: `7/7`
|
- `new_editor_xcui_rhi_command_compiler_tests`: `7/7`
|
||||||
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
||||||
|
- `new_editor_xcui_standalone_text_atlas_provider_tests`: `6/6`
|
||||||
- `new_editor_xcui_hosted_preview_presenter_tests`: `20/20`
|
- `new_editor_xcui_hosted_preview_presenter_tests`: `20/20`
|
||||||
- `new_editor_imgui_window_ui_compositor_tests`: `7/7`
|
- `new_editor_imgui_window_ui_compositor_tests`: `7/7`
|
||||||
- `new_editor_native_window_ui_compositor_tests`: `8/8`
|
- `new_editor_native_window_ui_compositor_tests`: `8/8`
|
||||||
- `new_editor_xcui_editor_command_router_tests`: `5/5`
|
- `new_editor_xcui_editor_command_router_tests`: `5/5`
|
||||||
- `new_editor_application_shell_command_bindings_tests`: `8/8`
|
- `new_editor_application_shell_command_bindings_tests`: `11/11`
|
||||||
- `new_editor_xcui_shell_chrome_state_tests`: `11/11`
|
- `new_editor_xcui_shell_chrome_state_tests`: `11/11`
|
||||||
- `new_editor_xcui_panel_canvas_host_tests`: `4/4`
|
- `new_editor_xcui_panel_canvas_host_tests`: `4/4`
|
||||||
- `new_editor_imgui_xcui_panel_canvas_host_tests`: `1/1`
|
- `new_editor_imgui_xcui_panel_canvas_host_tests`: `1/1`
|
||||||
- `new_editor_native_xcui_panel_canvas_host_tests`: `4/4`
|
- `new_editor_native_xcui_panel_canvas_host_tests`: `4/4`
|
||||||
- `new_editor_xcui_layout_lab_panel_tests`: `3/3`
|
- `new_editor_xcui_layout_lab_panel_tests`: `3/3`
|
||||||
- `XCNewEditor` Debug target builds successfully
|
- `XCNewEditor` Debug target builds successfully
|
||||||
|
- `XCNewEditor.exe` native-default smoke run stayed alive for `5s`
|
||||||
- `core_ui_tests`: `52 total` (`50` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`)
|
- `core_ui_tests`: `52 total` (`50` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`)
|
||||||
- `scene_tests`: `68/68`
|
- `scene_tests`: `68/68`
|
||||||
- `core_ui_style_tests`: `5/5`
|
- `core_ui_style_tests`: `5/5`
|
||||||
@@ -253,7 +259,7 @@ Current gap:
|
|||||||
- generic preview frame submission no longer carries an ImGui draw-list pointer
|
- generic preview frame submission no longer carries an ImGui draw-list pointer
|
||||||
- the ImGui presenter now resolves inline draw targets through an explicit ImGui-only binding seam
|
- the ImGui presenter now resolves inline draw targets through an explicit ImGui-only binding seam
|
||||||
- panel/runtime callers still preserve the same legacy and native-preview behavior
|
- panel/runtime callers still preserve the same legacy and native-preview behavior
|
||||||
- Native hosted-preview publication is now wired through the default shell path:
|
- Native hosted-preview publication milestone is now wired through the default shell path:
|
||||||
- `NativeWindowUICompositor` publishes hosted preview textures as SRV-backed XCUI registrations and frees them through the compositor seam
|
- `NativeWindowUICompositor` publishes hosted preview textures as SRV-backed XCUI registrations and frees them through the compositor seam
|
||||||
- `Application::BeginHostedPreviewFrameLifecycle(...)` now resets queue/registry state for both legacy and native frame paths
|
- `Application::BeginHostedPreviewFrameLifecycle(...)` now resets queue/registry state for both legacy and native frame paths
|
||||||
- hosted-preview surface readiness now keys on published texture availability instead of ImGui-style descriptor validity
|
- hosted-preview surface readiness now keys on published texture availability instead of ImGui-style descriptor validity
|
||||||
@@ -266,19 +272,38 @@ Current gap:
|
|||||||
- XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash.
|
- XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash.
|
||||||
- `UIDocumentCompiler.cpp` repaired enough to restore full local builds after the duplicated schema-helper regression.
|
- `UIDocumentCompiler.cpp` repaired enough to restore full local builds after the duplicated schema-helper regression.
|
||||||
- MSVC debug build hardening was tightened again so large parallel `engine` rebuilds stop tripping over compile-PDB contention.
|
- MSVC debug build hardening was tightened again so large parallel `engine` rebuilds stop tripping over compile-PDB contention.
|
||||||
|
- `XCUIStandaloneTextAtlasProvider` no longer uses ImGui font-atlas/internal baking helpers:
|
||||||
|
- atlas ownership now stays inside a standalone provider implementation built on Windows/GDI glyph rasterization
|
||||||
|
- default editor atlas prewarms the current supported nominal sizes, exposes both `RGBA32` and `Alpha8` atlas views, lazily inserts non-prebaked BMP glyphs, and falls back to `?` for unrasterizable codepoints
|
||||||
|
- standalone atlas coverage now includes reset/rebuild, non-nominal size resolution, lazy glyph insertion/fallback behavior, and smoke use without any ImGui context
|
||||||
|
|
||||||
## Phase Risks Still Open
|
## Phase Risks Still Open
|
||||||
|
|
||||||
- Schema instance validation is still open beyond `.xcschema` self-definition and artifact round-trip coverage.
|
- Schema instance validation is still open beyond `.xcschema` self-definition and artifact round-trip coverage.
|
||||||
- `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet.
|
- `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet.
|
||||||
- The default native text path still builds its font atlas on top of ImGui font types/internal baking helpers.
|
- The default native shell path still compiles through an `Application.cpp` unit that directly pulls in ImGui compatibility host code, so the native default path is not yet isolated at the translation-unit boundary.
|
||||||
|
- `new_editor/CMakeLists.txt` still builds the legacy ImGui host/compositor/backend sources and include directories as target-global defaults instead of a compatibility-only slice.
|
||||||
|
- The default native text path now owns its atlas without ImGui, but the provider is still Windows-only and remains trapped inside `new_editor` instead of a shared/cross-platform text layer.
|
||||||
- Hosted-preview compatibility presentation still depends on an ImGui-only inline presenter path when not using the queued native surface path.
|
- Hosted-preview compatibility presentation still depends on an ImGui-only inline presenter path when not using the queued native surface path.
|
||||||
- Editor widget coverage is still prototype-driven inside `LayoutLab`; it has not yet been promoted into a full reusable shared widget/runtime layer with command routing, virtualization, and property-edit transactions.
|
- Editor widget coverage is still prototype-driven inside `LayoutLab`; it has not yet been promoted into a full reusable shared widget/runtime layer with command routing, virtualization, and property-edit transactions.
|
||||||
|
|
||||||
|
## Execution-Plan Alignment
|
||||||
|
|
||||||
|
- Against `XCUI完整架构设计与执行计划.md`, current `new_editor` progress should be treated as an early `Phase 8` foothold rather than full `Milestone E` completion:
|
||||||
|
- landed: `NativeWindowUICompositor`, native shell packet composition, native hosted-preview publication, XCUI-owned texture registrations, native panel surface-image presentation, standalone native text-atlas ownership inside `new_editor`
|
||||||
|
- not yet landed: default-path translation-unit isolation from legacy ImGui host code, promotion of the native text-atlas path into a shared/cross-platform text subsystem, shared XCUI-authored editor shell chrome
|
||||||
|
- That means the next de-ImGui push should not keep centering on hosted-preview publication; that milestone is now effectively closed for the default native shell path.
|
||||||
|
- The real remaining default-path blockers are:
|
||||||
|
- isolate legacy ImGui shell/compositor/presenter wiring from the default `Application` build path
|
||||||
|
- stop treating ImGui include paths and backend sources as the default `new_editor` compilation surface
|
||||||
|
- harden and promote `XCUIStandaloneTextAtlasProvider` / editor font bootstrap into a shared native text subsystem
|
||||||
|
- move native shell chrome out of bespoke `Application` layout code and into a shared XCUI shell model or authored shell document
|
||||||
|
|
||||||
## Next Phase
|
## Next Phase
|
||||||
|
|
||||||
1. Expand runtime/game-layer ownership from the current `SceneRuntime` UI context into scene-declared HUD/menu bootstrapping, draw submission, and higher-level runtime UI policies.
|
1. Expand runtime/game-layer ownership from the current `SceneRuntime` UI context into scene-declared HUD/menu bootstrapping, draw submission, and higher-level runtime UI policies.
|
||||||
2. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu chrome, shell-state adoption, virtualization, and broader focus/multi-selection behavior.
|
2. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu chrome, shell-state adoption, virtualization, and broader focus/multi-selection behavior.
|
||||||
3. Strip the remaining default-path ImGui seams down to compatibility-only code, starting with the standalone text/font-atlas path and then the remaining hosted-preview fallback layers.
|
3. Isolate legacy ImGui shell/compositor/presenter wiring from the default native path first, starting with `Application` translation-unit separation and a `new_editor` build split that stops treating ImGui include paths and sources as the default compilation surface.
|
||||||
4. Promote the native shell chrome and card layout out of bespoke `Application` code into a shared XCUI/editor-layer shell model or authored shell document.
|
4. Promote the current standalone native text/font path out of `new_editor`, harden its atlas invalidation/caching contract, and remove the remaining default-path ImGui text/bootstrap ownership around compatibility-only code.
|
||||||
5. Continue phased validation, commit, push, and plan refresh after each stable batch.
|
5. Promote the native shell chrome and card layout out of bespoke `Application` code into a shared XCUI/editor-layer shell model or authored shell document.
|
||||||
|
6. Continue phased validation, commit, push, and plan refresh after each stable batch.
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h"
|
#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 XCEngine {
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
@@ -8,65 +23,688 @@ namespace XCUIBackend {
|
|||||||
|
|
||||||
namespace {
|
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 = {};
|
IXCUITextAtlasProvider::FontHandle handle = {};
|
||||||
handle.value = reinterpret_cast<std::uintptr_t>(font);
|
handle.value = 1u;
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImFont* ResolveFontHandle(IXCUITextAtlasProvider::FontHandle handle) {
|
bool IsDefaultFontHandle(IXCUITextAtlasProvider::FontHandle handle) {
|
||||||
return reinterpret_cast<ImFont*>(handle.value);
|
return handle.value == MakeDefaultFontHandle().value;
|
||||||
}
|
}
|
||||||
|
|
||||||
float ResolveNominalFontSize(const ImFont& font) {
|
std::size_t ResolveFontIndex(IXCUITextAtlasProvider::FontHandle handle) {
|
||||||
return font.LegacySize > 0.0f ? font.LegacySize : 16.0f;
|
return IsDefaultFontHandle(handle) ? 0u : kInvalidFontIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
float ResolveRequestedFontSize(const ImFont& font, float requestedFontSize) {
|
std::uint64_t MakeGlyphKey(int sizeKey, std::uint32_t codepoint) {
|
||||||
return requestedFontSize > 0.0f ? requestedFontSize : ResolveNominalFontSize(font);
|
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) {
|
void AppendCodepointRange(
|
||||||
return font != nullptr && atlas != nullptr && font->OwnerAtlas == atlas;
|
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(
|
std::vector<std::uint32_t> BuildPrebakedCodepointSet() {
|
||||||
IXCUITextAtlasProvider::FontHandle fontHandle,
|
std::vector<std::uint32_t> codepoints = {};
|
||||||
ImFontAtlas* atlas,
|
codepoints.reserve(768u);
|
||||||
float requestedFontSize) {
|
for (const auto& range : kPrebakedCodepointRanges) {
|
||||||
ImFont* font = ResolveFontHandle(fontHandle);
|
AppendCodepointRange(codepoints, range.first, range.second);
|
||||||
if (!IsFontOwnedByAtlas(font, atlas)) {
|
}
|
||||||
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float resolvedFontSize = ResolveRequestedFontSize(*font, requestedFontSize);
|
int m_sizeKey = 0;
|
||||||
if (resolvedFontSize <= 0.0f) {
|
HDC m_dc = nullptr;
|
||||||
return 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
|
} // 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();
|
RebuildDefaultEditorAtlas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XCUIStandaloneTextAtlasProvider::~XCUIStandaloneTextAtlasProvider() = default;
|
||||||
|
|
||||||
void XCUIStandaloneTextAtlasProvider::Reset() {
|
void XCUIStandaloneTextAtlasProvider::Reset() {
|
||||||
m_atlas.Clear();
|
if (m_impl == nullptr) {
|
||||||
m_defaultFont = nullptr;
|
m_impl = std::make_unique<Impl>();
|
||||||
m_ready = false;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetImpl(*m_impl);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool XCUIStandaloneTextAtlasProvider::RebuildDefaultEditorAtlas() {
|
bool XCUIStandaloneTextAtlasProvider::RebuildDefaultEditorAtlas() {
|
||||||
Reset();
|
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 {
|
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(
|
bool XCUIStandaloneTextAtlasProvider::GetAtlasTextureView(
|
||||||
@@ -74,74 +712,59 @@ bool XCUIStandaloneTextAtlasProvider::GetAtlasTextureView(
|
|||||||
AtlasTextureView& outView) const {
|
AtlasTextureView& outView) const {
|
||||||
outView = {};
|
outView = {};
|
||||||
|
|
||||||
ImFontAtlas* atlas = ResolveAtlas();
|
if (!IsReady() || m_impl == nullptr) {
|
||||||
if (atlas == nullptr) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned char* pixels = nullptr;
|
const CachedPixelBuffer* resolvedPixels = nullptr;
|
||||||
int width = 0;
|
PixelFormat resolvedFormat = PixelFormat::Unknown;
|
||||||
int height = 0;
|
if (preferredFormat == PixelFormat::Alpha8 && m_impl->alpha8Pixels.IsValid()) {
|
||||||
int bytesPerPixel = 0;
|
resolvedPixels = &m_impl->alpha8Pixels;
|
||||||
PixelFormat resolvedFormat = preferredFormat;
|
|
||||||
|
|
||||||
switch (preferredFormat) {
|
|
||||||
case PixelFormat::Alpha8:
|
|
||||||
atlas->GetTexDataAsAlpha8(&pixels, &width, &height, &bytesPerPixel);
|
|
||||||
resolvedFormat = PixelFormat::Alpha8;
|
resolvedFormat = PixelFormat::Alpha8;
|
||||||
break;
|
} else if (m_impl->rgba32Pixels.IsValid()) {
|
||||||
case PixelFormat::RGBA32:
|
resolvedPixels = &m_impl->rgba32Pixels;
|
||||||
case PixelFormat::Unknown:
|
|
||||||
default:
|
|
||||||
atlas->GetTexDataAsRGBA32(&pixels, &width, &height, &bytesPerPixel);
|
|
||||||
resolvedFormat = PixelFormat::RGBA32;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
outView.pixels = pixels;
|
outView.pixels = resolvedPixels->bytes.data();
|
||||||
outView.width = width;
|
outView.width = resolvedPixels->width;
|
||||||
outView.height = height;
|
outView.height = resolvedPixels->height;
|
||||||
outView.stride = width * bytesPerPixel;
|
outView.stride = resolvedPixels->width * resolvedPixels->bytesPerPixel;
|
||||||
outView.bytesPerPixel = bytesPerPixel;
|
outView.bytesPerPixel = resolvedPixels->bytesPerPixel;
|
||||||
outView.format = resolvedFormat;
|
outView.format = resolvedFormat;
|
||||||
outView.atlasStorageKey = reinterpret_cast<std::uintptr_t>(atlas);
|
outView.atlasStorageKey = reinterpret_cast<std::uintptr_t>(m_impl.get());
|
||||||
outView.pixelDataKey = reinterpret_cast<std::uintptr_t>(pixels);
|
outView.pixelDataKey = MakePixelDataKey(*resolvedPixels, m_impl->pixelGeneration);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t XCUIStandaloneTextAtlasProvider::GetFontCount() const {
|
std::size_t XCUIStandaloneTextAtlasProvider::GetFontCount() const {
|
||||||
const ImFontAtlas* atlas = ResolveAtlas();
|
return IsReady() ? 1u : 0u;
|
||||||
return atlas != nullptr ? static_cast<std::size_t>(atlas->Fonts.Size) : 0u;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetFont(std::size_t index) const {
|
IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetFont(std::size_t index) const {
|
||||||
const ImFontAtlas* atlas = ResolveAtlas();
|
return IsReady() && index == 0u ? MakeDefaultFontHandle() : FontHandle{};
|
||||||
if (atlas == nullptr || index >= static_cast<std::size_t>(atlas->Fonts.Size)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return MakeFontHandle(atlas->Fonts[static_cast<int>(index)]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetDefaultFont() const {
|
IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetDefaultFont() const {
|
||||||
return MakeFontHandle(ResolveDefaultFont());
|
return IsReady() ? MakeDefaultFontHandle() : FontHandle{};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool XCUIStandaloneTextAtlasProvider::GetFontInfo(FontHandle fontHandle, FontInfo& outInfo) const {
|
bool XCUIStandaloneTextAtlasProvider::GetFontInfo(FontHandle fontHandle, FontInfo& outInfo) const {
|
||||||
outInfo = {};
|
outInfo = {};
|
||||||
|
|
||||||
ImFontAtlas* atlas = ResolveAtlas();
|
if (!IsReady() || ResolveFontIndex(fontHandle) == kInvalidFontIndex || m_impl == nullptr) {
|
||||||
ImFont* font = ResolveFontHandle(fontHandle);
|
|
||||||
if (!IsFontOwnedByAtlas(font, atlas)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
outInfo.handle = fontHandle;
|
outInfo.handle = fontHandle;
|
||||||
outInfo.nominalSize = ResolveNominalFontSize(*font);
|
outInfo.nominalSize = m_impl->nominalSize;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,15 +774,17 @@ bool XCUIStandaloneTextAtlasProvider::GetBakedFontInfo(
|
|||||||
BakedFontInfo& outInfo) const {
|
BakedFontInfo& outInfo) const {
|
||||||
outInfo = {};
|
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) {
|
if (bakedFont == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
outInfo.lineHeight = bakedFont->Size;
|
outInfo = bakedFont->metrics;
|
||||||
outInfo.ascent = bakedFont->Ascent;
|
|
||||||
outInfo.descent = bakedFont->Descent;
|
|
||||||
outInfo.rasterizerDensity = bakedFont->RasterizerDensity;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,53 +795,48 @@ bool XCUIStandaloneTextAtlasProvider::FindGlyph(
|
|||||||
GlyphInfo& outInfo) const {
|
GlyphInfo& outInfo) const {
|
||||||
outInfo = {};
|
outInfo = {};
|
||||||
|
|
||||||
if (codepoint > IM_UNICODE_CODEPOINT_MAX) {
|
if (!IsReady() || ResolveFontIndex(fontHandle) == kInvalidFontIndex || m_impl == nullptr) {
|
||||||
return false;
|
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) {
|
if (bakedFont == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImFontGlyph* glyph = bakedFont->FindGlyph(static_cast<ImWchar>(codepoint));
|
const auto glyphIt = bakedFont->glyphs.find(codepoint);
|
||||||
if (glyph == nullptr) {
|
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;
|
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.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;
|
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 XCUIBackend
|
||||||
} // namespace Editor
|
} // namespace Editor
|
||||||
} // namespace XCEngine
|
} // namespace XCEngine
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include "IXCUITextAtlasProvider.h"
|
#include "IXCUITextAtlasProvider.h"
|
||||||
#include "XCUIEditorFontSetup.h"
|
#include "XCUIEditorFontSetup.h"
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <memory>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
@@ -12,6 +12,7 @@ namespace XCUIBackend {
|
|||||||
class XCUIStandaloneTextAtlasProvider final : public IXCUITextAtlasProvider {
|
class XCUIStandaloneTextAtlasProvider final : public IXCUITextAtlasProvider {
|
||||||
public:
|
public:
|
||||||
XCUIStandaloneTextAtlasProvider();
|
XCUIStandaloneTextAtlasProvider();
|
||||||
|
~XCUIStandaloneTextAtlasProvider();
|
||||||
|
|
||||||
XCUIStandaloneTextAtlasProvider(const XCUIStandaloneTextAtlasProvider&) = delete;
|
XCUIStandaloneTextAtlasProvider(const XCUIStandaloneTextAtlasProvider&) = delete;
|
||||||
XCUIStandaloneTextAtlasProvider& operator=(const XCUIStandaloneTextAtlasProvider&) = delete;
|
XCUIStandaloneTextAtlasProvider& operator=(const XCUIStandaloneTextAtlasProvider&) = delete;
|
||||||
@@ -28,13 +29,10 @@ public:
|
|||||||
bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override;
|
bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override;
|
||||||
bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override;
|
bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override;
|
||||||
|
|
||||||
private:
|
struct Impl;
|
||||||
::ImFontAtlas* ResolveAtlas() const;
|
|
||||||
::ImFont* ResolveDefaultFont() const;
|
|
||||||
|
|
||||||
mutable ::ImFontAtlas m_atlas = {};
|
private:
|
||||||
::ImFont* m_defaultFont = nullptr;
|
std::unique_ptr<Impl> m_impl;
|
||||||
bool m_ready = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace XCUIBackend
|
} // namespace XCUIBackend
|
||||||
|
|||||||
@@ -745,18 +745,10 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER}" AND
|
if(EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER}" AND
|
||||||
EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}" AND
|
EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}")
|
||||||
EXISTS "${NEW_EDITOR_FONT_SETUP_HEADER}" AND
|
|
||||||
EXISTS "${NEW_EDITOR_FONT_SETUP_SOURCE}" AND
|
|
||||||
EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
|
|
||||||
add_executable(new_editor_xcui_standalone_text_atlas_provider_tests
|
add_executable(new_editor_xcui_standalone_text_atlas_provider_tests
|
||||||
test_xcui_standalone_text_atlas_provider.cpp
|
test_xcui_standalone_text_atlas_provider.cpp
|
||||||
${NEW_EDITOR_FONT_SETUP_SOURCE}
|
|
||||||
${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}
|
${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}
|
||||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
|
|
||||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
|
|
||||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
|
|
||||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
xcengine_configure_new_editor_test_target(new_editor_xcui_standalone_text_atlas_provider_tests)
|
xcengine_configure_new_editor_test_target(new_editor_xcui_standalone_text_atlas_provider_tests)
|
||||||
@@ -767,19 +759,18 @@ if(EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER}" AND
|
|||||||
GTest::gtest
|
GTest::gtest
|
||||||
GTest::gtest_main
|
GTest::gtest_main
|
||||||
user32
|
user32
|
||||||
|
gdi32
|
||||||
comdlg32
|
comdlg32
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(new_editor_xcui_standalone_text_atlas_provider_tests PRIVATE
|
target_include_directories(new_editor_xcui_standalone_text_atlas_provider_tests PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/engine/include
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
${CMAKE_SOURCE_DIR}/new_editor/src
|
${CMAKE_SOURCE_DIR}/new_editor/src
|
||||||
${CMAKE_BINARY_DIR}/_deps/imgui-src
|
|
||||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
|
|
||||||
)
|
)
|
||||||
|
|
||||||
xcengine_discover_new_editor_gtests(new_editor_xcui_standalone_text_atlas_provider_tests)
|
xcengine_discover_new_editor_gtests(new_editor_xcui_standalone_text_atlas_provider_tests)
|
||||||
else()
|
else()
|
||||||
message(STATUS "Skipping new_editor_xcui_standalone_text_atlas_provider_tests because standalone atlas provider, font setup, or ImGui sources are missing.")
|
message(STATUS "Skipping new_editor_xcui_standalone_text_atlas_provider_tests because standalone atlas provider files are missing.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER}" AND
|
if(EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER}" AND
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ using XCEngine::Editor::XCUIBackend::XCUIStandaloneTextAtlasProvider;
|
|||||||
|
|
||||||
TEST(XCUIStandaloneTextAtlasProviderTest, BuildsDefaultEditorAtlasWithoutImGuiContext) {
|
TEST(XCUIStandaloneTextAtlasProviderTest, BuildsDefaultEditorAtlasWithoutImGuiContext) {
|
||||||
XCUIStandaloneTextAtlasProvider provider = {};
|
XCUIStandaloneTextAtlasProvider provider = {};
|
||||||
|
|
||||||
IXCUITextAtlasProvider::AtlasTextureView atlasView = {};
|
IXCUITextAtlasProvider::AtlasTextureView atlasView = {};
|
||||||
|
IXCUITextAtlasProvider::AtlasTextureView alphaView = {};
|
||||||
ASSERT_TRUE(provider.IsReady());
|
ASSERT_TRUE(provider.IsReady());
|
||||||
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView));
|
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView));
|
||||||
|
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::Alpha8, alphaView));
|
||||||
EXPECT_TRUE(atlasView.IsValid());
|
EXPECT_TRUE(atlasView.IsValid());
|
||||||
|
EXPECT_TRUE(alphaView.IsValid());
|
||||||
EXPECT_EQ(atlasView.format, IXCUITextAtlasProvider::PixelFormat::RGBA32);
|
EXPECT_EQ(atlasView.format, IXCUITextAtlasProvider::PixelFormat::RGBA32);
|
||||||
|
EXPECT_EQ(alphaView.format, IXCUITextAtlasProvider::PixelFormat::Alpha8);
|
||||||
EXPECT_GT(provider.GetFontCount(), 0u);
|
EXPECT_GT(provider.GetFontCount(), 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,4 +45,113 @@ TEST(XCUIStandaloneTextAtlasProviderTest, ExposesDefaultFontMetricsAndGlyphs) {
|
|||||||
EXPECT_GE(glyphInfo.v1, glyphInfo.v0);
|
EXPECT_GE(glyphInfo.v1, glyphInfo.v0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(XCUIStandaloneTextAtlasProviderTest, RebuildsAfterResetAndRejectsQueriesWhileUnready) {
|
||||||
|
XCUIStandaloneTextAtlasProvider provider = {};
|
||||||
|
const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont();
|
||||||
|
ASSERT_TRUE(defaultFont.IsValid());
|
||||||
|
|
||||||
|
provider.Reset();
|
||||||
|
|
||||||
|
IXCUITextAtlasProvider::AtlasTextureView atlasView = {};
|
||||||
|
IXCUITextAtlasProvider::FontInfo fontInfo = {};
|
||||||
|
IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {};
|
||||||
|
IXCUITextAtlasProvider::GlyphInfo glyphInfo = {};
|
||||||
|
|
||||||
|
EXPECT_FALSE(provider.IsReady());
|
||||||
|
EXPECT_FALSE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView));
|
||||||
|
EXPECT_EQ(provider.GetFontCount(), 0u);
|
||||||
|
EXPECT_FALSE(provider.GetDefaultFont().IsValid());
|
||||||
|
EXPECT_FALSE(provider.GetFontInfo(defaultFont, fontInfo));
|
||||||
|
EXPECT_FALSE(provider.GetBakedFontInfo(defaultFont, 18.0f, bakedFontInfo));
|
||||||
|
EXPECT_FALSE(provider.FindGlyph(defaultFont, 18.0f, static_cast<std::uint32_t>('A'), glyphInfo));
|
||||||
|
|
||||||
|
ASSERT_TRUE(provider.RebuildDefaultEditorAtlas());
|
||||||
|
ASSERT_TRUE(provider.IsReady());
|
||||||
|
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView));
|
||||||
|
EXPECT_TRUE(atlasView.IsValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(XCUIStandaloneTextAtlasProviderTest, ResolvesGlyphsForNonNominalRequestedFontSizes) {
|
||||||
|
XCUIStandaloneTextAtlasProvider provider = {};
|
||||||
|
const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont();
|
||||||
|
ASSERT_TRUE(defaultFont.IsValid());
|
||||||
|
|
||||||
|
IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {};
|
||||||
|
ASSERT_TRUE(provider.GetBakedFontInfo(defaultFont, 13.0f, bakedFontInfo));
|
||||||
|
EXPECT_GT(bakedFontInfo.lineHeight, 0.0f);
|
||||||
|
EXPECT_GT(bakedFontInfo.rasterizerDensity, 0.0f);
|
||||||
|
|
||||||
|
IXCUITextAtlasProvider::GlyphInfo glyphInfo = {};
|
||||||
|
ASSERT_TRUE(provider.FindGlyph(defaultFont, 13.0f, static_cast<std::uint32_t>('A'), glyphInfo));
|
||||||
|
EXPECT_EQ(glyphInfo.requestedCodepoint, static_cast<std::uint32_t>('A'));
|
||||||
|
EXPECT_GT(glyphInfo.advanceX, 0.0f);
|
||||||
|
EXPECT_GE(glyphInfo.u1, glyphInfo.u0);
|
||||||
|
EXPECT_GE(glyphInfo.v1, glyphInfo.v0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(XCUIStandaloneTextAtlasProviderTest, LazilyAddsGlyphsOutsideThePrebakedAsciiRanges) {
|
||||||
|
XCUIStandaloneTextAtlasProvider provider = {};
|
||||||
|
const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont();
|
||||||
|
ASSERT_TRUE(defaultFont.IsValid());
|
||||||
|
|
||||||
|
IXCUITextAtlasProvider::AtlasTextureView beforeView = {};
|
||||||
|
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, beforeView));
|
||||||
|
|
||||||
|
constexpr std::uint32_t dynamicCodepoint = 0x4E2Du;
|
||||||
|
IXCUITextAtlasProvider::GlyphInfo glyphInfo = {};
|
||||||
|
ASSERT_TRUE(provider.FindGlyph(defaultFont, 16.0f, dynamicCodepoint, glyphInfo));
|
||||||
|
|
||||||
|
IXCUITextAtlasProvider::AtlasTextureView refreshedView = {};
|
||||||
|
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, refreshedView));
|
||||||
|
EXPECT_EQ(refreshedView.atlasStorageKey, beforeView.atlasStorageKey);
|
||||||
|
EXPECT_EQ(glyphInfo.requestedCodepoint, dynamicCodepoint);
|
||||||
|
|
||||||
|
if (refreshedView.pixelDataKey != beforeView.pixelDataKey) {
|
||||||
|
EXPECT_EQ(glyphInfo.resolvedCodepoint, dynamicCodepoint);
|
||||||
|
EXPECT_GT(glyphInfo.advanceX, 0.0f);
|
||||||
|
EXPECT_TRUE(glyphInfo.visible);
|
||||||
|
|
||||||
|
IXCUITextAtlasProvider::GlyphInfo secondLookup = {};
|
||||||
|
ASSERT_TRUE(provider.FindGlyph(defaultFont, 16.0f, dynamicCodepoint, secondLookup));
|
||||||
|
EXPECT_EQ(secondLookup.requestedCodepoint, dynamicCodepoint);
|
||||||
|
EXPECT_EQ(secondLookup.resolvedCodepoint, dynamicCodepoint);
|
||||||
|
|
||||||
|
IXCUITextAtlasProvider::AtlasTextureView secondView = {};
|
||||||
|
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, secondView));
|
||||||
|
EXPECT_EQ(secondView.pixelDataKey, refreshedView.pixelDataKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glyphInfo.resolvedCodepoint != dynamicCodepoint) {
|
||||||
|
EXPECT_EQ(glyphInfo.resolvedCodepoint, static_cast<std::uint32_t>('?'));
|
||||||
|
EXPECT_GT(glyphInfo.advanceX, 0.0f);
|
||||||
|
EXPECT_TRUE(glyphInfo.visible);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!glyphInfo.visible) {
|
||||||
|
EXPECT_GE(glyphInfo.advanceX, 0.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glyphInfo.resolvedCodepoint == dynamicCodepoint) {
|
||||||
|
EXPECT_NE(refreshedView.pixelDataKey, beforeView.pixelDataKey);
|
||||||
|
}
|
||||||
|
EXPECT_GT(glyphInfo.advanceX, 0.0f);
|
||||||
|
EXPECT_TRUE(glyphInfo.visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(XCUIStandaloneTextAtlasProviderTest, MissingGlyphFallsBackToQuestionMarkWhenFontCannotRasterizeIt) {
|
||||||
|
XCUIStandaloneTextAtlasProvider provider = {};
|
||||||
|
const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont();
|
||||||
|
ASSERT_TRUE(defaultFont.IsValid());
|
||||||
|
|
||||||
|
IXCUITextAtlasProvider::GlyphInfo glyphInfo = {};
|
||||||
|
ASSERT_TRUE(provider.FindGlyph(defaultFont, 16.0f, 0x10FFFFu, glyphInfo));
|
||||||
|
EXPECT_EQ(glyphInfo.requestedCodepoint, 0x10FFFFu);
|
||||||
|
EXPECT_EQ(glyphInfo.resolvedCodepoint, static_cast<std::uint32_t>('?'));
|
||||||
|
EXPECT_GT(glyphInfo.advanceX, 0.0f);
|
||||||
|
EXPECT_TRUE(glyphInfo.visible);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
Reference in New Issue
Block a user