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

@@ -533,12 +533,10 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Text/UITextInputController.h
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextEditing.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextInputController.cpp
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIExpansionModel.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIKeyboardNavigationModel.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIPropertyEditModel.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UISelectionModel.h
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIEditorCollectionPrimitives.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIExpansionModel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIKeyboardNavigationModel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIPropertyEditModel.cpp

View File

@@ -19,7 +19,11 @@ struct UIInputDispatcherOptions {
struct UIInputDispatchSummary {
UIFocusChange focusChange = {};
UIInputDispatchResult routing = {};
bool shortcutMatched = false;
bool shortcutHandled = false;
bool shortcutSuppressed = false;
UIShortcutScope shortcutScope = UIShortcutScope::Global;
UIElementId shortcutOwnerId = 0;
std::string commandId = {};
bool Handled() const {
@@ -50,6 +54,14 @@ public:
return m_shortcutRegistry;
}
void SetShortcutContext(const UIShortcutContext& context) {
m_shortcutContext = context;
}
const UIShortcutContext& GetShortcutContext() const {
return m_shortcutContext;
}
template <typename HandlerFn>
UIInputDispatchSummary Dispatch(
const UIInputEvent& event,
@@ -66,15 +78,23 @@ public:
m_focusController.SetActivePath(capturePath.Empty() ? hoveredPath : capturePath);
}
const UIShortcutContext shortcutContext = {
m_focusController.GetFocusedPath(),
m_focusController.GetActivePath(),
hoveredPath };
if (ShouldStartActivePathOnKeyDown(event)) {
m_focusController.SetActivePath(m_focusController.GetFocusedPath());
}
const UIShortcutContext shortcutContext = BuildEffectiveShortcutContext(hoveredPath);
const UIShortcutMatch shortcutMatch = m_shortcutRegistry.Match(event, shortcutContext);
if (shortcutMatch.matched) {
summary.shortcutHandled = true;
summary.shortcutMatched = true;
summary.commandId = shortcutMatch.binding.commandId;
return FinalizeDispatch(event, std::move(summary));
summary.shortcutScope = shortcutMatch.binding.scope;
summary.shortcutOwnerId = shortcutMatch.binding.ownerId;
if (ShouldSuppressShortcutMatch(event, shortcutMatch, shortcutContext)) {
summary.shortcutSuppressed = true;
} else {
summary.shortcutHandled = true;
return FinalizeDispatch(event, std::move(summary));
}
}
UIInputRouteContext routeContext = {};
@@ -97,6 +117,20 @@ private:
const UIInputEvent& event,
const UIInputPath& hoveredPath) const;
bool ShouldStartActivePathOnKeyDown(
const UIInputEvent& event) const;
bool ShouldClearActivePathOnKeyUp(
const UIInputEvent& event) const;
UIShortcutContext BuildEffectiveShortcutContext(
const UIInputPath& hoveredPath) const;
bool ShouldSuppressShortcutMatch(
const UIInputEvent& event,
const UIShortcutMatch& shortcutMatch,
const UIShortcutContext& shortcutContext) const;
UIInputDispatchSummary FinalizeDispatch(
const UIInputEvent& event,
UIInputDispatchSummary&& summary);
@@ -104,6 +138,7 @@ private:
UIInputDispatcherOptions m_options = {};
UIFocusController m_focusController = {};
UIShortcutRegistry m_shortcutRegistry = {};
UIShortcutContext m_shortcutContext = {};
};
} // namespace UI

View File

