feat(xcui): advance core and editor validation flow
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user