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

@@ -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