@@ -33,10 +33,19 @@ struct UIShortcutBinding {
std::string commandId = {};
};
struct UIShortcutScopeChain {
UIInputPath path = {};
UIElementId windowId = 0;
UIElementId panelId = 0;
UIElementId widgetId = 0;
};
struct UIShortcutContext {
UIInputPath focusedPath = {};
UIInputPath activePath = {};
UIInputPath hoveredPath = {};
UIShortcutScopeChain commandScope = {};
bool textInputActive = false;
};
struct UIShortcutMatch {

View File

@@ -0,0 +1,275 @@
#pragma once
#include <XCEngine/UI/Layout/LayoutTypes.h>
#include <algorithm>
#include <cmath>
namespace XCEngine {
namespace UI {
namespace Layout {
struct UISplitterMetrics {
float thickness = 8.0f;
float hitThickness = 12.0f;
};
struct UISplitterConstraints {
float primaryMin = 0.0f;
float primaryMax = GetUnboundedLayoutExtent();
float secondaryMin = 0.0f;
float secondaryMax = GetUnboundedLayoutExtent();
};
struct UISplitterLayoutOptions {
UILayoutAxis axis = UILayoutAxis::Horizontal;
float ratio = 0.5f;
float handleThickness = 8.0f;
float minPrimaryExtent = 0.0f;
float minSecondaryExtent = 0.0f;
};
struct UISplitterLayoutResult {
UIRect primaryRect = {};
UIRect handleRect = {};
UIRect secondaryRect = {};
float resolvedRatio = 0.5f;
float splitRatio = 0.5f;
float primaryExtent = 0.0f;
float secondaryExtent = 0.0f;
};
namespace SplitterDetail {
inline float ClampSplitterExtent(float value) {
return (std::max)(0.0f, value);
}
inline float ClampFiniteExtent(float value, float minValue, float maxValue) {
return (std::clamp)(value, minValue, maxValue);
}
inline float GetMainExtent(const UISize& size, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? size.width : size.height;
}
inline float GetCrossExtent(const UISize& size, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? size.height : size.width;
}
inline float GetMainExtent(const UIRect& rect, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? rect.width : rect.height;
}
inline float GetCrossExtent(const UIRect& rect, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? rect.height : rect.width;
}
} // namespace SplitterDetail
inline UISize MeasureSplitterDesiredSize(
UILayoutAxis axis,
const UISize& primarySize,
const UISize& secondarySize,
float handleThickness) {
const float clampedHandleThickness = SplitterDetail::ClampSplitterExtent(handleThickness);
if (axis == UILayoutAxis::Horizontal) {
return UISize(
primarySize.width + clampedHandleThickness + secondarySize.width,
(std::max)(primarySize.height, secondarySize.height));
}
return UISize(
(std::max)(primarySize.width, secondarySize.width),
primarySize.height + clampedHandleThickness + secondarySize.height);
}
inline float ClampSplitterRatio(
const UISplitterLayoutOptions& options,
float totalMainExtent) {
const float mainExtent = SplitterDetail::ClampSplitterExtent(totalMainExtent);
const float handleThickness = (std::min)(
SplitterDetail::ClampSplitterExtent(options.handleThickness),
mainExtent);
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
if (usableExtent <= 0.0f) {
return 0.5f;
}
const float requestedPrimaryExtent =
usableExtent * (std::clamp)(options.ratio, 0.0f, 1.0f);
const float minPrimaryExtent = (std::min)(
SplitterDetail::ClampSplitterExtent(options.minPrimaryExtent),
usableExtent);
const float minSecondaryExtent = (std::min)(
SplitterDetail::ClampSplitterExtent(options.minSecondaryExtent),
usableExtent);
float minimumPrimaryExtent = minPrimaryExtent;
float maximumPrimaryExtent = usableExtent - minSecondaryExtent;
if (minimumPrimaryExtent > maximumPrimaryExtent) {
minimumPrimaryExtent = 0.0f;
maximumPrimaryExtent = usableExtent;
}
const float clampedPrimaryExtent = (std::clamp)(
requestedPrimaryExtent,
minimumPrimaryExtent,
maximumPrimaryExtent);
return clampedPrimaryExtent / usableExtent;
}
inline float ClampSplitterRatio(
UILayoutAxis axis,
float requestedRatio,
float totalMainExtent,
const UISplitterConstraints& constraints,
const UISplitterMetrics& metrics) {
UISplitterLayoutOptions options = {};
options.axis = axis;
options.ratio = requestedRatio;
options.handleThickness = metrics.thickness;
options.minPrimaryExtent = constraints.primaryMin;
options.minSecondaryExtent = constraints.secondaryMin;
const float mainExtent = SplitterDetail::ClampSplitterExtent(totalMainExtent);
const float handleThickness = (std::min)(
SplitterDetail::ClampSplitterExtent(metrics.thickness),
mainExtent);
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
if (usableExtent <= 0.0f) {
return 0.5f;
}
const float requestedPrimaryExtent =
usableExtent * (std::clamp)(requestedRatio, 0.0f, 1.0f);
float minimumPrimaryExtent = (std::min)(
SplitterDetail::ClampSplitterExtent(constraints.primaryMin),
usableExtent);
float maximumPrimaryExtent = std::isfinite(constraints.primaryMax)
? (std::clamp)(constraints.primaryMax, minimumPrimaryExtent, usableExtent)
: usableExtent;
const float minimumFromSecondary = (std::max)(
0.0f,
usableExtent - (std::isfinite(constraints.secondaryMax)
? (std::max)(SplitterDetail::ClampSplitterExtent(constraints.secondaryMax), SplitterDetail::ClampSplitterExtent(constraints.secondaryMin))
: usableExtent));
const float maximumFromSecondary = (std::max)(
0.0f,
usableExtent - (std::min)(SplitterDetail::ClampSplitterExtent(constraints.secondaryMin), usableExtent));
minimumPrimaryExtent = (std::max)(minimumPrimaryExtent, minimumFromSecondary);
maximumPrimaryExtent = (std::min)(maximumPrimaryExtent, maximumFromSecondary);
if (minimumPrimaryExtent > maximumPrimaryExtent) {
minimumPrimaryExtent = 0.0f;
maximumPrimaryExtent = usableExtent;
}
const float clampedPrimaryExtent = (std::clamp)(
requestedPrimaryExtent,
minimumPrimaryExtent,
maximumPrimaryExtent);
return clampedPrimaryExtent / usableExtent;
}
inline UISplitterLayoutResult ArrangeSplitterLayout(
const UISplitterLayoutOptions& options,
const UIRect& bounds) {
UISplitterLayoutResult result = {};
const float mainExtent = SplitterDetail::GetMainExtent(bounds, options.axis);
const float crossExtent = SplitterDetail::GetCrossExtent(bounds, options.axis);
const float handleThickness = (std::min)(
SplitterDetail::ClampSplitterExtent(options.handleThickness),
mainExtent);
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
result.resolvedRatio = ClampSplitterRatio(options, mainExtent);
result.primaryExtent = usableExtent * result.resolvedRatio;
result.secondaryExtent = (std::max)(0.0f, usableExtent - result.primaryExtent);
if (options.axis == UILayoutAxis::Horizontal) {
result.primaryRect = UIRect(
bounds.x,
bounds.y,
result.primaryExtent,
crossExtent);
result.handleRect = UIRect(
bounds.x + result.primaryExtent,
bounds.y,
handleThickness,
crossExtent);
result.secondaryRect = UIRect(
result.handleRect.x + handleThickness,
bounds.y,
result.secondaryExtent,
crossExtent);
} else {
result.primaryRect = UIRect(
bounds.x,
bounds.y,
crossExtent,
result.primaryExtent);
result.handleRect = UIRect(
bounds.x,
bounds.y + result.primaryExtent,
crossExtent,
handleThickness);
result.secondaryRect = UIRect(
bounds.x,
result.handleRect.y + handleThickness,
crossExtent,
result.secondaryExtent);
}
result.splitRatio = result.resolvedRatio;
return result;
}
inline float ResolveSplitterRatioFromPointerPosition(
const UISplitterLayoutOptions& options,
const UIRect& bounds,
float pointerMainPosition) {
const float mainExtent = SplitterDetail::GetMainExtent(bounds, options.axis);
const float handleThickness = (std::min)(
SplitterDetail::ClampSplitterExtent(options.handleThickness),
mainExtent);
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
if (usableExtent <= 0.0f) {
return 0.5f;
}
const float origin = options.axis == UILayoutAxis::Horizontal ? bounds.x : bounds.y;
UISplitterLayoutOptions pointerOptions = options;
pointerOptions.ratio = (pointerMainPosition - origin - handleThickness * 0.5f) / usableExtent;
return ClampSplitterRatio(pointerOptions, mainExtent);
}
inline UISplitterLayoutResult ArrangeUISplitter(
const UIRect& bounds,
UILayoutAxis axis,
float requestedRatio,
const UISplitterConstraints& constraints,
const UISplitterMetrics& metrics) {
UISplitterLayoutOptions options = {};
options.axis = axis;
options.ratio = ClampSplitterRatio(
axis,
requestedRatio,
SplitterDetail::GetMainExtent(bounds, axis),
constraints,
metrics);
options.handleThickness = metrics.thickness;
options.minPrimaryExtent = constraints.primaryMin;
options.minSecondaryExtent = constraints.secondaryMin;
UISplitterLayoutResult result = ArrangeSplitterLayout(options, bounds);
result.splitRatio = options.ratio;
result.resolvedRatio = options.ratio;
return result;
}
} // namespace Layout
} // namespace UI
} // namespace XCEngine

View File

@@ -2,6 +2,7 @@
#include <XCEngine/UI/Input/UIInputDispatcher.h>
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
#include <cstdint>
#include <string>
@@ -27,13 +28,28 @@ public:
std::uint64_t totalPointerEventCount = 0u;
UIPoint pointerPosition = {};
bool pointerInsideViewport = false;
bool textInputActive = false;
std::string hoveredStateKey = {};
std::string focusedStateKey = {};
std::string activeStateKey = {};
std::string captureStateKey = {};
std::string commandScopeStateKey = {};
std::string windowScopeStateKey = {};
std::string panelScopeStateKey = {};
std::string widgetScopeStateKey = {};
std::string lastEventType = {};
std::string lastTargetStateKey = {};
std::string lastTargetKind = {};
std::string lastShortcutCommandId = {};
std::string lastShortcutScope = {};
std::string lastShortcutOwnerStateKey = {};
bool lastShortcutHandled = false;
bool lastShortcutSuppressed = false;
std::string recentShortcutCommandId = {};
std::string recentShortcutScope = {};
std::string recentShortcutOwnerStateKey = {};
bool recentShortcutHandled = false;
bool recentShortcutSuppressed = false;
std::string lastResult = {};
};
@@ -60,6 +76,11 @@ public:
const InputDebugSnapshot& GetInputDebugSnapshot() const;
const ScrollDebugSnapshot& GetScrollDebugSnapshot() const;
struct SplitterDragRuntimeState {
std::string stateKey = {};
Widgets::UISplitterDragState drag = {};
};
private:
struct PointerState {
UIPoint position = {};
@@ -69,7 +90,9 @@ private:
UIInputDispatcher m_inputDispatcher;
std::unordered_map<std::string, float> m_verticalScrollOffsets = {};
std::unordered_map<std::string, float> m_splitterRatios = {};
PointerState m_pointerState = {};
SplitterDragRuntimeState m_splitterDragState = {};
InputDebugSnapshot m_inputDebugSnapshot = {};
ScrollDebugSnapshot m_scrollDebugSnapshot = {};
};

