Add XCUI input focus shortcut MVP
This commit is contained in:
@@ -405,6 +405,35 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Platform/Windows/WindowsInputModule.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Platform/Windows/WindowsInputModule.cpp
|
||||
|
||||
# UI
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIInvalidation.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIViewModel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIBuildContext.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIElementTree.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIContext.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIBuildContext.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIElementTree.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Layout/LayoutTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Layout/LayoutEngine.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/Theme.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleSet.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleResolver.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/StyleTypes.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/Theme.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/StyleResolver.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIInputPath.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIFocusController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIShortcutRegistry.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIInputRouter.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIInputDispatcher.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputPath.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIFocusController.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIShortcutRegistry.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputRouter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputDispatcher.cpp
|
||||
|
||||
# Input
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Input/InputTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Input/InputEvent.h
|
||||
|
||||
65
engine/include/XCEngine/UI/Input/UIFocusController.h
Normal file
65
engine/include/XCEngine/UI/Input/UIFocusController.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "UIInputPath.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
struct UIFocusChange {
|
||||
UIInputPath previousPath = {};
|
||||
UIInputPath currentPath = {};
|
||||
UIInputPath lostPath = {};
|
||||
UIInputPath gainedPath = {};
|
||||
|
||||
bool Changed() const {
|
||||
return previousPath != currentPath;
|
||||
}
|
||||
};
|
||||
|
||||
class UIFocusController {
|
||||
public:
|
||||
const UIInputPath& GetFocusedPath() const {
|
||||
return m_focusedPath;
|
||||
}
|
||||
|
||||
const UIInputPath& GetActivePath() const {
|
||||
return m_activePath;
|
||||
}
|
||||
|
||||
const UIInputPath& GetPointerCapturePath() const {
|
||||
return m_pointerCapturePath;
|
||||
}
|
||||
|
||||
bool HasFocus() const {
|
||||
return !m_focusedPath.Empty();
|
||||
}
|
||||
|
||||
bool HasActivePath() const {
|
||||
return !m_activePath.Empty();
|
||||
}
|
||||
|
||||
bool HasPointerCapture() const {
|
||||
return !m_pointerCapturePath.Empty();
|
||||
}
|
||||
|
||||
UIFocusChange SetFocusedPath(const UIInputPath& path);
|
||||
UIFocusChange ClearFocus();
|
||||
|
||||
void SetActivePath(const UIInputPath& path);
|
||||
void ClearActivePath();
|
||||
|
||||
void SetPointerCapturePath(const UIInputPath& path);
|
||||
void ClearPointerCapturePath();
|
||||
|
||||
bool IsFocused(UIElementId elementId) const;
|
||||
bool IsActive(UIElementId elementId) const;
|
||||
bool IsCapturingPointer(UIElementId elementId) const;
|
||||
|
||||
private:
|
||||
UIInputPath m_focusedPath = {};
|
||||
UIInputPath m_activePath = {};
|
||||
UIInputPath m_pointerCapturePath = {};
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
110
engine/include/XCEngine/UI/Input/UIInputDispatcher.h
Normal file
110
engine/include/XCEngine/UI/Input/UIInputDispatcher.h
Normal file
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include "UIFocusController.h"
|
||||
#include "UIInputRouter.h"
|
||||
#include "UIShortcutRegistry.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
struct UIInputDispatcherOptions {
|
||||
bool pointerDownChangesFocus = true;
|
||||
bool pointerDownStartsActivePath = true;
|
||||
UIPointerButton focusTransferButton = UIPointerButton::Left;
|
||||
};
|
||||
|
||||
struct UIInputDispatchSummary {
|
||||
UIFocusChange focusChange = {};
|
||||
UIInputDispatchResult routing = {};
|
||||
bool shortcutHandled = false;
|
||||
std::string commandId = {};
|
||||
|
||||
bool Handled() const {
|
||||
return shortcutHandled || routing.handled;
|
||||
}
|
||||
};
|
||||
|
||||
class UIInputDispatcher {
|
||||
public:
|
||||
explicit UIInputDispatcher(
|
||||
const UIInputDispatcherOptions& options = UIInputDispatcherOptions())
|
||||
: m_options(options) {
|
||||
}
|
||||
|
||||
UIFocusController& GetFocusController() {
|
||||
return m_focusController;
|
||||
}
|
||||
|
||||
const UIFocusController& GetFocusController() const {
|
||||
return m_focusController;
|
||||
}
|
||||
|
||||
UIShortcutRegistry& GetShortcutRegistry() {
|
||||
return m_shortcutRegistry;
|
||||
}
|
||||
|
||||
const UIShortcutRegistry& GetShortcutRegistry() const {
|
||||
return m_shortcutRegistry;
|
||||
}
|
||||
|
||||
template <typename HandlerFn>
|
||||
UIInputDispatchSummary Dispatch(
|
||||
const UIInputEvent& event,
|
||||
const UIInputPath& hoveredPath,
|
||||
HandlerFn&& handler) {
|
||||
UIInputDispatchSummary summary = {};
|
||||
|
||||
if (ShouldTransferFocusOnPointerDown(event, hoveredPath)) {
|
||||
summary.focusChange = m_focusController.SetFocusedPath(hoveredPath);
|
||||
}
|
||||
|
||||
if (ShouldStartActivePathOnPointerDown(event, hoveredPath)) {
|
||||
const UIInputPath& capturePath = m_focusController.GetPointerCapturePath();
|
||||
m_focusController.SetActivePath(capturePath.Empty() ? hoveredPath : capturePath);
|
||||
}
|
||||
|
||||
const UIShortcutContext shortcutContext = {
|
||||
m_focusController.GetFocusedPath(),
|
||||
m_focusController.GetActivePath(),
|
||||
hoveredPath };
|
||||
const UIShortcutMatch shortcutMatch = m_shortcutRegistry.Match(event, shortcutContext);
|
||||
if (shortcutMatch.matched) {
|
||||
summary.shortcutHandled = true;
|
||||
summary.commandId = shortcutMatch.binding.commandId;
|
||||
return FinalizeDispatch(event, std::move(summary));
|
||||
}
|
||||
|
||||
UIInputRouteContext routeContext = {};
|
||||
routeContext.hoveredPath = hoveredPath;
|
||||
routeContext.focusedPath = m_focusController.GetFocusedPath();
|
||||
routeContext.capturePath = m_focusController.GetPointerCapturePath();
|
||||
summary.routing = UIInputRouter::Dispatch(
|
||||
event,
|
||||
routeContext,
|
||||
std::forward<HandlerFn>(handler));
|
||||
return FinalizeDispatch(event, std::move(summary));
|
||||
}
|
||||
|
||||
private:
|
||||
bool ShouldTransferFocusOnPointerDown(
|
||||
const UIInputEvent& event,
|
||||
const UIInputPath& hoveredPath) const;
|
||||
|
||||
bool ShouldStartActivePathOnPointerDown(
|
||||
const UIInputEvent& event,
|
||||
const UIInputPath& hoveredPath) const;
|
||||
|
||||
UIInputDispatchSummary FinalizeDispatch(
|
||||
const UIInputEvent& event,
|
||||
UIInputDispatchSummary&& summary);
|
||||
|
||||
UIInputDispatcherOptions m_options = {};
|
||||
UIFocusController m_focusController = {};
|
||||
UIShortcutRegistry m_shortcutRegistry = {};
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
56
engine/include/XCEngine/UI/Input/UIInputPath.h
Normal file
56
engine/include/XCEngine/UI/Input/UIInputPath.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
using UIElementId = std::uint64_t;
|
||||
|
||||
struct UIInputPath {
|
||||
std::vector<UIElementId> elements = {};
|
||||
|
||||
UIInputPath() = default;
|
||||
UIInputPath(std::initializer_list<UIElementId> values)
|
||||
: elements(values) {
|
||||
}
|
||||
|
||||
bool Empty() const {
|
||||
return elements.empty();
|
||||
}
|
||||
|
||||
std::size_t Size() const {
|
||||
return elements.size();
|
||||
}
|
||||
|
||||
UIElementId Root() const {
|
||||
return elements.empty() ? 0u : elements.front();
|
||||
}
|
||||
|
||||
UIElementId Target() const {
|
||||
return elements.empty() ? 0u : elements.back();
|
||||
}
|
||||
|
||||
bool Contains(UIElementId elementId) const;
|
||||
|
||||
void Clear() {
|
||||
elements.clear();
|
||||
}
|
||||
};
|
||||
|
||||
bool operator==(const UIInputPath& lhs, const UIInputPath& rhs);
|
||||
bool operator!=(const UIInputPath& lhs, const UIInputPath& rhs);
|
||||
|
||||
std::size_t GetUIInputPathCommonPrefixLength(
|
||||
const UIInputPath& lhs,
|
||||
const UIInputPath& rhs);
|
||||
|
||||
UIInputPath BuildUIInputPathSuffix(
|
||||
const UIInputPath& path,
|
||||
std::size_t startIndex);
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
104
engine/include/XCEngine/UI/Input/UIInputRouter.h
Normal file
104
engine/include/XCEngine/UI/Input/UIInputRouter.h
Normal file
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include "UIInputPath.h"
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
enum class UIInputRoutingPhase : std::uint8_t {
|
||||
Capture = 0,
|
||||
Target,
|
||||
Bubble
|
||||
};
|
||||
|
||||
enum class UIInputTargetKind : std::uint8_t {
|
||||
None = 0,
|
||||
Hovered,
|
||||
Focused,
|
||||
Captured
|
||||
};
|
||||
|
||||
struct UIInputRouteContext {
|
||||
UIInputPath hoveredPath = {};
|
||||
UIInputPath focusedPath = {};
|
||||
UIInputPath capturePath = {};
|
||||
};
|
||||
|
||||
struct UIInputRoutingStep {
|
||||
UIElementId elementId = 0;
|
||||
UIInputRoutingPhase phase = UIInputRoutingPhase::Target;
|
||||
bool isTargetElement = false;
|
||||
};
|
||||
|
||||
struct UIInputRoutingPlan {
|
||||
UIInputTargetKind targetKind = UIInputTargetKind::None;
|
||||
UIInputPath targetPath = {};
|
||||
std::vector<UIInputRoutingStep> steps = {};
|
||||
|
||||
bool HasTargetPath() const {
|
||||
return !targetPath.Empty();
|
||||
}
|
||||
};
|
||||
|
||||
struct UIInputDispatchRequest {
|
||||
const UIInputEvent* event = nullptr;
|
||||
UIElementId elementId = 0;
|
||||
UIInputRoutingPhase phase = UIInputRoutingPhase::Target;
|
||||
UIInputTargetKind targetKind = UIInputTargetKind::None;
|
||||
bool isTargetElement = false;
|
||||
};
|
||||
|
||||
struct UIInputDispatchDecision {
|
||||
bool handled = false;
|
||||
bool stopPropagation = false;
|
||||
};
|
||||
|
||||
struct UIInputDispatchResult {
|
||||
UIInputRoutingPlan plan = {};
|
||||
bool handled = false;
|
||||
};
|
||||
|
||||
class UIInputRouter {
|
||||
public:
|
||||
static bool IsPointerEvent(UIInputEventType type);
|
||||
static bool IsKeyboardEvent(UIInputEventType type);
|
||||
|
||||
static UIInputRoutingPlan BuildRoutingPlan(
|
||||
const UIInputEvent& event,
|
||||
const UIInputRouteContext& context);
|
||||
|
||||
template <typename HandlerFn>
|
||||
static UIInputDispatchResult Dispatch(
|
||||
const UIInputEvent& event,
|
||||
const UIInputRouteContext& context,
|
||||
HandlerFn&& handler) {
|
||||
UIInputDispatchResult result = {};
|
||||
result.plan = BuildRoutingPlan(event, context);
|
||||
|
||||
for (const UIInputRoutingStep& step : result.plan.steps) {
|
||||
UIInputDispatchRequest request = {};
|
||||
request.event = &event;
|
||||
request.elementId = step.elementId;
|
||||
request.phase = step.phase;
|
||||
request.targetKind = result.plan.targetKind;
|
||||
request.isTargetElement = step.isTargetElement;
|
||||
|
||||
const UIInputDispatchDecision decision = handler(request);
|
||||
result.handled = result.handled || decision.handled;
|
||||
if (decision.stopPropagation) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
67
engine/include/XCEngine/UI/Input/UIShortcutRegistry.h
Normal file
67
engine/include/XCEngine/UI/Input/UIShortcutRegistry.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "UIInputPath.h"
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
enum class UIShortcutScope : std::uint8_t {
|
||||
Global = 0,
|
||||
Window,
|
||||
Panel,
|
||||
Widget
|
||||
};
|
||||
|
||||
struct UIShortcutChord {
|
||||
std::int32_t keyCode = 0;
|
||||
UIInputModifiers modifiers = {};
|
||||
bool allowRepeat = false;
|
||||
};
|
||||
|
||||
struct UIShortcutBinding {
|
||||
std::uint64_t bindingId = 0;
|
||||
UIShortcutScope scope = UIShortcutScope::Global;
|
||||
UIElementId ownerId = 0;
|
||||
UIInputEventType triggerEventType = UIInputEventType::KeyDown;
|
||||
UIShortcutChord chord = {};
|
||||
std::string commandId = {};
|
||||
};
|
||||
|
||||
struct UIShortcutContext {
|
||||
UIInputPath focusedPath = {};
|
||||
UIInputPath activePath = {};
|
||||
UIInputPath hoveredPath = {};
|
||||
};
|
||||
|
||||
struct UIShortcutMatch {
|
||||
bool matched = false;
|
||||
UIShortcutBinding binding = {};
|
||||
};
|
||||
|
||||
class UIShortcutRegistry {
|
||||
public:
|
||||
std::uint64_t RegisterBinding(const UIShortcutBinding& binding);
|
||||
bool UnregisterBinding(std::uint64_t bindingId);
|
||||
void Clear();
|
||||
|
||||
const std::vector<UIShortcutBinding>& GetBindings() const {
|
||||
return m_bindings;
|
||||
}
|
||||
|
||||
UIShortcutMatch Match(
|
||||
const UIInputEvent& event,
|
||||
const UIShortcutContext& context) const;
|
||||
|
||||
private:
|
||||
std::vector<UIShortcutBinding> m_bindings = {};
|
||||
std::uint64_t m_nextBindingId = 1;
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
102
engine/include/XCEngine/UI/Types.h
Normal file
102
engine/include/XCEngine/UI/Types.h
Normal file
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
struct UIPoint {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
|
||||
constexpr UIPoint() = default;
|
||||
constexpr UIPoint(float xValue, float yValue)
|
||||
: x(xValue)
|
||||
, y(yValue) {
|
||||
}
|
||||
};
|
||||
|
||||
struct UISize {
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
|
||||
constexpr UISize() = default;
|
||||
constexpr UISize(float widthValue, float heightValue)
|
||||
: width(widthValue)
|
||||
, height(heightValue) {
|
||||
}
|
||||
};
|
||||
|
||||
struct UIRect {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
|
||||
constexpr UIRect() = default;
|
||||
constexpr UIRect(float xValue, float yValue, float widthValue, float heightValue)
|
||||
: x(xValue)
|
||||
, y(yValue)
|
||||
, width(widthValue)
|
||||
, height(heightValue) {
|
||||
}
|
||||
};
|
||||
|
||||
struct UITextureHandle {
|
||||
std::uintptr_t nativeHandle = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
|
||||
constexpr bool IsValid() const {
|
||||
return nativeHandle != 0 && width > 0 && height > 0;
|
||||
}
|
||||
};
|
||||
|
||||
enum class UIPointerButton : std::uint8_t {
|
||||
None = 0,
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
X1,
|
||||
X2
|
||||
};
|
||||
|
||||
enum class UIInputEventType : std::uint8_t {
|
||||
None = 0,
|
||||
PointerMove,
|
||||
PointerEnter,
|
||||
PointerLeave,
|
||||
PointerButtonDown,
|
||||
PointerButtonUp,
|
||||
PointerWheel,
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
Character,
|
||||
FocusGained,
|
||||
FocusLost
|
||||
};
|
||||
|
||||
struct UIInputModifiers {
|
||||
bool shift = false;
|
||||
bool control = false;
|
||||
bool alt = false;
|
||||
bool super = false;
|
||||
};
|
||||
|
||||
struct UIInputEvent {
|
||||
UIInputEventType type = UIInputEventType::None;
|
||||
UIPoint position = {};
|
||||
UIPoint delta = {};
|
||||
float wheelDelta = 0.0f;
|
||||
std::int32_t keyCode = 0;
|
||||
std::uint32_t character = 0;
|
||||
std::uint32_t pointerId = 0;
|
||||
std::uint64_t timestampNanoseconds = 0;
|
||||
UIPointerButton pointerButton = UIPointerButton::None;
|
||||
UIInputModifiers modifiers = {};
|
||||
bool repeat = false;
|
||||
bool synthetic = false;
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
63
engine/src/UI/Input/UIFocusController.cpp
Normal file
63
engine/src/UI/Input/UIFocusController.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
#include <XCEngine/UI/Input/UIFocusController.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
namespace {
|
||||
|
||||
UIFocusChange BuildFocusChange(
|
||||
const UIInputPath& previousPath,
|
||||
const UIInputPath& currentPath) {
|
||||
UIFocusChange change = {};
|
||||
change.previousPath = previousPath;
|
||||
change.currentPath = currentPath;
|
||||
|
||||
const std::size_t commonPrefixLength =
|
||||
GetUIInputPathCommonPrefixLength(previousPath, currentPath);
|
||||
change.lostPath = BuildUIInputPathSuffix(previousPath, commonPrefixLength);
|
||||
change.gainedPath = BuildUIInputPathSuffix(currentPath, commonPrefixLength);
|
||||
return change;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIFocusChange UIFocusController::SetFocusedPath(const UIInputPath& path) {
|
||||
const UIInputPath previousPath = m_focusedPath;
|
||||
m_focusedPath = path;
|
||||
return BuildFocusChange(previousPath, m_focusedPath);
|
||||
}
|
||||
|
||||
UIFocusChange UIFocusController::ClearFocus() {
|
||||
return SetFocusedPath({});
|
||||
}
|
||||
|
||||
void UIFocusController::SetActivePath(const UIInputPath& path) {
|
||||
m_activePath = path;
|
||||
}
|
||||
|
||||
void UIFocusController::ClearActivePath() {
|
||||
m_activePath.Clear();
|
||||
}
|
||||
|
||||
void UIFocusController::SetPointerCapturePath(const UIInputPath& path) {
|
||||
m_pointerCapturePath = path;
|
||||
}
|
||||
|
||||
void UIFocusController::ClearPointerCapturePath() {
|
||||
m_pointerCapturePath.Clear();
|
||||
}
|
||||
|
||||
bool UIFocusController::IsFocused(UIElementId elementId) const {
|
||||
return m_focusedPath.Contains(elementId);
|
||||
}
|
||||
|
||||
bool UIFocusController::IsActive(UIElementId elementId) const {
|
||||
return m_activePath.Contains(elementId);
|
||||
}
|
||||
|
||||
bool UIFocusController::IsCapturingPointer(UIElementId elementId) const {
|
||||
return m_pointerCapturePath.Contains(elementId);
|
||||
}
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
34
engine/src/UI/Input/UIInputDispatcher.cpp
Normal file
34
engine/src/UI/Input/UIInputDispatcher.cpp
Normal file
@@ -0,0 +1,34 @@
|
||||
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
bool UIInputDispatcher::ShouldTransferFocusOnPointerDown(
|
||||
const UIInputEvent& event,
|
||||
const UIInputPath& hoveredPath) const {
|
||||
return m_options.pointerDownChangesFocus &&
|
||||
event.type == UIInputEventType::PointerButtonDown &&
|
||||
event.pointerButton == m_options.focusTransferButton &&
|
||||
!hoveredPath.Empty();
|
||||
}
|
||||
|
||||
bool UIInputDispatcher::ShouldStartActivePathOnPointerDown(
|
||||
const UIInputEvent& event,
|
||||
const UIInputPath& hoveredPath) const {
|
||||
return m_options.pointerDownStartsActivePath &&
|
||||
event.type == UIInputEventType::PointerButtonDown &&
|
||||
!hoveredPath.Empty();
|
||||
}
|
||||
|
||||
UIInputDispatchSummary UIInputDispatcher::FinalizeDispatch(
|
||||
const UIInputEvent& event,
|
||||
UIInputDispatchSummary&& summary) {
|
||||
if (event.type == UIInputEventType::PointerButtonUp) {
|
||||
m_focusController.ClearActivePath();
|
||||
}
|
||||
|
||||
return std::move(summary);
|
||||
}
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
45
engine/src/UI/Input/UIInputPath.cpp
Normal file
45
engine/src/UI/Input/UIInputPath.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include <XCEngine/UI/Input/UIInputPath.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
bool UIInputPath::Contains(UIElementId elementId) const {
|
||||
return std::find(elements.begin(), elements.end(), elementId) != elements.end();
|
||||
}
|
||||
|
||||
bool operator==(const UIInputPath& lhs, const UIInputPath& rhs) {
|
||||
return lhs.elements == rhs.elements;
|
||||
}
|
||||
|
||||
bool operator!=(const UIInputPath& lhs, const UIInputPath& rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
std::size_t GetUIInputPathCommonPrefixLength(
|
||||
const UIInputPath& lhs,
|
||||
const UIInputPath& rhs) {
|
||||
const std::size_t limit = (std::min)(lhs.elements.size(), rhs.elements.size());
|
||||
std::size_t index = 0;
|
||||
while (index < limit && lhs.elements[index] == rhs.elements[index]) {
|
||||
++index;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
UIInputPath BuildUIInputPathSuffix(
|
||||
const UIInputPath& path,
|
||||
std::size_t startIndex) {
|
||||
UIInputPath suffix = {};
|
||||
if (startIndex >= path.elements.size()) {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
suffix.elements.assign(path.elements.begin() + static_cast<std::ptrdiff_t>(startIndex), path.elements.end());
|
||||
return suffix;
|
||||
}
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
86
engine/src/UI/Input/UIInputRouter.cpp
Normal file
86
engine/src/UI/Input/UIInputRouter.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include <XCEngine/UI/Input/UIInputRouter.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
namespace {
|
||||
|
||||
UIInputRoutingPlan BuildPlanForTargetPath(
|
||||
UIInputTargetKind targetKind,
|
||||
const UIInputPath& path) {
|
||||
UIInputRoutingPlan plan = {};
|
||||
plan.targetKind = targetKind;
|
||||
plan.targetPath = path;
|
||||
|
||||
if (path.Empty()) {
|
||||
return plan;
|
||||
}
|
||||
|
||||
const std::size_t size = path.elements.size();
|
||||
for (std::size_t index = 0; index + 1 < size; ++index) {
|
||||
UIInputRoutingStep step = {};
|
||||
step.elementId = path.elements[index];
|
||||
step.phase = UIInputRoutingPhase::Capture;
|
||||
plan.steps.push_back(step);
|
||||
}
|
||||
|
||||
UIInputRoutingStep targetStep = {};
|
||||
targetStep.elementId = path.Target();
|
||||
targetStep.phase = UIInputRoutingPhase::Target;
|
||||
targetStep.isTargetElement = true;
|
||||
plan.steps.push_back(targetStep);
|
||||
|
||||
for (std::size_t index = size; index > 1; --index) {
|
||||
UIInputRoutingStep bubbleStep = {};
|
||||
bubbleStep.elementId = path.elements[index - 2];
|
||||
bubbleStep.phase = UIInputRoutingPhase::Bubble;
|
||||
plan.steps.push_back(bubbleStep);
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool UIInputRouter::IsPointerEvent(UIInputEventType type) {
|
||||
switch (type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
case UIInputEventType::PointerLeave:
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
case UIInputEventType::PointerWheel:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UIInputRouter::IsKeyboardEvent(UIInputEventType type) {
|
||||
return type == UIInputEventType::KeyDown ||
|
||||
type == UIInputEventType::KeyUp ||
|
||||
type == UIInputEventType::Character;
|
||||
}
|
||||
|
||||
UIInputRoutingPlan UIInputRouter::BuildRoutingPlan(
|
||||
const UIInputEvent& event,
|
||||
const UIInputRouteContext& context) {
|
||||
if (IsPointerEvent(event.type)) {
|
||||
if (!context.capturePath.Empty()) {
|
||||
return BuildPlanForTargetPath(UIInputTargetKind::Captured, context.capturePath);
|
||||
}
|
||||
|
||||
return BuildPlanForTargetPath(UIInputTargetKind::Hovered, context.hoveredPath);
|
||||
}
|
||||
|
||||
if (IsKeyboardEvent(event.type) ||
|
||||
event.type == UIInputEventType::FocusGained ||
|
||||
event.type == UIInputEventType::FocusLost) {
|
||||
return BuildPlanForTargetPath(UIInputTargetKind::Focused, context.focusedPath);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
121
engine/src/UI/Input/UIShortcutRegistry.cpp
Normal file
121
engine/src/UI/Input/UIShortcutRegistry.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include <XCEngine/UI/Input/UIShortcutRegistry.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
|
||||
namespace {
|
||||
|
||||
const UIInputPath& ResolvePrimaryShortcutPath(const UIShortcutContext& context) {
|
||||
if (!context.activePath.Empty()) {
|
||||
return context.activePath;
|
||||
}
|
||||
|
||||
if (!context.focusedPath.Empty()) {
|
||||
return context.focusedPath;
|
||||
}
|
||||
|
||||
return context.hoveredPath;
|
||||
}
|
||||
|
||||
bool ModifiersEqual(
|
||||
const UIInputModifiers& lhs,
|
||||
const UIInputModifiers& rhs) {
|
||||
return lhs.shift == rhs.shift &&
|
||||
lhs.control == rhs.control &&
|
||||
lhs.alt == rhs.alt &&
|
||||
lhs.super == rhs.super;
|
||||
}
|
||||
|
||||
bool IsBindingActive(
|
||||
const UIShortcutBinding& binding,
|
||||
const UIShortcutContext& context) {
|
||||
if (binding.scope == UIShortcutScope::Global) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (binding.ownerId == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ResolvePrimaryShortcutPath(context).Contains(binding.ownerId);
|
||||
}
|
||||
|
||||
bool DoesBindingMatchEvent(
|
||||
const UIShortcutBinding& binding,
|
||||
const UIInputEvent& event) {
|
||||
return binding.triggerEventType == event.type &&
|
||||
binding.chord.keyCode == event.keyCode &&
|
||||
ModifiersEqual(binding.chord.modifiers, event.modifiers) &&
|
||||
(binding.chord.allowRepeat || !event.repeat);
|
||||
}
|
||||
|
||||
int GetShortcutScopePriority(UIShortcutScope scope) {
|
||||
switch (scope) {
|
||||
case UIShortcutScope::Widget:
|
||||
return 3;
|
||||
case UIShortcutScope::Panel:
|
||||
return 2;
|
||||
case UIShortcutScope::Window:
|
||||
return 1;
|
||||
case UIShortcutScope::Global:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::uint64_t UIShortcutRegistry::RegisterBinding(const UIShortcutBinding& binding) {
|
||||
UIShortcutBinding storedBinding = binding;
|
||||
storedBinding.bindingId = storedBinding.bindingId != 0 ? storedBinding.bindingId : m_nextBindingId++;
|
||||
if (storedBinding.bindingId >= m_nextBindingId) {
|
||||
m_nextBindingId = storedBinding.bindingId + 1;
|
||||
}
|
||||
|
||||
m_bindings.push_back(storedBinding);
|
||||
return m_bindings.back().bindingId;
|
||||
}
|
||||
|
||||
bool UIShortcutRegistry::UnregisterBinding(std::uint64_t bindingId) {
|
||||
for (auto it = m_bindings.begin(); it != m_bindings.end(); ++it) {
|
||||
if (it->bindingId != bindingId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_bindings.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void UIShortcutRegistry::Clear() {
|
||||
m_bindings.clear();
|
||||
}
|
||||
|
||||
UIShortcutMatch UIShortcutRegistry::Match(
|
||||
const UIInputEvent& event,
|
||||
const UIShortcutContext& context) const {
|
||||
UIShortcutMatch bestMatch = {};
|
||||
int bestPriority = -1;
|
||||
|
||||
for (const UIShortcutBinding& binding : m_bindings) {
|
||||
if (!DoesBindingMatchEvent(binding, event) || !IsBindingActive(binding, context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int priority = GetShortcutScopePriority(binding.scope);
|
||||
if (!bestMatch.matched ||
|
||||
priority > bestPriority ||
|
||||
(priority == bestPriority && binding.bindingId > bestMatch.binding.bindingId)) {
|
||||
bestMatch.matched = true;
|
||||
bestMatch.binding = binding;
|
||||
bestPriority = priority;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
Reference in New Issue
Block a user