Build XCUI splitter foundation and test harness
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
275
engine/include/XCEngine/UI/Layout/UISplitterLayout.h
Normal file
275
engine/include/XCEngine/UI/Layout/UISplitterLayout.h
Normal 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
|
||||
@@ -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 = {};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
124
engine/include/XCEngine/UI/Widgets/UISplitterInteraction.h
Normal file
124
engine/include/XCEngine/UI/Widgets/UISplitterInteraction.h
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user