View File

@@ -6,7 +6,7 @@ namespace XCEngine {
namespace UI {
enum class UITextureHandleKind : std::uint8_t {
ImGuiDescriptor = 0,
DescriptorHandle = 0,
ShaderResourceView
};
@@ -51,7 +51,7 @@ struct UITextureHandle {
std::uintptr_t nativeHandle = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
UITextureHandleKind kind = UITextureHandleKind::ImGuiDescriptor;
UITextureHandleKind kind = UITextureHandleKind::DescriptorHandle;
constexpr bool IsValid() const {
return nativeHandle != 0 && width > 0 && height > 0;

View File

@@ -1,41 +0,0 @@
#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

@@ -1,126 +0,0 @@
#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

View File

@@ -0,0 +1,124 @@
#pragma once
#include <XCEngine/UI/Core/UIInvalidation.h>
#include <XCEngine/UI/Layout/UISplitterLayout.h>
#include <cmath>
namespace XCEngine {
namespace UI {
namespace Widgets {
struct UISplitterDragState {
bool active = false;
UIElementId ownerId = 0u;
Layout::UILayoutAxis axis = Layout::UILayoutAxis::Horizontal;
UIRect bounds = {};
Layout::UISplitterConstraints constraints = {};
Layout::UISplitterMetrics metrics = {};
float splitRatio = 0.5f;
};
inline UIRect ExpandUISplitterHandleHitRect(
const UIRect& handleRect,
Layout::UILayoutAxis axis,
float crossAxisPadding = 3.0f) {
const float padding = (std::max)(0.0f, crossAxisPadding);
if (axis == Layout::UILayoutAxis::Horizontal) {
return UIRect(
handleRect.x - padding,
handleRect.y,
handleRect.width + padding * 2.0f,
handleRect.height);
}
return UIRect(
handleRect.x,
handleRect.y - padding,
handleRect.width,
handleRect.height + padding * 2.0f);
}
inline bool HitTestUISplitterHandle(
const UIRect& handleRect,
Layout::UILayoutAxis axis,
const UIPoint& point,
float crossAxisPadding = 3.0f) {
const UIRect hitRect = ExpandUISplitterHandleHitRect(handleRect, axis, crossAxisPadding);
return point.x >= hitRect.x &&
point.x <= hitRect.x + hitRect.width &&
point.y >= hitRect.y &&
point.y <= hitRect.y + hitRect.height;
}
inline bool BeginUISplitterDrag(
UIElementId ownerId,
Layout::UILayoutAxis axis,
const UIRect& bounds,
const Layout::UISplitterLayoutResult& currentLayout,
const Layout::UISplitterConstraints& constraints,
const Layout::UISplitterMetrics& metrics,
const UIPoint& pointerPosition,
UISplitterDragState& outState) {
const float extraHitPadding = (std::max)(
0.0f,
(metrics.hitThickness - metrics.thickness) * 0.5f);
if (!HitTestUISplitterHandle(
currentLayout.handleRect,
axis,
pointerPosition,
extraHitPadding)) {
return false;
}
outState = {};
outState.active = true;
outState.ownerId = ownerId;
outState.axis = axis;
outState.bounds = bounds;
outState.constraints = constraints;
outState.metrics = metrics;
outState.splitRatio = currentLayout.splitRatio;
return true;
}
inline bool UpdateUISplitterDrag(
UISplitterDragState& state,
const UIPoint& pointerPosition,
Layout::UISplitterLayoutResult& outLayout) {
if (!state.active) {
return false;
}
const float pointerMainPosition = state.axis == Layout::UILayoutAxis::Horizontal
? pointerPosition.x
: pointerPosition.y;
const float requestedRatio = Layout::ResolveSplitterRatioFromPointerPosition(
Layout::UISplitterLayoutOptions {
state.axis,
state.splitRatio,
state.metrics.thickness,
state.constraints.primaryMin,
state.constraints.secondaryMin
},
state.bounds,
pointerMainPosition);
outLayout = Layout::ArrangeUISplitter(
state.bounds,
state.axis,
requestedRatio,
state.constraints,
state.metrics);
const bool changed = std::fabs(outLayout.splitRatio - state.splitRatio) > 0.0001f;
state.splitRatio = outLayout.splitRatio;
return changed;
}
inline void EndUISplitterDrag(UISplitterDragState& state) {
state = {};
}
} // namespace Widgets
} // namespace UI
} // namespace XCEngine

View File

@@ -1,8 +1,19 @@
#include <XCEngine/UI/Input/UIInputDispatcher.h>
#include <XCEngine/Input/InputTypes.h>
namespace XCEngine {
namespace UI {
namespace {
bool IsKeyboardActivationKey(std::int32_t keyCode) {
return keyCode == static_cast<std::int32_t>(Input::KeyCode::Enter) ||
keyCode == static_cast<std::int32_t>(Input::KeyCode::Space);
}
} // namespace
bool UIInputDispatcher::ShouldTransferFocusOnPointerDown(
const UIInputEvent& event,
const UIInputPath& hoveredPath) const {
@@ -20,10 +31,52 @@ bool UIInputDispatcher::ShouldStartActivePathOnPointerDown(
!hoveredPath.Empty();
}
bool UIInputDispatcher::ShouldStartActivePathOnKeyDown(
const UIInputEvent& event) const {
return event.type == UIInputEventType::KeyDown &&
!m_focusController.GetFocusedPath().Empty() &&
IsKeyboardActivationKey(event.keyCode);
}
bool UIInputDispatcher::ShouldClearActivePathOnKeyUp(
const UIInputEvent& event) const {
return event.type == UIInputEventType::KeyUp &&
IsKeyboardActivationKey(event.keyCode);
}
UIShortcutContext UIInputDispatcher::BuildEffectiveShortcutContext(
const UIInputPath& hoveredPath) const {
UIShortcutContext context = m_shortcutContext;
context.focusedPath = m_focusController.GetFocusedPath();
context.activePath = m_focusController.GetActivePath();
context.hoveredPath = hoveredPath;
if (context.commandScope.path.Empty()) {
context.commandScope.path = !context.focusedPath.Empty()
? context.focusedPath
: context.activePath;
}
if (context.commandScope.widgetId == 0 &&
!context.commandScope.path.Empty()) {
context.commandScope.widgetId = context.commandScope.path.Target();
}
return context;
}
bool UIInputDispatcher::ShouldSuppressShortcutMatch(
const UIInputEvent&,
const UIShortcutMatch& shortcutMatch,
const UIShortcutContext& shortcutContext) const {
return shortcutMatch.matched && shortcutContext.textInputActive;
}
UIInputDispatchSummary UIInputDispatcher::FinalizeDispatch(
const UIInputEvent& event,
UIInputDispatchSummary&& summary) {
if (event.type == UIInputEventType::PointerButtonUp) {
if (event.type == UIInputEventType::PointerButtonUp ||
ShouldClearActivePathOnKeyUp(event)) {
m_focusController.ClearActivePath();
}

View File

@@ -6,14 +6,18 @@ namespace UI {
namespace {
const UIInputPath& ResolvePrimaryShortcutPath(const UIShortcutContext& context) {
if (!context.activePath.Empty()) {
return context.activePath;
if (!context.commandScope.path.Empty()) {
return context.commandScope.path;
}
if (!context.focusedPath.Empty()) {
return context.focusedPath;
}
if (!context.activePath.Empty()) {
return context.activePath;
}
return context.hoveredPath;
}
@@ -26,6 +30,28 @@ bool ModifiersEqual(
lhs.super == rhs.super;
}
UIElementId ResolveScopedOwnerId(
UIShortcutScope scope,
const UIShortcutContext& context) {
switch (scope) {
case UIShortcutScope::Window:
return context.commandScope.windowId;
case UIShortcutScope::Panel:
return context.commandScope.panelId;
case UIShortcutScope::Widget:
if (context.commandScope.widgetId != 0) {
return context.commandScope.widgetId;
}
return context.commandScope.path.Empty()
? 0
: context.commandScope.path.Target();
case UIShortcutScope::Global:
default:
return 0;
}
}
bool IsBindingActive(
const UIShortcutBinding& binding,
const UIShortcutContext& context) {
@@ -37,6 +63,11 @@ bool IsBindingActive(
return false;
}
const UIElementId resolvedOwnerId = ResolveScopedOwnerId(binding.scope, context);
if (resolvedOwnerId != 0) {
return binding.ownerId == resolvedOwnerId;
}
return ResolvePrimaryShortcutPath(context).Contains(binding.ownerId);
}
@@ -91,6 +122,7 @@ bool UIShortcutRegistry::UnregisterBinding(std::uint64_t bindingId) {
void UIShortcutRegistry::Clear() {
m_bindings.clear();
m_nextBindingId = 1;
}
UIShortcutMatch UIShortcutRegistry::Match(

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +0,0 @@
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
namespace XCEngine {
namespace UI {
namespace Widgets {
namespace {
float ResolveFloatToken(
const Style::UITheme& theme,
const char* tokenName,
float fallbackValue) {
const Style::UITokenResolveResult result =
theme.ResolveToken(tokenName, Style::UIStyleValueType::Float);
if (result.status != Style::UITokenResolveStatus::Resolved) {
return fallbackValue;
}
const float* value = result.value.TryGetFloat();
return value != nullptr ? *value : fallbackValue;
}
} // namespace
UIEditorCollectionPrimitiveKind ClassifyUIEditorCollectionPrimitive(std::string_view tagName) {
if (tagName == "ScrollView") {
return UIEditorCollectionPrimitiveKind::ScrollView;
}
if (tagName == "TreeView") {
return UIEditorCollectionPrimitiveKind::TreeView;
}
if (tagName == "TreeItem") {
return UIEditorCollectionPrimitiveKind::TreeItem;
}
if (tagName == "ListView") {
return UIEditorCollectionPrimitiveKind::ListView;
}
if (tagName == "ListItem") {
return UIEditorCollectionPrimitiveKind::ListItem;
}
if (tagName == "PropertySection") {
return UIEditorCollectionPrimitiveKind::PropertySection;
}
if (tagName == "FieldRow") {
return UIEditorCollectionPrimitiveKind::FieldRow;
}
return UIEditorCollectionPrimitiveKind::None;
}
bool IsUIEditorCollectionPrimitiveContainer(UIEditorCollectionPrimitiveKind kind) {
return kind == UIEditorCollectionPrimitiveKind::ScrollView ||
kind == UIEditorCollectionPrimitiveKind::TreeView ||
kind == UIEditorCollectionPrimitiveKind::ListView ||
kind == UIEditorCollectionPrimitiveKind::PropertySection;
}
bool UsesUIEditorCollectionPrimitiveColumnLayout(UIEditorCollectionPrimitiveKind kind) {
return kind == UIEditorCollectionPrimitiveKind::TreeView ||
kind == UIEditorCollectionPrimitiveKind::ListView ||
kind == UIEditorCollectionPrimitiveKind::PropertySection;
}
bool IsUIEditorCollectionPrimitiveHoverable(UIEditorCollectionPrimitiveKind kind) {
return kind == UIEditorCollectionPrimitiveKind::TreeItem ||
kind == UIEditorCollectionPrimitiveKind::ListItem ||
kind == UIEditorCollectionPrimitiveKind::PropertySection ||
kind == UIEditorCollectionPrimitiveKind::FieldRow;
}
bool DoesUIEditorCollectionPrimitiveClipChildren(UIEditorCollectionPrimitiveKind kind) {
return kind == UIEditorCollectionPrimitiveKind::ScrollView ||
kind == UIEditorCollectionPrimitiveKind::TreeView ||
kind == UIEditorCollectionPrimitiveKind::ListView;
}
float ResolveUIEditorCollectionPrimitivePadding(
UIEditorCollectionPrimitiveKind kind,
const Style::UITheme& theme) {
return kind == UIEditorCollectionPrimitiveKind::TreeView ||
kind == UIEditorCollectionPrimitiveKind::ListView ||
kind == UIEditorCollectionPrimitiveKind::PropertySection
? ResolveFloatToken(theme, "space.cardInset", 12.0f)
: 0.0f;
}
float ResolveUIEditorCollectionPrimitiveDefaultHeight(
UIEditorCollectionPrimitiveKind kind,
const Style::UITheme& theme) {
switch (kind) {
case UIEditorCollectionPrimitiveKind::TreeItem:
return ResolveFloatToken(theme, "size.treeItemHeight", 28.0f);
case UIEditorCollectionPrimitiveKind::ListItem:
return ResolveFloatToken(theme, "size.listItemHeight", 60.0f);
case UIEditorCollectionPrimitiveKind::FieldRow:
return ResolveFloatToken(theme, "size.fieldRowHeight", 32.0f);
case UIEditorCollectionPrimitiveKind::PropertySection:
return ResolveFloatToken(theme, "size.propertySectionHeight", 148.0f);
default:
return 0.0f;
}
}
float ResolveUIEditorCollectionPrimitiveIndent(
UIEditorCollectionPrimitiveKind kind,
const Style::UITheme& theme,
float indentLevel) {
return kind == UIEditorCollectionPrimitiveKind::TreeItem
? indentLevel * ResolveFloatToken(theme, "size.treeIndent", 18.0f)
: 0.0f;
}
} // namespace Widgets
} // namespace UI
} // namespace XCEngine