feat(xcui): advance core and editor validation flow

This commit is contained in:
2026-04-06 16:20:46 +08:00
parent 33bb84f650
commit 2d030a97da
128 changed files with 9961 additions and 773 deletions

View File

@@ -6,6 +6,7 @@
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <XCEngine/UI/Layout/LayoutEngine.h>
#include <XCEngine/UI/Layout/UITabStripLayout.h>
#include <XCEngine/UI/Style/DocumentStyleCompiler.h>
#include <XCEngine/UI/Widgets/UITabStripModel.h>
#include <algorithm>
@@ -34,6 +35,7 @@ using XCEngine::Resources::UIDocumentCompileResult;
using XCEngine::Resources::UIDocumentKind;
using XCEngine::Resources::UIDocumentNode;
namespace Layout = XCEngine::UI::Layout;
namespace Style = XCEngine::UI::Style;
constexpr float kDefaultFontSize = 16.0f;
constexpr float kSmallFontSize = 13.0f;
@@ -78,6 +80,8 @@ struct RuntimeLayoutNode {
bool tabSelected = false;
bool hasShortcutBinding = false;
UIShortcutBinding shortcutBinding = {};
Style::UIStyleSet localStyle = {};
Style::UIResolvedStyle resolvedStyle = {};
enum class ShortcutScopeRoot : std::uint8_t {
None = 0,
Window,
@@ -580,6 +584,166 @@ Layout::UILayoutThickness ParsePadding(
return Layout::UILayoutThickness::Uniform(ParseFloatAttribute(node, "padding", fallback));
}
Layout::UILayoutThickness ToLayoutThickness(const Style::UIThickness& thickness) {
return Layout::UILayoutThickness(
thickness.left,
thickness.top,
thickness.right,
thickness.bottom);
}
const Style::UIStylePropertyResolution* FindResolvedProperty(
const RuntimeLayoutNode& node,
Style::UIStylePropertyId propertyId) {
return node.resolvedStyle.FindProperty(propertyId);
}
float ResolveNodeFontSize(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::FontSize);
if (resolution == nullptr) {
return fallback;
}
if (const float* value = resolution->value.TryGetFloat()) {
return (std::max)(1.0f, *value);
}
return fallback;
}
float ResolveNodeGap(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::Gap);
if (resolution == nullptr) {
return fallback;
}
if (const float* value = resolution->value.TryGetFloat()) {
return (std::max)(0.0f, *value);
}
return fallback;
}
Layout::UILayoutThickness ResolveNodePadding(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::Padding);
if (resolution == nullptr) {
return Layout::UILayoutThickness::Uniform(fallback);
}
if (const Style::UIThickness* thickness = resolution->value.TryGetThickness()) {
return ToLayoutThickness(*thickness);
}
return Layout::UILayoutThickness::Uniform(fallback);
}
float ResolveNodeBorderWidth(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::BorderWidth);
if (resolution == nullptr) {
return fallback;
}
if (const float* value = resolution->value.TryGetFloat()) {
return (std::max)(0.0f, *value);
}
return fallback;
}
float ResolveNodeCornerRadius(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::CornerRadius);
if (resolution == nullptr) {
return fallback;
}
if (const Style::UICornerRadius* radius = resolution->value.TryGetCornerRadius()) {
return (std::max)({
0.0f,
radius->topLeft,
radius->topRight,
radius->bottomRight,
radius->bottomLeft
});
}
return fallback;
}
Color AdjustColorBrightness(const Color& color, float delta) {
auto clampChannel = [delta](float value) {
return (std::clamp)(value + delta, 0.0f, 1.0f);
};
return Color(
clampChannel(color.r),
clampChannel(color.g),
clampChannel(color.b),
color.a);
}
Color ResolveLegacyBaseBackgroundColor(const UIDocumentNode& node) {
const std::string tone = GetAttribute(node, "tone");
const std::string tagName = ToStdString(node.tagName);
if (tagName == "View") {
return Color(0.11f, 0.11f, 0.11f, 1.0f);
}
if (tone == "accent") {
return Color(0.25f, 0.25f, 0.25f, 1.0f);
}
if (tone == "accent-alt") {
return Color(0.22f, 0.22f, 0.22f, 1.0f);
}
if (tagName == "Button") {
return Color(0.24f, 0.24f, 0.24f, 1.0f);
}
return Color(0.16f, 0.16f, 0.16f, 1.0f);
}
Color ResolveLegacyBaseBorderColor(const UIDocumentNode& node) {
const std::string tone = GetAttribute(node, "tone");
if (tone == "accent") {
return Color(0.42f, 0.42f, 0.42f, 1.0f);
}
if (tone == "accent-alt") {
return Color(0.34f, 0.34f, 0.34f, 1.0f);
}
return Color(0.30f, 0.30f, 0.30f, 1.0f);
}
Color ResolveStyledColor(
const RuntimeLayoutNode& node,
Style::UIStylePropertyId propertyId,
const Color& fallback) {
const Style::UIStylePropertyResolution* resolution = FindResolvedProperty(node, propertyId);
if (resolution == nullptr) {
return fallback;
}
if (const Color* color = resolution->value.TryGetColor()) {
return *color;
}
return fallback;
}
Layout::UILayoutItem BuildLayoutItem(
const RuntimeLayoutNode& child,
Layout::UILayoutAxis parentAxis,
@@ -1283,61 +1447,68 @@ UIInputPath ResolveHoveredPath(
}
Color ResolveBackgroundColor(
const UIDocumentNode& node,
const RuntimeLayoutNode& node,
const RuntimeNodeVisualState& state) {
const std::string tone = GetAttribute(node, "tone");
const std::string tagName = ToStdString(node.tagName);
if (tagName == "View") {
return Color(0.11f, 0.11f, 0.11f, 1.0f);
}
if (tone == "accent") {
return Color(0.25f, 0.25f, 0.25f, 1.0f);
}
if (tone == "accent-alt") {
return Color(0.22f, 0.22f, 0.22f, 1.0f);
}
const std::string tagName = ToStdString(node.source->tagName);
const Color baseColor = ResolveStyledColor(
node,
Style::UIStylePropertyId::BackgroundColor,
ResolveLegacyBaseBackgroundColor(*node.source));
if (tagName == "Button") {
if (state.active || state.capture) {
return Color(0.30f, 0.30f, 0.30f, 1.0f);
return AdjustColorBrightness(baseColor, 0.06f);
}
if (state.hovered) {
return Color(0.27f, 0.27f, 0.27f, 1.0f);
return AdjustColorBrightness(baseColor, 0.03f);
}
return Color(0.24f, 0.24f, 0.24f, 1.0f);
}
return Color(0.16f, 0.16f, 0.16f, 1.0f);
return baseColor;
}
Color ResolveBorderColor(
const UIDocumentNode& node,
const RuntimeLayoutNode& node,
const RuntimeNodeVisualState& state) {
const Color baseColor = ResolveStyledColor(
node,
Style::UIStylePropertyId::BorderColor,
ResolveLegacyBaseBorderColor(*node.source));
if (state.capture) {
return Color(0.82f, 0.82f, 0.82f, 1.0f);
return AdjustColorBrightness(baseColor, 0.40f);
}
if (state.focused || state.active) {
return Color(0.62f, 0.62f, 0.62f, 1.0f);
return AdjustColorBrightness(baseColor, 0.24f);
}
if (state.hovered) {
return Color(0.45f, 0.45f, 0.45f, 1.0f);
return AdjustColorBrightness(baseColor, 0.12f);
}
const std::string tone = GetAttribute(node, "tone");
if (tone == "accent") {
return Color(0.42f, 0.42f, 0.42f, 1.0f);
}
if (tone == "accent-alt") {
return Color(0.34f, 0.34f, 0.34f, 1.0f);
}
return Color(0.30f, 0.30f, 0.30f, 1.0f);
return baseColor;
}
float ResolveBorderThickness(const RuntimeNodeVisualState& state) {
return (state.focused || state.active || state.capture) ? 2.0f : 1.0f;
float ResolveBorderThickness(
const RuntimeLayoutNode& node,
const RuntimeNodeVisualState& state) {
const float baseThickness = ResolveNodeBorderWidth(node, 1.0f);
if (state.focused || state.active || state.capture) {
return (std::max)(baseThickness, 2.0f);
}
return baseThickness;
}
Color ResolveForegroundColor(
const RuntimeLayoutNode& node,
const RuntimeNodeVisualState& state,
const Color& fallback) {
Color color = ResolveStyledColor(node, Style::UIStylePropertyId::ForegroundColor, fallback);
if (ToStdString(node.source->tagName) == "Button" &&
(state.capture || state.focused || state.active)) {
color = AdjustColorBrightness(color, 0.08f);
}
return color;
}
bool IsPathTarget(
@@ -1360,6 +1531,7 @@ RuntimeNodeVisualState ResolveNodeVisualState(
RuntimeLayoutNode BuildLayoutTree(
const UIDocumentNode& source,
const UIScreenDocument& document,
const std::string& parentStateKey,
const UIInputPath& parentInputPath,
std::size_t siblingIndex,
@@ -1388,10 +1560,19 @@ RuntimeLayoutNode BuildLayoutTree(
ParseRatioAttribute(source, "ratio", 0.5f));
node.shortcutScopeRoot = ParseShortcutScopeRoot(source);
node.hasShortcutBinding = TryBuildShortcutBinding(source, node.elementId, node.shortcutBinding);
node.localStyle = Style::BuildInlineStyle(source);
Style::UIStyleResolveContext styleContext = {};
styleContext.theme = document.GetRuntimeTheme();
styleContext.styleSheet = document.GetRuntimeStyleSheet();
styleContext.selector.typeName = tagName;
styleContext.selector.styleName = GetAttribute(source, "style");
styleContext.localStyle = &node.localStyle;
node.resolvedStyle = Style::ResolveStyle(styleContext);
node.children.reserve(source.children.Size());
for (std::size_t index = 0; index < source.children.Size(); ++index) {
node.children.push_back(BuildLayoutTree(
source.children[index],
document,
node.stateKey,
node.inputPath,
index,
@@ -1417,17 +1598,22 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
if (tagName == "Text") {
const std::string text = ResolveNodeText(source);
const float fontSize = ResolveNodeFontSize(node, kDefaultFontSize);
node.desiredSize = UISize(
MeasureTextWidth(text, kDefaultFontSize),
MeasureTextHeight(kDefaultFontSize));
MeasureTextWidth(text, fontSize),
MeasureTextHeight(fontSize));
node.minimumSize = node.desiredSize;
return node.desiredSize;
}
if (!IsContainerTag(source)) {
const float fontSize = ResolveNodeFontSize(
node,
tagName == "Button" ? kButtonFontSize : kDefaultFontSize);
const Layout::UILayoutThickness padding = ResolveNodePadding(node, 12.0f);
node.desiredSize = UISize(
(std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + 24.0f),
44.0f);
(std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), fontSize) + padding.Horizontal()),
(std::max)(44.0f, MeasureTextHeight(fontSize) + padding.Vertical()));
node.minimumSize = node.desiredSize;
return node.desiredSize;
}
@@ -1532,12 +1718,11 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
options.axis = IsHorizontalTag(tagName)
? Layout::UILayoutAxis::Horizontal
: Layout::UILayoutAxis::Vertical;
options.spacing = ParseFloatAttribute(
source,
"gap",
options.spacing = ResolveNodeGap(
node,
options.axis == Layout::UILayoutAxis::Horizontal ? 10.0f : 8.0f);
options.padding = ParsePadding(
source,
options.padding = ResolveNodePadding(
node,
tagName == "View" ? 16.0f : 12.0f);
std::vector<Layout::UILayoutItem> desiredItems = {};
@@ -1676,12 +1861,11 @@ void ArrangeNode(
options.axis = IsHorizontalTag(tagName)
? Layout::UILayoutAxis::Horizontal
: Layout::UILayoutAxis::Vertical;
options.spacing = ParseFloatAttribute(
source,
"gap",
options.spacing = ResolveNodeGap(
node,
options.axis == Layout::UILayoutAxis::Horizontal ? 10.0f : 8.0f);
options.padding = ParsePadding(
source,
options.padding = ResolveNodePadding(
node,
tagName == "View" ? 16.0f : 12.0f);
const float headerHeight = node.isTab ? 0.0f : MeasureHeaderHeight(source);
@@ -2414,15 +2598,16 @@ void EmitNode(
++stats.nodeCount;
if (tagName == "View" || tagName == "Card" || tagName == "Button") {
drawList.AddFilledRect(node.rect, ToUIColor(ResolveBackgroundColor(source, visualState)), 10.0f);
const float cornerRadius = ResolveNodeCornerRadius(node, 10.0f);
drawList.AddFilledRect(node.rect, ToUIColor(ResolveBackgroundColor(node, visualState)), cornerRadius);
++stats.filledRectCommandCount;
if (tagName != "View") {
drawList.AddRectOutline(
node.rect,
ToUIColor(ResolveBorderColor(source, visualState)),
ResolveBorderThickness(visualState),
10.0f);
ToUIColor(ResolveBorderColor(node, visualState)),
ResolveBorderThickness(node, visualState),
cornerRadius);
}
}
@@ -2539,22 +2724,27 @@ void EmitNode(
}
if (tagName == "Text") {
const float fontSize = ResolveNodeFontSize(node, kDefaultFontSize);
drawList.AddText(
UIPoint(node.rect.x, node.rect.y),
ResolveNodeText(source),
ToUIColor(Color(0.92f, 0.94f, 0.97f, 1.0f)),
kDefaultFontSize);
ToUIColor(ResolveForegroundColor(node, visualState, Color(0.92f, 0.94f, 0.97f, 1.0f))),
fontSize);
++stats.textCommandCount;
}
if (tagName == "Button" && title.empty() && subtitle.empty()) {
const float fontSize = ResolveNodeFontSize(node, kButtonFontSize);
drawList.AddText(
UIPoint(node.rect.x + 12.0f, ComputeCenteredTextTop(node.rect, kButtonFontSize)),
UIPoint(node.rect.x + 12.0f, ComputeCenteredTextTop(node.rect, fontSize)),
ResolveNodeText(source),
ToUIColor(visualState.capture || visualState.focused
? Color(1.0f, 1.0f, 1.0f, 1.0f)
: Color(0.95f, 0.97f, 1.0f, 1.0f)),
kButtonFontSize);
ToUIColor(ResolveForegroundColor(
node,
visualState,
visualState.capture || visualState.focused
? Color(1.0f, 1.0f, 1.0f, 1.0f)
: Color(0.95f, 0.97f, 1.0f, 1.0f))),
fontSize);
++stats.textCommandCount;
}
@@ -2667,6 +2857,19 @@ UIScreenLoadResult UIDocumentScreenHost::LoadScreen(const UIScreenAsset& asset)
result.document.themeDocument = themeResult.document;
result.document.hasThemeDocument = true;
std::string runtimeThemeError = {};
if (!Style::TryCompileDocumentStyle(
result.document.themeDocument,
result.document.runtimeTheme,
result.document.runtimeStyleSheet,
&runtimeThemeError)) {
result = {};
result.errorMessage = runtimeThemeError.empty()
? "Failed to compile runtime UI theme styles."
: runtimeThemeError;
return result;
}
result.document.hasRuntimeTheme = true;
if (seenDependencies.insert(asset.themePath).second) {
result.document.dependencies.push_back(asset.themePath);
}
@@ -2693,6 +2896,7 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
: document.sourcePath;
RuntimeLayoutNode root = BuildLayoutTree(
document.viewDocument.rootNode,
document,
stateRoot,
UIInputPath(),
0u,