Build XCUI splitter foundation and test harness

This commit is contained in:
2026-04-06 03:17:53 +08:00
parent dc17685099
commit c7dc8d7484
77 changed files with 4749 additions and 542 deletions

View File

@@ -0,0 +1,52 @@
#pragma once
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEngine/UI/DrawData.h>
#include <cstdint>
#include <filesystem>
#include <string>
#include <string_view>
namespace XCEngine::XCUI::Host {
class NativeRenderer;
class AutoScreenshotController {
public:
void Initialize(const std::filesystem::path& captureRoot);
void Shutdown();
void RequestCapture(std::string reason);
void CaptureIfRequested(
NativeRenderer& renderer,
const ::XCEngine::UI::UIDrawData& drawData,
unsigned int width,
unsigned int height,
bool framePresented);
bool HasPendingCapture() const;
const std::filesystem::path& GetLatestCapturePath() const;
const std::string& GetLastCaptureSummary() const;
const std::string& GetLastCaptureError() const;
private:
std::filesystem::path BuildHistoryCapturePath(std::string_view reason) const;
static std::string BuildTimestampString();
static std::string SanitizeReason(std::string_view reason);
std::filesystem::path m_captureRoot = {};
std::filesystem::path m_historyRoot = {};
std::filesystem::path m_latestCapturePath = {};
std::string m_pendingReason = {};
std::string m_lastCaptureSummary = {};
std::string m_lastCaptureError = {};
std::uint64_t m_captureCount = 0;
bool m_capturePending = false;
};
} // namespace XCEngine::XCUI::Host

View File

