feat(xcui): add tab strip and workspace compose foundations
This commit is contained in:
133
engine/include/XCEngine/UI/Layout/UITabStripLayout.h
Normal file
133
engine/include/XCEngine/UI/Layout/UITabStripLayout.h
Normal file
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Layout/LayoutTypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Layout {
|
||||
|
||||
struct UITabStripMetrics {
|
||||
float headerHeight = 34.0f;
|
||||
float tabMinWidth = 72.0f;
|
||||
float tabHorizontalPadding = 12.0f;
|
||||
float tabGap = 1.0f;
|
||||
};
|
||||
|
||||
struct UITabStripMeasureItem {
|
||||
float desiredHeaderLabelWidth = 0.0f;
|
||||
UISize desiredContentSize = {};
|
||||
UISize minimumContentSize = {};
|
||||
};
|
||||
|
||||
struct UITabStripMeasureResult {
|
||||
UISize desiredSize = {};
|
||||
UISize minimumSize = {};
|
||||
};
|
||||
|
||||
struct UITabStripLayoutResult {
|
||||
UIRect headerRect = {};
|
||||
UIRect contentRect = {};
|
||||
std::vector<UIRect> tabHeaderRects = {};
|
||||
};
|
||||
|
||||
inline float MeasureUITabStripHeaderWidth(
|
||||
float labelWidth,
|
||||
const UITabStripMetrics& metrics) {
|
||||
return (std::max)(
|
||||
metrics.tabMinWidth,
|
||||
(std::max)(0.0f, labelWidth) + (std::max)(0.0f, metrics.tabHorizontalPadding) * 2.0f);
|
||||
}
|
||||
|
||||
inline UITabStripMeasureResult MeasureUITabStrip(
|
||||
const std::vector<UITabStripMeasureItem>& items,
|
||||
const UITabStripMetrics& metrics) {
|
||||
UITabStripMeasureResult result = {};
|
||||
|
||||
const float gap = (std::max)(0.0f, metrics.tabGap);
|
||||
float desiredHeaderWidth = 0.0f;
|
||||
float minimumHeaderWidth = 0.0f;
|
||||
float desiredContentWidth = 0.0f;
|
||||
float desiredContentHeight = 0.0f;
|
||||
float minimumContentWidth = 0.0f;
|
||||
float minimumContentHeight = 0.0f;
|
||||
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
const UITabStripMeasureItem& item = items[index];
|
||||
desiredHeaderWidth += MeasureUITabStripHeaderWidth(
|
||||
item.desiredHeaderLabelWidth,
|
||||
metrics);
|
||||
minimumHeaderWidth += metrics.tabMinWidth;
|
||||
if (index > 0u) {
|
||||
desiredHeaderWidth += gap;
|
||||
minimumHeaderWidth += gap;
|
||||
}
|
||||
|
||||
desiredContentWidth = (std::max)(desiredContentWidth, item.desiredContentSize.width);
|
||||
desiredContentHeight = (std::max)(desiredContentHeight, item.desiredContentSize.height);
|
||||
minimumContentWidth = (std::max)(minimumContentWidth, item.minimumContentSize.width);
|
||||
minimumContentHeight = (std::max)(minimumContentHeight, item.minimumContentSize.height);
|
||||
}
|
||||
|
||||
const float headerHeight = items.empty() ? 0.0f : (std::max)(0.0f, metrics.headerHeight);
|
||||
result.desiredSize = UISize(
|
||||
(std::max)(desiredHeaderWidth, desiredContentWidth),
|
||||
headerHeight + desiredContentHeight);
|
||||
result.minimumSize = UISize(
|
||||
(std::max)(minimumHeaderWidth, minimumContentWidth),
|
||||
headerHeight + minimumContentHeight);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline UITabStripLayoutResult ArrangeUITabStrip(
|
||||
const UIRect& bounds,
|
||||
const std::vector<float>& desiredHeaderWidths,
|
||||
const UITabStripMetrics& metrics) {
|
||||
UITabStripLayoutResult result = {};
|
||||
|
||||
const float headerHeight = desiredHeaderWidths.empty()
|
||||
? 0.0f
|
||||
: (std::min)((std::max)(0.0f, metrics.headerHeight), bounds.height);
|
||||
result.headerRect = UIRect(bounds.x, bounds.y, bounds.width, headerHeight);
|
||||
result.contentRect = UIRect(
|
||||
bounds.x,
|
||||
bounds.y + headerHeight,
|
||||
bounds.width,
|
||||
(std::max)(0.0f, bounds.height - headerHeight));
|
||||
|
||||
result.tabHeaderRects.resize(desiredHeaderWidths.size());
|
||||
if (desiredHeaderWidths.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const float gap = (std::max)(0.0f, metrics.tabGap);
|
||||
const float totalGapWidth = gap * static_cast<float>(desiredHeaderWidths.size() - 1u);
|
||||
const float availableTabsWidth = (std::max)(0.0f, bounds.width - totalGapWidth);
|
||||
|
||||
float totalDesiredWidth = 0.0f;
|
||||
for (float width : desiredHeaderWidths) {
|
||||
totalDesiredWidth += (std::max)(0.0f, width);
|
||||
}
|
||||
|
||||
const float scale = totalDesiredWidth > 0.0f && totalDesiredWidth > availableTabsWidth
|
||||
? availableTabsWidth / totalDesiredWidth
|
||||
: 1.0f;
|
||||
|
||||
float cursorX = bounds.x;
|
||||
for (std::size_t index = 0; index < desiredHeaderWidths.size(); ++index) {
|
||||
const float width = totalDesiredWidth > 0.0f
|
||||
? (std::max)(0.0f, desiredHeaderWidths[index]) * scale
|
||||
: availableTabsWidth / static_cast<float>(desiredHeaderWidths.size());
|
||||
result.tabHeaderRects[index] = UIRect(cursorX, bounds.y, width, headerHeight);
|
||||
cursorX += width + gap;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Layout
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -91,6 +92,7 @@ private:
|
||||
UIInputDispatcher m_inputDispatcher;
|
||||
std::unordered_map<std::string, float> m_verticalScrollOffsets = {};
|
||||
std::unordered_map<std::string, float> m_splitterRatios = {};
|
||||
std::unordered_map<std::string, std::size_t> m_tabStripSelectedIndices = {};
|
||||
PointerState m_pointerState = {};
|
||||
SplitterDragRuntimeState m_splitterDragState = {};
|
||||
InputDebugSnapshot m_inputDebugSnapshot = {};
|
||||
|
||||
110
engine/include/XCEngine/UI/Widgets/UITabStripModel.h
Normal file
110
engine/include/XCEngine/UI/Widgets/UITabStripModel.h
Normal file
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Widgets {
|
||||
|
||||
class UITabStripModel {
|
||||
public:
|
||||
static constexpr std::size_t InvalidIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
std::size_t GetItemCount() const {
|
||||
return m_itemCount;
|
||||
}
|
||||
|
||||
bool SetItemCount(std::size_t itemCount) {
|
||||
m_itemCount = itemCount;
|
||||
return ClampSelection();
|
||||
}
|
||||
|
||||
bool HasSelection() const {
|
||||
return m_itemCount > 0u && m_selectedIndex != InvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t GetSelectedIndex() const {
|
||||
return m_selectedIndex;
|
||||
}
|
||||
|
||||
bool SetSelectedIndex(std::size_t index) {
|
||||
if (m_itemCount == 0u) {
|
||||
index = InvalidIndex;
|
||||
} else if (index >= m_itemCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_selectedIndex == index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_selectedIndex = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SelectFirst() {
|
||||
return SetSelectedIndex(m_itemCount > 0u ? 0u : InvalidIndex);
|
||||
}
|
||||
|
||||
bool SelectLast() {
|
||||
return SetSelectedIndex(m_itemCount > 0u ? (m_itemCount - 1u) : InvalidIndex);
|
||||
}
|
||||
|
||||
bool SelectNext() {
|
||||
if (m_itemCount == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_selectedIndex == InvalidIndex) {
|
||||
m_selectedIndex = 0u;
|
||||
return true;
|
||||
}
|
||||
|
||||
return SetSelectedIndex((std::min)(m_selectedIndex + 1u, m_itemCount - 1u));
|
||||
}
|
||||
|
||||
bool SelectPrevious() {
|
||||
if (m_itemCount == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_selectedIndex == InvalidIndex) {
|
||||
m_selectedIndex = 0u;
|
||||
return true;
|
||||
}
|
||||
|
||||
return SetSelectedIndex(m_selectedIndex > 0u ? m_selectedIndex - 1u : 0u);
|
||||
}
|
||||
|
||||
private:
|
||||
bool ClampSelection() {
|
||||
if (m_itemCount == 0u) {
|
||||
if (m_selectedIndex == InvalidIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_selectedIndex = InvalidIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_selectedIndex == InvalidIndex) {
|
||||
m_selectedIndex = 0u;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_selectedIndex < m_itemCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_selectedIndex = m_itemCount - 1u;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t m_itemCount = 0u;
|
||||
std::size_t m_selectedIndex = InvalidIndex;
|
||||
};
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
#include <XCEngine/UI/Layout/LayoutEngine.h>
|
||||
#include <XCEngine/UI/Layout/UITabStripLayout.h>
|
||||
#include <XCEngine/UI/Widgets/UITabStripModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -36,6 +38,7 @@ namespace Layout = XCEngine::UI::Layout;
|
||||
constexpr float kDefaultFontSize = 16.0f;
|
||||
constexpr float kSmallFontSize = 13.0f;
|
||||
constexpr float kButtonFontSize = 14.0f;
|
||||
constexpr float kTabFontSize = 13.0f;
|
||||
constexpr float kApproximateGlyphWidth = 0.56f;
|
||||
constexpr float kHeaderTextInset = 12.0f;
|
||||
constexpr float kHeaderTextGap = 2.0f;
|
||||
@@ -57,6 +60,8 @@ struct RuntimeLayoutNode {
|
||||
bool wantsPointerCapture = false;
|
||||
bool isScrollView = false;
|
||||
bool isSplitter = false;
|
||||
bool isTabStrip = false;
|
||||
bool isTab = false;
|
||||
bool textInput = false;
|
||||
Layout::UILayoutAxis splitterAxis = Layout::UILayoutAxis::Horizontal;
|
||||
Layout::UISplitterMetrics splitterMetrics = {};
|
||||
@@ -64,6 +69,13 @@ struct RuntimeLayoutNode {
|
||||
float splitterRatio = 0.5f;
|
||||
UIRect splitterHandleRect = {};
|
||||
UIRect splitterHandleHitRect = {};
|
||||
Layout::UITabStripMetrics tabStripMetrics = {};
|
||||
std::size_t tabStripSelectedIndex = 0u;
|
||||
std::size_t tabIndex = 0u;
|
||||
UIRect tabStripHeaderRect = {};
|
||||
UIRect tabStripContentRect = {};
|
||||
UIRect tabHeaderRect = {};
|
||||
bool tabSelected = false;
|
||||
bool hasShortcutBinding = false;
|
||||
UIShortcutBinding shortcutBinding = {};
|
||||
enum class ShortcutScopeRoot : std::uint8_t {
|
||||
@@ -175,6 +187,8 @@ std::string GetAttribute(
|
||||
return attribute != nullptr ? ToStdString(attribute->value) : fallback;
|
||||
}
|
||||
|
||||
std::string ResolveNodeText(const UIDocumentNode& node);
|
||||
|
||||
bool ParseBoolAttribute(
|
||||
const UIDocumentNode& node,
|
||||
const char* name,
|
||||
@@ -446,6 +460,42 @@ float MeasureHeaderHeight(const UIDocumentNode& node) {
|
||||
return headerHeight;
|
||||
}
|
||||
|
||||
bool TryParseIndexValue(const std::string& text, std::size_t& outValue) {
|
||||
if (text.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t value = 0u;
|
||||
for (unsigned char ch : text) {
|
||||
if (std::isspace(ch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!std::isdigit(ch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = value * 10u + static_cast<std::size_t>(ch - '0');
|
||||
}
|
||||
|
||||
outValue = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ResolveTabLabelText(const UIDocumentNode& node) {
|
||||
std::string label = GetAttribute(node, "label");
|
||||
if (!label.empty()) {
|
||||
return label;
|
||||
}
|
||||
|
||||
label = GetAttribute(node, "title");
|
||||
if (!label.empty()) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return ResolveNodeText(node);
|
||||
}
|
||||
|
||||
bool TryParseFloat(const std::string& text, float& outValue) {
|
||||
if (text.empty()) {
|
||||
return false;
|
||||
@@ -468,6 +518,28 @@ bool TryParseFloat(const std::string& text, float& outValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t ResolveInitialTabStripSelectedIndex(
|
||||
const UIDocumentNode& node,
|
||||
std::size_t childCount) {
|
||||
if (childCount == 0u) {
|
||||
return Widgets::UITabStripModel::InvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t attributeIndex = 0u;
|
||||
if (TryParseIndexValue(GetAttribute(node, "selectedIndex"), attributeIndex) &&
|
||||
attributeIndex < childCount) {
|
||||
return attributeIndex;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < childCount; ++index) {
|
||||
if (ParseBoolAttribute(node.children[index], "selected", false)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return 0u;
|
||||
}
|
||||
|
||||
float ParseFloatAttribute(
|
||||
const UIDocumentNode& node,
|
||||
const char* name,
|
||||
@@ -577,6 +649,47 @@ std::string ResolveNodeText(const UIDocumentNode& node) {
|
||||
return ToStdString(node.tagName);
|
||||
}
|
||||
|
||||
std::size_t ResolveTabStripSelectedIndex(
|
||||
const UIDocumentNode& source,
|
||||
const std::string& stateKey,
|
||||
std::size_t childCount,
|
||||
const std::unordered_map<std::string, std::size_t>& tabStripSelectedIndices) {
|
||||
Widgets::UITabStripModel model = {};
|
||||
model.SetItemCount(childCount);
|
||||
|
||||
const auto found = tabStripSelectedIndices.find(stateKey);
|
||||
if (found != tabStripSelectedIndices.end() &&
|
||||
found->second != Widgets::UITabStripModel::InvalidIndex) {
|
||||
model.SetSelectedIndex(found->second);
|
||||
} else {
|
||||
const std::size_t initialIndex = ResolveInitialTabStripSelectedIndex(source, childCount);
|
||||
if (initialIndex != Widgets::UITabStripModel::InvalidIndex) {
|
||||
model.SetSelectedIndex(initialIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return model.HasSelection() ? model.GetSelectedIndex() : 0u;
|
||||
}
|
||||
|
||||
void ApplyTabStripSelection(RuntimeLayoutNode& tabStripNode, std::size_t selectedIndex) {
|
||||
Widgets::UITabStripModel model = {};
|
||||
model.SetItemCount(tabStripNode.children.size());
|
||||
if (!tabStripNode.children.empty()) {
|
||||
model.SetSelectedIndex((std::min)(selectedIndex, tabStripNode.children.size() - 1u));
|
||||
}
|
||||
|
||||
tabStripNode.tabStripSelectedIndex = model.HasSelection() ? model.GetSelectedIndex() : 0u;
|
||||
for (std::size_t index = 0; index < tabStripNode.children.size(); ++index) {
|
||||
RuntimeLayoutNode& child = tabStripNode.children[index];
|
||||
child.tabIndex = index;
|
||||
child.tabSelected = model.HasSelection() && index == model.GetSelectedIndex();
|
||||
}
|
||||
}
|
||||
|
||||
bool AreTabChildrenVisible(const RuntimeLayoutNode& node) {
|
||||
return !node.isTab || node.tabSelected;
|
||||
}
|
||||
|
||||
bool IsHorizontalTag(const std::string& tagName) {
|
||||
return tagName == "Row";
|
||||
}
|
||||
@@ -589,6 +702,14 @@ bool IsSplitterTag(const std::string& tagName) {
|
||||
return tagName == "Splitter";
|
||||
}
|
||||
|
||||
bool IsTabStripTag(const std::string& tagName) {
|
||||
return tagName == "TabStrip";
|
||||
}
|
||||
|
||||
bool IsTabTag(const std::string& tagName) {
|
||||
return tagName == "Tab";
|
||||
}
|
||||
|
||||
bool IsButtonTag(const std::string& tagName) {
|
||||
return tagName == "Button";
|
||||
}
|
||||
@@ -625,6 +746,27 @@ Layout::UISplitterMetrics ParseSplitterMetrics(const UIDocumentNode& node) {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
Layout::UITabStripMetrics ParseTabStripMetrics(const UIDocumentNode& node) {
|
||||
Layout::UITabStripMetrics metrics = {};
|
||||
metrics.headerHeight = (std::max)(
|
||||
24.0f,
|
||||
ParseFloatAttributeAny(
|
||||
node,
|
||||
"tabHeaderHeight",
|
||||
"tabHeight",
|
||||
ParseFloatAttribute(node, "headerHeight", metrics.headerHeight)));
|
||||
metrics.tabMinWidth = (std::max)(
|
||||
32.0f,
|
||||
ParseFloatAttributeAny(node, "tabMinWidth", "minTabWidth", metrics.tabMinWidth));
|
||||
metrics.tabHorizontalPadding = (std::max)(
|
||||
0.0f,
|
||||
ParseFloatAttributeAny(node, "tabPaddingX", "tabHorizontalPadding", metrics.tabHorizontalPadding));
|
||||
metrics.tabGap = (std::max)(
|
||||
0.0f,
|
||||
ParseFloatAttributeAny(node, "tabGap", "headerGap", metrics.tabGap));
|
||||
return metrics;
|
||||
}
|
||||
|
||||
bool IsContainerTag(const UIDocumentNode& node) {
|
||||
if (node.children.Size() > 0u) {
|
||||
return true;
|
||||
@@ -635,6 +777,8 @@ bool IsContainerTag(const UIDocumentNode& node) {
|
||||
tagName == "Column" ||
|
||||
tagName == "Row" ||
|
||||
tagName == "Splitter" ||
|
||||
tagName == "TabStrip" ||
|
||||
tagName == "Tab" ||
|
||||
tagName == "ScrollView" ||
|
||||
tagName == "Card" ||
|
||||
tagName == "Button";
|
||||
@@ -645,12 +789,12 @@ bool IsPointerInteractiveNode(const UIDocumentNode& node) {
|
||||
return ParseBoolAttribute(
|
||||
node,
|
||||
"interactive",
|
||||
IsButtonTag(tagName) || IsScrollViewTag(tagName) || IsSplitterTag(tagName));
|
||||
IsButtonTag(tagName) || IsScrollViewTag(tagName) || IsSplitterTag(tagName) || IsTabTag(tagName));
|
||||
}
|
||||
|
||||
bool IsFocusableNode(const UIDocumentNode& node) {
|
||||
const std::string tagName = ToStdString(node.tagName);
|
||||
return ParseBoolAttribute(node, "focusable", IsButtonTag(tagName));
|
||||
return ParseBoolAttribute(node, "focusable", IsButtonTag(tagName) || IsTabTag(tagName));
|
||||
}
|
||||
|
||||
bool WantsPointerCapture(const UIDocumentNode& node) {
|
||||
@@ -713,11 +857,15 @@ const UIRect& GetNodeInteractionRect(const RuntimeLayoutNode& node) {
|
||||
return node.splitterHandleHitRect;
|
||||
}
|
||||
|
||||
if (node.isTab) {
|
||||
return node.tabHeaderRect;
|
||||
}
|
||||
|
||||
return node.isScrollView ? node.scrollViewportRect : node.rect;
|
||||
}
|
||||
|
||||
bool IsNodeTargetable(const RuntimeLayoutNode& node) {
|
||||
return node.pointerInteractive || node.focusable || node.isScrollView || node.isSplitter;
|
||||
return node.pointerInteractive || node.focusable || node.isScrollView || node.isSplitter || node.isTab;
|
||||
}
|
||||
|
||||
const RuntimeLayoutNode* FindNodeByElementId(
|
||||
@@ -752,6 +900,37 @@ RuntimeLayoutNode* FindNodeByElementId(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RuntimeLayoutNode* FindParentTabStripNode(RuntimeLayoutNode& root, const RuntimeLayoutNode& node) {
|
||||
if (!node.isTab || node.inputPath.elements.size() < 2u) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RuntimeLayoutNode* parent = FindNodeByElementId(
|
||||
root,
|
||||
node.inputPath.elements[node.inputPath.elements.size() - 2u]);
|
||||
return parent != nullptr && parent->isTabStrip ? parent : nullptr;
|
||||
}
|
||||
|
||||
const RuntimeLayoutNode* FindVisibleNodeByElementId(
|
||||
const RuntimeLayoutNode& node,
|
||||
UIElementId elementId) {
|
||||
if (node.elementId == elementId) {
|
||||
return &node;
|
||||
}
|
||||
|
||||
if (!AreTabChildrenVisible(node)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (const RuntimeLayoutNode* found = FindVisibleNodeByElementId(child, elementId); found != nullptr) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool CollectNodesForInputPath(
|
||||
const RuntimeLayoutNode& node,
|
||||
const UIInputPath& path,
|
||||
@@ -766,6 +945,11 @@ bool CollectNodesForInputPath(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!AreTabChildrenVisible(node)) {
|
||||
outNodes.pop_back();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (CollectNodesForInputPath(child, path, index + 1u, outNodes)) {
|
||||
return true;
|
||||
@@ -793,7 +977,7 @@ std::string ResolveStateKeyForElementId(
|
||||
bool PathTargetExists(
|
||||
const RuntimeLayoutNode& root,
|
||||
const UIInputPath& path) {
|
||||
return !path.Empty() && FindNodeByElementId(root, path.Target()) != nullptr;
|
||||
return !path.Empty() && FindVisibleNodeByElementId(root, path.Target()) != nullptr;
|
||||
}
|
||||
|
||||
bool SplitterTargetExists(
|
||||
@@ -843,7 +1027,26 @@ std::string ValidateRuntimeLayoutTree(const RuntimeLayoutNode& node) {
|
||||
return "Splitter '" + splitterName + "' must contain exactly 2 child elements.";
|
||||
}
|
||||
|
||||
if (node.isTabStrip) {
|
||||
const std::string tabStripName = node.stateKey.empty()
|
||||
? std::string("<unnamed-tab-strip>")
|
||||
: node.stateKey;
|
||||
if (node.children.empty()) {
|
||||
return "TabStrip '" + tabStripName + "' must contain at least 1 Tab child.";
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (!child.isTab) {
|
||||
return "TabStrip '" + tabStripName + "' may only contain Tab children.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (child.isTab && !node.isTabStrip) {
|
||||
return "Tab '" + child.stateKey + "' must be parented directly by a TabStrip.";
|
||||
}
|
||||
|
||||
const std::string error = ValidateRuntimeLayoutTree(child);
|
||||
if (!error.empty()) {
|
||||
return error;
|
||||
@@ -923,6 +1126,10 @@ void RegisterShortcutBindings(
|
||||
registry.RegisterBinding(node.shortcutBinding);
|
||||
}
|
||||
|
||||
if (!AreTabChildrenVisible(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
RegisterShortcutBindings(child, registry);
|
||||
}
|
||||
@@ -964,6 +1171,10 @@ void CollectFocusablePaths(
|
||||
outPaths.push_back(node.inputPath);
|
||||
}
|
||||
|
||||
if (!AreTabChildrenVisible(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
CollectFocusablePaths(child, outPaths);
|
||||
}
|
||||
@@ -1036,9 +1247,11 @@ const RuntimeLayoutNode* FindDeepestInputTarget(
|
||||
return &node;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (const RuntimeLayoutNode* found = FindDeepestInputTarget(child, point, &nextClip); found != nullptr) {
|
||||
return found;
|
||||
if (AreTabChildrenVisible(node)) {
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (const RuntimeLayoutNode* found = FindDeepestInputTarget(child, point, &nextClip); found != nullptr) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1149,8 +1362,10 @@ RuntimeLayoutNode BuildLayoutTree(
|
||||
const UIDocumentNode& source,
|
||||
const std::string& parentStateKey,
|
||||
const UIInputPath& parentInputPath,
|
||||
std::size_t siblingIndex) {
|
||||
std::size_t siblingIndex,
|
||||
const std::unordered_map<std::string, std::size_t>& tabStripSelectedIndices) {
|
||||
RuntimeLayoutNode node = {};
|
||||
const std::string tagName = ToStdString(source.tagName);
|
||||
node.source = &source;
|
||||
node.stateKey = parentStateKey + "/" + BuildNodeStateKeySegment(source, siblingIndex);
|
||||
node.elementId = HashStateKeyToElementId(node.stateKey);
|
||||
@@ -1159,11 +1374,14 @@ RuntimeLayoutNode BuildLayoutTree(
|
||||
node.pointerInteractive = IsPointerInteractiveNode(source);
|
||||
node.focusable = ParseBoolAttribute(source, "focusable", IsFocusableNode(source));
|
||||
node.wantsPointerCapture = WantsPointerCapture(source);
|
||||
node.isScrollView = IsScrollViewTag(ToStdString(source.tagName));
|
||||
node.isSplitter = IsSplitterTag(ToStdString(source.tagName));
|
||||
node.isScrollView = IsScrollViewTag(tagName);
|
||||
node.isSplitter = IsSplitterTag(tagName);
|
||||
node.isTabStrip = IsTabStripTag(tagName);
|
||||
node.isTab = IsTabTag(tagName);
|
||||
node.textInput = IsTextInputNode(source);
|
||||
node.splitterAxis = ParseAxisAttribute(source, "axis", Layout::UILayoutAxis::Horizontal);
|
||||
node.splitterMetrics = ParseSplitterMetrics(source);
|
||||
node.tabStripMetrics = ParseTabStripMetrics(source);
|
||||
node.splitterRatio = ParseRatioAttribute(
|
||||
source,
|
||||
"splitRatio",
|
||||
@@ -1172,8 +1390,24 @@ RuntimeLayoutNode BuildLayoutTree(
|
||||
node.hasShortcutBinding = TryBuildShortcutBinding(source, node.elementId, node.shortcutBinding);
|
||||
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], node.stateKey, node.inputPath, index));
|
||||
node.children.push_back(BuildLayoutTree(
|
||||
source.children[index],
|
||||
node.stateKey,
|
||||
node.inputPath,
|
||||
index,
|
||||
tabStripSelectedIndices));
|
||||
}
|
||||
|
||||
if (node.isTabStrip) {
|
||||
ApplyTabStripSelection(
|
||||
node,
|
||||
ResolveTabStripSelectedIndex(
|
||||
source,
|
||||
node.stateKey,
|
||||
node.children.size(),
|
||||
tabStripSelectedIndices));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -1260,6 +1494,40 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
|
||||
return node.desiredSize;
|
||||
}
|
||||
|
||||
if (node.isTabStrip) {
|
||||
std::vector<Layout::UITabStripMeasureItem> items = {};
|
||||
items.reserve(node.children.size());
|
||||
for (RuntimeLayoutNode& child : node.children) {
|
||||
MeasureNode(child);
|
||||
Layout::UITabStripMeasureItem item = {};
|
||||
item.desiredHeaderLabelWidth = MeasureTextWidth(
|
||||
ResolveTabLabelText(*child.source),
|
||||
kTabFontSize);
|
||||
item.desiredContentSize = child.desiredSize;
|
||||
item.minimumContentSize = child.minimumSize;
|
||||
items.push_back(item);
|
||||
}
|
||||
|
||||
const Layout::UITabStripMeasureResult measured = Layout::MeasureUITabStrip(
|
||||
items,
|
||||
node.tabStripMetrics);
|
||||
node.contentDesiredSize = measured.desiredSize;
|
||||
node.desiredSize = measured.desiredSize;
|
||||
node.minimumSize = measured.minimumSize;
|
||||
|
||||
float explicitWidth = 0.0f;
|
||||
if (TryParseFloat(GetAttribute(source, "width"), explicitWidth)) {
|
||||
node.desiredSize.width = (std::max)(node.desiredSize.width, explicitWidth);
|
||||
}
|
||||
|
||||
float explicitHeight = 0.0f;
|
||||
if (TryParseFloat(GetAttribute(source, "height"), explicitHeight)) {
|
||||
node.desiredSize.height = (std::max)(node.desiredSize.height, explicitHeight);
|
||||
}
|
||||
|
||||
return node.desiredSize;
|
||||
}
|
||||
|
||||
Layout::UIStackLayoutOptions options = {};
|
||||
options.axis = IsHorizontalTag(tagName)
|
||||
? Layout::UILayoutAxis::Horizontal
|
||||
@@ -1288,20 +1556,22 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
|
||||
node.desiredSize = measured.desiredSize;
|
||||
node.minimumSize = minimumMeasured.desiredSize;
|
||||
|
||||
const float headerHeight = MeasureHeaderHeight(source);
|
||||
const float headerTextWidth = MeasureHeaderTextWidth(source);
|
||||
const float headerHeight = node.isTab ? 0.0f : MeasureHeaderHeight(source);
|
||||
const float headerTextWidth = node.isTab ? 0.0f : MeasureHeaderTextWidth(source);
|
||||
|
||||
node.desiredSize.width = (std::max)(
|
||||
node.desiredSize.width,
|
||||
headerTextWidth > 0.0f
|
||||
? headerTextWidth + options.padding.Horizontal()
|
||||
: MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
||||
if (!node.isTab) {
|
||||
node.desiredSize.width = (std::max)(
|
||||
node.desiredSize.width,
|
||||
headerTextWidth > 0.0f
|
||||
? headerTextWidth + options.padding.Horizontal()
|
||||
: MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
||||
node.minimumSize.width = (std::max)(
|
||||
node.minimumSize.width,
|
||||
headerTextWidth > 0.0f
|
||||
? headerTextWidth + options.padding.Horizontal()
|
||||
: MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
||||
}
|
||||
node.desiredSize.height += headerHeight;
|
||||
node.minimumSize.width = (std::max)(
|
||||
node.minimumSize.width,
|
||||
headerTextWidth > 0.0f
|
||||
? headerTextWidth + options.padding.Horizontal()
|
||||
: MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
||||
node.minimumSize.height += headerHeight;
|
||||
|
||||
if (node.isScrollView) {
|
||||
@@ -1331,6 +1601,9 @@ void ArrangeNode(
|
||||
node.scrollOffsetY = 0.0f;
|
||||
node.splitterHandleRect = {};
|
||||
node.splitterHandleHitRect = {};
|
||||
node.tabStripHeaderRect = {};
|
||||
node.tabStripContentRect = {};
|
||||
node.tabHeaderRect = {};
|
||||
|
||||
const UIDocumentNode& source = *node.source;
|
||||
if (!IsContainerTag(source)) {
|
||||
@@ -1367,6 +1640,37 @@ void ArrangeNode(
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.isTabStrip) {
|
||||
std::vector<float> desiredHeaderWidths = {};
|
||||
desiredHeaderWidths.reserve(node.children.size());
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
desiredHeaderWidths.push_back(Layout::MeasureUITabStripHeaderWidth(
|
||||
MeasureTextWidth(ResolveTabLabelText(*child.source), kTabFontSize),
|
||||
node.tabStripMetrics));
|
||||
}
|
||||
|
||||
const Layout::UITabStripLayoutResult arranged = Layout::ArrangeUITabStrip(
|
||||
rect,
|
||||
desiredHeaderWidths,
|
||||
node.tabStripMetrics);
|
||||
node.tabStripHeaderRect = arranged.headerRect;
|
||||
node.tabStripContentRect = arranged.contentRect;
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
RuntimeLayoutNode& child = node.children[index];
|
||||
child.tabIndex = index;
|
||||
child.tabSelected = index == node.tabStripSelectedIndex;
|
||||
ArrangeNode(
|
||||
child,
|
||||
arranged.contentRect,
|
||||
verticalScrollOffsets,
|
||||
splitterRatios);
|
||||
child.tabHeaderRect = index < arranged.tabHeaderRects.size()
|
||||
? arranged.tabHeaderRects[index]
|
||||
: UIRect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string tagName = ToStdString(source.tagName);
|
||||
Layout::UIStackLayoutOptions options = {};
|
||||
options.axis = IsHorizontalTag(tagName)
|
||||
@@ -1380,7 +1684,7 @@ void ArrangeNode(
|
||||
source,
|
||||
tagName == "View" ? 16.0f : 12.0f);
|
||||
|
||||
const float headerHeight = MeasureHeaderHeight(source);
|
||||
const float headerHeight = node.isTab ? 0.0f : MeasureHeaderHeight(source);
|
||||
|
||||
UIRect contentRect = rect;
|
||||
contentRect.y += headerHeight;
|
||||
@@ -1428,9 +1732,11 @@ void ArrangeNode(
|
||||
RuntimeLayoutNode* FindDeepestScrollTarget(
|
||||
RuntimeLayoutNode& node,
|
||||
const UIPoint& point) {
|
||||
for (RuntimeLayoutNode& child : node.children) {
|
||||
if (RuntimeLayoutNode* target = FindDeepestScrollTarget(child, point); target != nullptr) {
|
||||
return target;
|
||||
if (AreTabChildrenVisible(node)) {
|
||||
for (RuntimeLayoutNode& child : node.children) {
|
||||
if (RuntimeLayoutNode* target = FindDeepestScrollTarget(child, point); target != nullptr) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,9 +1758,11 @@ RuntimeLayoutNode* FindDeepestScrollTarget(
|
||||
RuntimeLayoutNode* FindDeepestHoveredScrollView(
|
||||
RuntimeLayoutNode& node,
|
||||
const UIPoint& point) {
|
||||
for (RuntimeLayoutNode& child : node.children) {
|
||||
if (RuntimeLayoutNode* hovered = FindDeepestHoveredScrollView(child, point); hovered != nullptr) {
|
||||
return hovered;
|
||||
if (AreTabChildrenVisible(node)) {
|
||||
for (RuntimeLayoutNode& child : node.children) {
|
||||
if (RuntimeLayoutNode* hovered = FindDeepestHoveredScrollView(child, point); hovered != nullptr) {
|
||||
return hovered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1472,9 +1780,11 @@ const RuntimeLayoutNode* FindFirstScrollView(const RuntimeLayoutNode& node) {
|
||||
return &node;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (const RuntimeLayoutNode* found = FindFirstScrollView(child); found != nullptr) {
|
||||
return found;
|
||||
if (AreTabChildrenVisible(node)) {
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (const RuntimeLayoutNode* found = FindFirstScrollView(child); found != nullptr) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1672,12 +1982,153 @@ void UpdateInputDebugSnapshot(
|
||||
shortcutContext.commandScope.widgetId);
|
||||
}
|
||||
|
||||
enum class TabStripSelectionCommand : std::uint8_t {
|
||||
None = 0,
|
||||
SelectPrevious,
|
||||
SelectNext,
|
||||
SelectFirst,
|
||||
SelectLast
|
||||
};
|
||||
|
||||
TabStripSelectionCommand ResolveTabStripSelectionCommand(const UIInputEvent& event) {
|
||||
if (event.type != UIInputEventType::KeyDown ||
|
||||
event.modifiers.control ||
|
||||
event.modifiers.alt ||
|
||||
event.modifiers.super) {
|
||||
return TabStripSelectionCommand::None;
|
||||
}
|
||||
|
||||
switch (static_cast<KeyCode>(event.keyCode)) {
|
||||
case KeyCode::Left:
|
||||
return TabStripSelectionCommand::SelectPrevious;
|
||||
case KeyCode::Right:
|
||||
return TabStripSelectionCommand::SelectNext;
|
||||
case KeyCode::Home:
|
||||
return TabStripSelectionCommand::SelectFirst;
|
||||
case KeyCode::End:
|
||||
return TabStripSelectionCommand::SelectLast;
|
||||
default:
|
||||
return TabStripSelectionCommand::None;
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateTabStripSelection(
|
||||
RuntimeLayoutNode& tabStripNode,
|
||||
Widgets::UITabStripModel& model,
|
||||
std::unordered_map<std::string, std::size_t>& tabStripSelectedIndices,
|
||||
UIFocusController& focusController) {
|
||||
ApplyTabStripSelection(tabStripNode, model.HasSelection() ? model.GetSelectedIndex() : 0u);
|
||||
if (model.HasSelection()) {
|
||||
tabStripSelectedIndices[tabStripNode.stateKey] = model.GetSelectedIndex();
|
||||
focusController.SetFocusedPath(tabStripNode.children[model.GetSelectedIndex()].inputPath);
|
||||
} else {
|
||||
tabStripSelectedIndices.erase(tabStripNode.stateKey);
|
||||
focusController.ClearFocus();
|
||||
}
|
||||
|
||||
focusController.ClearActivePath();
|
||||
return model.HasSelection();
|
||||
}
|
||||
|
||||
const RuntimeLayoutNode* FindInnermostVisibleTabNodeForPath(
|
||||
const RuntimeLayoutNode& root,
|
||||
const UIInputPath& path) {
|
||||
if (path.Empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<const RuntimeLayoutNode*> nodes = {};
|
||||
if (!CollectNodesForInputPath(root, path, 0u, nodes)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (auto iter = nodes.rbegin(); iter != nodes.rend(); ++iter) {
|
||||
if ((*iter)->isTab) {
|
||||
return *iter;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool TryHandleFocusedTabStripSelectionCommand(
|
||||
RuntimeLayoutNode& root,
|
||||
const UIInputEvent& event,
|
||||
UIInputDispatcher& inputDispatcher,
|
||||
std::unordered_map<std::string, std::size_t>& tabStripSelectedIndices,
|
||||
UIDocumentScreenHost::InputDebugSnapshot& inputDebugSnapshot,
|
||||
bool& layoutChanged) {
|
||||
layoutChanged = false;
|
||||
if (inputDispatcher.GetShortcutContext().textInputActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const TabStripSelectionCommand selectionCommand = ResolveTabStripSelectionCommand(event);
|
||||
if (selectionCommand == TabStripSelectionCommand::None) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RuntimeLayoutNode* focusedTabNode = FindInnermostVisibleTabNodeForPath(
|
||||
root,
|
||||
inputDispatcher.GetFocusController().GetFocusedPath());
|
||||
if (focusedTabNode == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RuntimeLayoutNode* focusedTab = FindNodeByElementId(root, focusedTabNode->elementId);
|
||||
if (focusedTab == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RuntimeLayoutNode* tabStripNode = FindParentTabStripNode(root, *focusedTab);
|
||||
if (tabStripNode == nullptr || tabStripNode->children.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Widgets::UITabStripModel model = {};
|
||||
model.SetItemCount(tabStripNode->children.size());
|
||||
model.SetSelectedIndex((std::min)(focusedTab->tabIndex, tabStripNode->children.size() - 1u));
|
||||
|
||||
switch (selectionCommand) {
|
||||
case TabStripSelectionCommand::SelectPrevious:
|
||||
layoutChanged = model.SelectPrevious();
|
||||
break;
|
||||
case TabStripSelectionCommand::SelectNext:
|
||||
layoutChanged = model.SelectNext();
|
||||
break;
|
||||
case TabStripSelectionCommand::SelectFirst:
|
||||
layoutChanged = model.SelectFirst();
|
||||
break;
|
||||
case TabStripSelectionCommand::SelectLast:
|
||||
layoutChanged = model.SelectLast();
|
||||
break;
|
||||
case TabStripSelectionCommand::None:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTabStripSelection(
|
||||
*tabStripNode,
|
||||
model,
|
||||
tabStripSelectedIndices,
|
||||
inputDispatcher.GetFocusController());
|
||||
|
||||
const RuntimeLayoutNode& targetTab = tabStripNode->children[tabStripNode->tabStripSelectedIndex];
|
||||
inputDebugSnapshot.lastTargetKind = "Focused";
|
||||
inputDebugSnapshot.lastTargetStateKey = targetTab.stateKey;
|
||||
inputDebugSnapshot.lastResult = layoutChanged
|
||||
? "Tab navigated"
|
||||
: "Tab navigation unchanged";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DispatchInputEvent(
|
||||
RuntimeLayoutNode& root,
|
||||
const UIInputEvent& event,
|
||||
const UIInputPath& hoveredPath,
|
||||
UIInputDispatcher& inputDispatcher,
|
||||
std::unordered_map<std::string, float>& splitterRatios,
|
||||
std::unordered_map<std::string, std::size_t>& tabStripSelectedIndices,
|
||||
UIDocumentScreenHost::SplitterDragRuntimeState& splitterDragState,
|
||||
UIDocumentScreenHost::InputDebugSnapshot& inputDebugSnapshot) {
|
||||
if (event.type == UIInputEventType::PointerWheel) {
|
||||
@@ -1725,9 +2176,22 @@ bool DispatchInputEvent(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool focusedTabNavigationChanged = false;
|
||||
if (TryHandleFocusedTabStripSelectionCommand(
|
||||
root,
|
||||
event,
|
||||
inputDispatcher,
|
||||
tabStripSelectedIndices,
|
||||
inputDebugSnapshot,
|
||||
focusedTabNavigationChanged)) {
|
||||
return focusedTabNavigationChanged;
|
||||
}
|
||||
|
||||
bool pointerCaptureStarted = false;
|
||||
bool layoutChanged = false;
|
||||
bool splitterDragFinished = false;
|
||||
bool tabSelectionTriggered = false;
|
||||
bool tabSelectionChanged = false;
|
||||
const UIInputDispatchSummary summary = inputDispatcher.Dispatch(
|
||||
event,
|
||||
hoveredPath,
|
||||
@@ -1736,7 +2200,7 @@ bool DispatchInputEvent(
|
||||
return UIInputDispatchDecision{};
|
||||
}
|
||||
|
||||
const RuntimeLayoutNode* node = FindNodeByElementId(root, request.elementId);
|
||||
RuntimeLayoutNode* node = FindNodeByElementId(root, request.elementId);
|
||||
if (node == nullptr) {
|
||||
return UIInputDispatchDecision{};
|
||||
}
|
||||
@@ -1811,6 +2275,25 @@ bool DispatchInputEvent(
|
||||
return UIInputDispatchDecision{ true, false };
|
||||
}
|
||||
|
||||
if (node->isTab &&
|
||||
event.type == UIInputEventType::PointerButtonUp &&
|
||||
event.pointerButton == UIPointerButton::Left) {
|
||||
if (RuntimeLayoutNode* tabStripNode = FindParentTabStripNode(root, *node);
|
||||
tabStripNode != nullptr) {
|
||||
tabSelectionTriggered = true;
|
||||
tabSelectionChanged = tabSelectionChanged || tabStripNode->tabStripSelectedIndex != node->tabIndex;
|
||||
Widgets::UITabStripModel model = {};
|
||||
model.SetItemCount(tabStripNode->children.size());
|
||||
model.SetSelectedIndex(node->tabIndex);
|
||||
UpdateTabStripSelection(
|
||||
*tabStripNode,
|
||||
model,
|
||||
tabStripSelectedIndices,
|
||||
inputDispatcher.GetFocusController());
|
||||
return UIInputDispatchDecision{ true, false };
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type == UIInputEventType::PointerButtonDown &&
|
||||
event.pointerButton == UIPointerButton::Left &&
|
||||
node->wantsPointerCapture) {
|
||||
@@ -1846,6 +2329,10 @@ bool DispatchInputEvent(
|
||||
inputDebugSnapshot.lastResult = "Shortcut suppressed by text input";
|
||||
} else if (focusTraversalSuppressed) {
|
||||
inputDebugSnapshot.lastResult = "Focus traversal suppressed by text input";
|
||||
} else if (tabSelectionTriggered) {
|
||||
inputDebugSnapshot.lastResult = tabSelectionChanged
|
||||
? "Tab selected"
|
||||
: "Tab selection unchanged";
|
||||
} else if (pointerCaptureStarted) {
|
||||
inputDebugSnapshot.lastResult = splitterDragState.drag.active
|
||||
? "Splitter drag started"
|
||||
@@ -1899,6 +2386,18 @@ void SyncSplitterRatios(
|
||||
}
|
||||
}
|
||||
|
||||
void SyncTabStripSelectedIndices(
|
||||
const RuntimeLayoutNode& node,
|
||||
std::unordered_map<std::string, std::size_t>& tabStripSelectedIndices) {
|
||||
if (node.isTabStrip && !node.children.empty()) {
|
||||
tabStripSelectedIndices[node.stateKey] = node.tabStripSelectedIndex;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
SyncTabStripSelectedIndices(child, tabStripSelectedIndices);
|
||||
}
|
||||
}
|
||||
|
||||
void EmitNode(
|
||||
const RuntimeLayoutNode& node,
|
||||
const UIInputPath& hoveredPath,
|
||||
@@ -1927,6 +2426,75 @@ void EmitNode(
|
||||
}
|
||||
}
|
||||
|
||||
if (node.isTabStrip) {
|
||||
if (HasPositiveArea(node.rect)) {
|
||||
drawList.AddFilledRect(node.rect, ToUIColor(Color(0.18f, 0.18f, 0.18f, 1.0f)), 8.0f);
|
||||
++stats.filledRectCommandCount;
|
||||
}
|
||||
|
||||
if (HasPositiveArea(node.tabStripHeaderRect)) {
|
||||
drawList.AddFilledRect(node.tabStripHeaderRect, ToUIColor(Color(0.16f, 0.16f, 0.16f, 1.0f)), 8.0f);
|
||||
++stats.filledRectCommandCount;
|
||||
}
|
||||
|
||||
if (HasPositiveArea(node.tabStripContentRect)) {
|
||||
drawList.AddFilledRect(node.tabStripContentRect, ToUIColor(Color(0.20f, 0.20f, 0.20f, 1.0f)), 8.0f);
|
||||
++stats.filledRectCommandCount;
|
||||
drawList.AddRectOutline(
|
||||
node.tabStripContentRect,
|
||||
ToUIColor(Color(0.26f, 0.26f, 0.26f, 1.0f)),
|
||||
1.0f,
|
||||
8.0f);
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (!HasPositiveArea(child.tabHeaderRect)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const RuntimeNodeVisualState tabState = ResolveNodeVisualState(
|
||||
child,
|
||||
hoveredPath,
|
||||
focusController);
|
||||
Color fillColor = child.tabSelected
|
||||
? Color(0.24f, 0.24f, 0.24f, 1.0f)
|
||||
: Color(0.15f, 0.15f, 0.15f, 1.0f);
|
||||
if (tabState.capture || tabState.active) {
|
||||
fillColor = child.tabSelected
|
||||
? Color(0.31f, 0.31f, 0.31f, 1.0f)
|
||||
: Color(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
} else if (tabState.hovered) {
|
||||
fillColor = child.tabSelected
|
||||
? Color(0.28f, 0.28f, 0.28f, 1.0f)
|
||||
: Color(0.20f, 0.20f, 0.20f, 1.0f);
|
||||
}
|
||||
|
||||
drawList.AddFilledRect(child.tabHeaderRect, ToUIColor(fillColor), 6.0f);
|
||||
++stats.filledRectCommandCount;
|
||||
drawList.AddRectOutline(
|
||||
child.tabHeaderRect,
|
||||
ToUIColor(child.tabSelected
|
||||
? Color(0.42f, 0.42f, 0.42f, 1.0f)
|
||||
: Color(0.24f, 0.24f, 0.24f, 1.0f)),
|
||||
tabState.focused || child.tabSelected ? 2.0f : 1.0f,
|
||||
6.0f);
|
||||
|
||||
const std::string label = ResolveTabLabelText(*child.source);
|
||||
if (!label.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
child.tabHeaderRect.x + node.tabStripMetrics.tabHorizontalPadding,
|
||||
ComputeCenteredTextTop(child.tabHeaderRect, kTabFontSize)),
|
||||
label,
|
||||
ToUIColor(child.tabSelected
|
||||
? Color(0.95f, 0.95f, 0.95f, 1.0f)
|
||||
: Color(0.78f, 0.78f, 0.78f, 1.0f)),
|
||||
kTabFontSize);
|
||||
++stats.textCommandCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.isSplitter && HasPositiveArea(node.splitterHandleRect)) {
|
||||
const Color splitterColor =
|
||||
visualState.capture || visualState.active
|
||||
@@ -1948,8 +2516,8 @@ void EmitNode(
|
||||
++stats.filledRectCommandCount;
|
||||
}
|
||||
|
||||
const std::string title = GetAttribute(source, "title");
|
||||
const std::string subtitle = GetAttribute(source, "subtitle");
|
||||
const std::string title = (node.isTab || node.isTabStrip) ? std::string() : GetAttribute(source, "title");
|
||||
const std::string subtitle = (node.isTab || node.isTabStrip) ? std::string() : GetAttribute(source, "subtitle");
|
||||
float textY = node.rect.y + kHeaderTextInset;
|
||||
if (!title.empty()) {
|
||||
drawList.AddText(
|
||||
@@ -1999,6 +2567,10 @@ void EmitNode(
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
if (child.isTab && !child.tabSelected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EmitNode(child, hoveredPath, focusController, drawList, stats);
|
||||
}
|
||||
|
||||
@@ -2119,7 +2691,12 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
|
||||
const std::string stateRoot = document.sourcePath.empty()
|
||||
? document.displayName
|
||||
: document.sourcePath;
|
||||
RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode, stateRoot, UIInputPath(), 0u);
|
||||
RuntimeLayoutNode root = BuildLayoutTree(
|
||||
document.viewDocument.rootNode,
|
||||
stateRoot,
|
||||
UIInputPath(),
|
||||
0u,
|
||||
m_tabStripSelectedIndices);
|
||||
result.errorMessage = ValidateRuntimeLayoutTree(root);
|
||||
if (!result.errorMessage.empty()) {
|
||||
return result;
|
||||
@@ -2191,6 +2768,7 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
|
||||
eventHoveredPath,
|
||||
m_inputDispatcher,
|
||||
m_splitterRatios,
|
||||
m_tabStripSelectedIndices,
|
||||
m_splitterDragState,
|
||||
m_inputDebugSnapshot)) {
|
||||
ArrangeNode(root, viewportRect, m_verticalScrollOffsets, m_splitterRatios);
|
||||
@@ -2219,6 +2797,8 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
|
||||
SyncScrollOffsets(root, m_verticalScrollOffsets);
|
||||
m_splitterRatios.clear();
|
||||
SyncSplitterRatios(root, m_splitterRatios);
|
||||
m_tabStripSelectedIndices.clear();
|
||||
SyncTabStripSelectedIndices(root, m_tabStripSelectedIndices);
|
||||
|
||||
const UIFocusController& focusController = m_inputDispatcher.GetFocusController();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user