@@ -0,0 +1,173 @@
#pragma once
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEngine/UI/Types.h>
#include <windows.h>
#include <cstddef>
#include <cstdint>
namespace XCEngine::XCUI::Host {
class InputModifierTracker {
public:
void Reset() {
m_leftShift = false;
m_rightShift = false;
m_leftControl = false;
m_rightControl = false;
m_leftAlt = false;
m_rightAlt = false;
m_leftSuper = false;
m_rightSuper = false;
}
void SyncFromSystemState() {
m_leftShift = (GetKeyState(VK_LSHIFT) & 0x8000) != 0;
m_rightShift = (GetKeyState(VK_RSHIFT) & 0x8000) != 0;
m_leftControl = (GetKeyState(VK_LCONTROL) & 0x8000) != 0;
m_rightControl = (GetKeyState(VK_RCONTROL) & 0x8000) != 0;
m_leftAlt = (GetKeyState(VK_LMENU) & 0x8000) != 0;
m_rightAlt = (GetKeyState(VK_RMENU) & 0x8000) != 0;
m_leftSuper = (GetKeyState(VK_LWIN) & 0x8000) != 0;
m_rightSuper = (GetKeyState(VK_RWIN) & 0x8000) != 0;
}
::XCEngine::UI::UIInputModifiers GetCurrentModifiers() const {
return BuildModifiers();
}
::XCEngine::UI::UIInputModifiers BuildPointerModifiers(std::size_t wParam) const {
::XCEngine::UI::UIInputModifiers modifiers = BuildModifiers();
modifiers.shift = modifiers.shift || (wParam & MK_SHIFT) != 0;
modifiers.control = modifiers.control || (wParam & MK_CONTROL) != 0;
return modifiers;
}
::XCEngine::UI::UIInputModifiers ApplyKeyMessage(
::XCEngine::UI::UIInputEventType type,
WPARAM wParam,
LPARAM lParam) {
if (type == ::XCEngine::UI::UIInputEventType::KeyDown) {
SetModifierState(ResolveModifierKey(wParam, lParam), true);
} else if (type == ::XCEngine::UI::UIInputEventType::KeyUp) {
SetModifierState(ResolveModifierKey(wParam, lParam), false);
}
return BuildModifiers();
}
private:
enum class ModifierKey : std::uint8_t {
None = 0,
LeftShift,
RightShift,
LeftControl,
RightControl,
LeftAlt,
RightAlt,
LeftSuper,
RightSuper
};
static bool IsExtendedKey(LPARAM lParam) {
return (static_cast<std::uint32_t>(lParam) & 0x01000000u) != 0u;
}
static std::uint32_t ExtractScanCode(LPARAM lParam) {
return (static_cast<std::uint32_t>(lParam) >> 16u) & 0xffu;
}
static ModifierKey ResolveModifierKey(WPARAM wParam, LPARAM lParam) {
switch (static_cast<std::uint32_t>(wParam)) {
case VK_SHIFT: {
const UINT shiftVirtualKey = MapVirtualKeyW(ExtractScanCode(lParam), MAPVK_VSC_TO_VK_EX);
return shiftVirtualKey == VK_RSHIFT
? ModifierKey::RightShift
: ModifierKey::LeftShift;
}
case VK_LSHIFT:
return ModifierKey::LeftShift;
case VK_RSHIFT:
return ModifierKey::RightShift;
case VK_CONTROL:
return IsExtendedKey(lParam)
? ModifierKey::RightControl
: ModifierKey::LeftControl;
case VK_LCONTROL:
return ModifierKey::LeftControl;
case VK_RCONTROL:
return ModifierKey::RightControl;
case VK_MENU:
return IsExtendedKey(lParam)
? ModifierKey::RightAlt
: ModifierKey::LeftAlt;
case VK_LMENU:
return ModifierKey::LeftAlt;
case VK_RMENU:
return ModifierKey::RightAlt;
case VK_LWIN:
return ModifierKey::LeftSuper;
case VK_RWIN:
return ModifierKey::RightSuper;
default:
return ModifierKey::None;
}
}
void SetModifierState(ModifierKey key, bool pressed) {
switch (key) {
case ModifierKey::LeftShift:
m_leftShift = pressed;
break;
case ModifierKey::RightShift:
m_rightShift = pressed;
break;
case ModifierKey::LeftControl:
m_leftControl = pressed;
break;
case ModifierKey::RightControl:
m_rightControl = pressed;
break;
case ModifierKey::LeftAlt:
m_leftAlt = pressed;
break;
case ModifierKey::RightAlt:
m_rightAlt = pressed;
break;
case ModifierKey::LeftSuper:
m_leftSuper = pressed;
break;
case ModifierKey::RightSuper:
m_rightSuper = pressed;
break;
case ModifierKey::None:
default:
break;
}
}
::XCEngine::UI::UIInputModifiers BuildModifiers() const {
::XCEngine::UI::UIInputModifiers modifiers = {};
modifiers.shift = m_leftShift || m_rightShift;
modifiers.control = m_leftControl || m_rightControl;
modifiers.alt = m_leftAlt || m_rightAlt;
modifiers.super = m_leftSuper || m_rightSuper;
return modifiers;
}
bool m_leftShift = false;
bool m_rightShift = false;
bool m_leftControl = false;
bool m_rightControl = false;
bool m_leftAlt = false;
bool m_rightAlt = false;
bool m_leftSuper = false;
bool m_rightSuper = false;
};
} // namespace XCEngine::XCUI::Host

View File

@@ -0,0 +1,65 @@
#pragma once
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEngine/UI/DrawData.h>
#include <d2d1.h>
#include <dwrite.h>
#include <wincodec.h>
#include <windows.h>
#include <wrl/client.h>
#include <filesystem>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace XCEngine::XCUI::Host {
class NativeRenderer {
public:
bool Initialize(HWND hwnd);
void Shutdown();
void Resize(UINT width, UINT height);
bool Render(const ::XCEngine::UI::UIDrawData& drawData);
bool CaptureToPng(
const ::XCEngine::UI::UIDrawData& drawData,
UINT width,
UINT height,
const std::filesystem::path& outputPath,
std::string& outError);
private:
bool EnsureRenderTarget();
bool EnsureWicFactory(std::string& outError);
void DiscardRenderTarget();
bool CreateDeviceResources();
bool RenderToTarget(
ID2D1RenderTarget& renderTarget,
ID2D1SolidColorBrush& solidBrush,
const ::XCEngine::UI::UIDrawData& drawData);
void RenderCommand(
ID2D1RenderTarget& renderTarget,
ID2D1SolidColorBrush& solidBrush,
const ::XCEngine::UI::UIDrawCommand& command,
std::vector<D2D1_RECT_F>& clipStack);
IDWriteTextFormat* GetTextFormat(float fontSize);
static D2D1_COLOR_F ToD2DColor(const ::XCEngine::UI::UIColor& color);
static std::wstring Utf8ToWide(std::string_view text);
HWND m_hwnd = nullptr;
Microsoft::WRL::ComPtr<ID2D1Factory> m_d2dFactory;
Microsoft::WRL::ComPtr<IDWriteFactory> m_dwriteFactory;
Microsoft::WRL::ComPtr<IWICImagingFactory> m_wicFactory;
Microsoft::WRL::ComPtr<ID2D1HwndRenderTarget> m_renderTarget;
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> m_solidBrush;
std::unordered_map<int, Microsoft::WRL::ComPtr<IDWriteTextFormat>> m_textFormats;
bool m_wicComInitialized = false;
};
} // namespace XCEngine::XCUI::Host

View File

@@ -0,0 +1,41 @@
#pragma once
#include <XCEngine/UI/Style/Theme.h>
#include <cstdint>
#include <string_view>
namespace XCEngine {
namespace UI {
namespace Widgets {
enum class UIEditorCollectionPrimitiveKind : std::uint8_t {
None = 0,
ScrollView,
TreeView,
TreeItem,
ListView,
ListItem,
PropertySection,
FieldRow
};
UIEditorCollectionPrimitiveKind ClassifyUIEditorCollectionPrimitive(std::string_view tagName);
bool IsUIEditorCollectionPrimitiveContainer(UIEditorCollectionPrimitiveKind kind);
bool UsesUIEditorCollectionPrimitiveColumnLayout(UIEditorCollectionPrimitiveKind kind);
bool IsUIEditorCollectionPrimitiveHoverable(UIEditorCollectionPrimitiveKind kind);
bool DoesUIEditorCollectionPrimitiveClipChildren(UIEditorCollectionPrimitiveKind kind);
float ResolveUIEditorCollectionPrimitivePadding(
UIEditorCollectionPrimitiveKind kind,
const Style::UITheme& theme);
float ResolveUIEditorCollectionPrimitiveDefaultHeight(
UIEditorCollectionPrimitiveKind kind,
const Style::UITheme& theme);
float ResolveUIEditorCollectionPrimitiveIndent(
UIEditorCollectionPrimitiveKind kind,
const Style::UITheme& theme,
float indentLevel);
} // namespace Widgets
} // namespace UI
} // namespace XCEngine

View File

@@ -0,0 +1,126 @@
#pragma once
#include <XCEngine/UI/DrawData.h>
#include <string>
#include <string_view>
namespace XCEngine {
namespace UI {
namespace Widgets {
struct UIEditorPanelChromeState {
bool active = false;
bool hovered = false;
};
struct UIEditorPanelChromeText {
std::string_view title = {};
std::string_view subtitle = {};
std::string_view footer = {};
};
struct UIEditorPanelChromeMetrics {
float cornerRounding = 18.0f;
float headerHeight = 42.0f;
float titleInsetX = 16.0f;
float titleInsetY = 12.0f;
float subtitleInsetY = 28.0f;
float footerInsetX = 16.0f;
float footerInsetBottom = 18.0f;
float activeBorderThickness = 2.0f;
float inactiveBorderThickness = 1.0f;
};
struct UIEditorPanelChromePalette {
UIColor surfaceColor = UIColor(9.0f / 255.0f, 13.0f / 255.0f, 18.0f / 255.0f, 212.0f / 255.0f);
UIColor borderColor = UIColor(53.0f / 255.0f, 72.0f / 255.0f, 96.0f / 255.0f, 1.0f);
UIColor accentColor = UIColor(84.0f / 255.0f, 176.0f / 255.0f, 244.0f / 255.0f, 1.0f);
UIColor hoveredAccentColor = UIColor(1.0f, 206.0f / 255.0f, 112.0f / 255.0f, 1.0f);
UIColor headerColor = UIColor(13.0f / 255.0f, 20.0f / 255.0f, 28.0f / 255.0f, 242.0f / 255.0f);
UIColor textPrimary = UIColor(232.0f / 255.0f, 238.0f / 255.0f, 246.0f / 255.0f, 1.0f);
UIColor textSecondary = UIColor(150.0f / 255.0f, 164.0f / 255.0f, 184.0f / 255.0f, 1.0f);
UIColor textMuted = UIColor(108.0f / 255.0f, 123.0f / 255.0f, 145.0f / 255.0f, 1.0f);
};
inline UIRect BuildUIEditorPanelChromeHeaderRect(
const UIRect& panelRect,
const UIEditorPanelChromeMetrics& metrics = {}) {
return UIRect(
panelRect.x,
panelRect.y,
panelRect.width,
metrics.headerHeight);
}
inline UIColor ResolveUIEditorPanelChromeBorderColor(
const UIEditorPanelChromeState& state,
const UIEditorPanelChromePalette& palette = {}) {
if (state.active) {
return palette.accentColor;
}
if (state.hovered) {
return palette.hoveredAccentColor;
}
return palette.borderColor;
}
inline float ResolveUIEditorPanelChromeBorderThickness(
const UIEditorPanelChromeState& state,
const UIEditorPanelChromeMetrics& metrics = {}) {
return state.active
? metrics.activeBorderThickness
: metrics.inactiveBorderThickness;
}
inline void AppendUIEditorPanelChromeBackground(
UIDrawList& drawList,
const UIRect& panelRect,
const UIEditorPanelChromeState& state,
const UIEditorPanelChromePalette& palette = {},
const UIEditorPanelChromeMetrics& metrics = {}) {
drawList.AddFilledRect(panelRect, palette.surfaceColor, metrics.cornerRounding);
drawList.AddRectOutline(
panelRect,
ResolveUIEditorPanelChromeBorderColor(state, palette),
ResolveUIEditorPanelChromeBorderThickness(state, metrics),
metrics.cornerRounding);
drawList.AddFilledRect(
BuildUIEditorPanelChromeHeaderRect(panelRect, metrics),
palette.headerColor,
metrics.cornerRounding);
}
inline void AppendUIEditorPanelChromeForeground(
UIDrawList& drawList,
const UIRect& panelRect,
const UIEditorPanelChromeText& text,
const UIEditorPanelChromePalette& palette = {},
const UIEditorPanelChromeMetrics& metrics = {}) {
if (!text.title.empty()) {
drawList.AddText(
UIPoint(panelRect.x + metrics.titleInsetX, panelRect.y + metrics.titleInsetY),
std::string(text.title),
palette.textPrimary);
}
if (!text.subtitle.empty()) {
drawList.AddText(
UIPoint(panelRect.x + metrics.titleInsetX, panelRect.y + metrics.subtitleInsetY),
std::string(text.subtitle),
palette.textSecondary);
}
if (!text.footer.empty()) {
drawList.AddText(
UIPoint(panelRect.x + metrics.footerInsetX, panelRect.y + panelRect.height - metrics.footerInsetBottom),
std::string(text.footer),
palette.textMuted);
}
}
} // namespace Widgets
} // namespace UI
} // namespace XCEngine