Add XCUI command routing and widget state models
This commit is contained in:
@@ -116,6 +116,7 @@ struct RuntimeBuildContext {
|
||||
bool accentEnabled = false;
|
||||
std::string resourceError = {};
|
||||
std::string lastCommandId = {};
|
||||
std::vector<std::string> pendingCommandIds = {};
|
||||
XCUIAssetDocumentSource documentSource = XCUIAssetDocumentSource(
|
||||
XCUIAssetDocumentSource::MakeDemoPathSet());
|
||||
fs::path repoRoot = {};
|
||||
@@ -945,6 +946,15 @@ std::string ResolveTextInputStateKey(const DemoNode& node) {
|
||||
return !stateKey.empty() ? stateKey : node.elementKey;
|
||||
}
|
||||
|
||||
void RecordCommand(RuntimeBuildContext& state, std::string commandId) {
|
||||
if (commandId.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.lastCommandId = commandId;
|
||||
state.pendingCommandIds.push_back(std::move(commandId));
|
||||
}
|
||||
|
||||
void EnsureTextInputStateInitialized(RuntimeBuildContext& state, const DemoNode& node) {
|
||||
if (!IsTextInputNode(node)) {
|
||||
return;
|
||||
@@ -1061,7 +1071,7 @@ bool HandleTextInputCharacterInput(
|
||||
return false;
|
||||
}
|
||||
|
||||
state.lastCommandId = "demo.text.edit." + stateKey;
|
||||
RecordCommand(state, "demo.text.edit." + stateKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1089,9 +1099,9 @@ bool HandleTextInputKeyDown(
|
||||
}
|
||||
|
||||
if (result.submitRequested) {
|
||||
state.lastCommandId = "demo.text.submit." + stateKey;
|
||||
RecordCommand(state, "demo.text.submit." + stateKey);
|
||||
} else if (result.valueChanged) {
|
||||
state.lastCommandId = "demo.text.edit." + stateKey;
|
||||
RecordCommand(state, "demo.text.edit." + stateKey);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1130,7 +1140,7 @@ void ActivateNode(RuntimeBuildContext& state, UIElementId elementId) {
|
||||
state.accentEnabled = !state.accentEnabled;
|
||||
}
|
||||
|
||||
state.lastCommandId = BuildActivationCommandId(node);
|
||||
RecordCommand(state, BuildActivationCommandId(node));
|
||||
}
|
||||
|
||||
void BuildDemoNodesRecursive(
|
||||
@@ -2149,6 +2159,13 @@ const XCUIDemoFrameResult& XCUIDemoRuntime::GetFrameResult() const {
|
||||
return m_state->data.frameResult;
|
||||
}
|
||||
|
||||
std::vector<std::string> XCUIDemoRuntime::DrainPendingCommandIds() {
|
||||
RuntimeBuildContext& state = m_state->data;
|
||||
std::vector<std::string> drainedCommandIds = std::move(state.pendingCommandIds);
|
||||
state.pendingCommandIds.clear();
|
||||
return drainedCommandIds;
|
||||
}
|
||||
|
||||
bool XCUIDemoRuntime::TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const {
|
||||
const RuntimeBuildContext& state = m_state->data;
|
||||
const auto it = state.nodeIndexByKey.find(elementId);
|
||||
|
||||
@@ -65,6 +65,7 @@ public:
|
||||
|
||||
const XCUIDemoFrameResult& Update(const XCUIDemoInputState& input);
|
||||
const XCUIDemoFrameResult& GetFrameResult() const;
|
||||
std::vector<std::string> DrainPendingCommandIds();
|
||||
|
||||
bool TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const;
|
||||
|
||||
|
||||
210
new_editor/src/XCUIBackend/XCUIEditorCommandRouter.cpp
Normal file
210
new_editor/src/XCUIBackend/XCUIEditorCommandRouter.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "XCUIBackend/XCUIEditorCommandRouter.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace XCUIBackend {
|
||||
|
||||
namespace {
|
||||
|
||||
bool ModifiersMatch(
|
||||
const UI::UIInputModifiers& required,
|
||||
const UI::UIInputModifiers& actual,
|
||||
bool exact) {
|
||||
if (exact) {
|
||||
return required.shift == actual.shift &&
|
||||
required.control == actual.control &&
|
||||
required.alt == actual.alt &&
|
||||
required.super == actual.super;
|
||||
}
|
||||
|
||||
return (!required.shift || actual.shift) &&
|
||||
(!required.control || actual.control) &&
|
||||
(!required.alt || actual.alt) &&
|
||||
(!required.super || actual.super);
|
||||
}
|
||||
|
||||
bool AcceleratorMatchesSnapshot(
|
||||
const XCUIEditorCommandAccelerator& accelerator,
|
||||
const XCUIEditorCommandInputSnapshot& snapshot) {
|
||||
if (!accelerator.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const XCUIEditorCommandKeyState* keyState = snapshot.FindKeyState(accelerator.keyCode);
|
||||
if (keyState == nullptr || !keyState->down) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyState->repeat && !accelerator.allowRepeat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ModifiersMatch(accelerator.modifiers, snapshot.modifiers, accelerator.exactModifiers);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const XCUIEditorCommandKeyState* XCUIEditorCommandInputSnapshot::FindKeyState(std::int32_t keyCode) const {
|
||||
for (const XCUIEditorCommandKeyState& keyState : keys) {
|
||||
if (keyState.keyCode == keyCode) {
|
||||
return &keyState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandInputSnapshot::IsKeyDown(std::int32_t keyCode) const {
|
||||
const XCUIEditorCommandKeyState* keyState = FindKeyState(keyCode);
|
||||
return keyState != nullptr && keyState->down;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandAccelerator::IsValid() const {
|
||||
return keyCode != 0;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandDefinition::IsValid() const {
|
||||
return !commandId.empty() && static_cast<bool>(invoke);
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::RegisterCommand(const XCUIEditorCommandDefinition& definition) {
|
||||
if (!definition.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CommandRecord replacement = {};
|
||||
replacement.commandId = definition.commandId;
|
||||
replacement.invoke = definition.invoke;
|
||||
replacement.isEnabled = definition.isEnabled;
|
||||
replacement.accelerators = definition.accelerators;
|
||||
|
||||
if (CommandRecord* existing = FindRecord(definition.commandId)) {
|
||||
*existing = std::move(replacement);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_commands.push_back(std::move(replacement));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::UnregisterCommand(std::string_view commandId) {
|
||||
const auto it = std::find_if(
|
||||
m_commands.begin(),
|
||||
m_commands.end(),
|
||||
[commandId](const CommandRecord& record) {
|
||||
return record.commandId == commandId;
|
||||
});
|
||||
if (it == m_commands.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_commands.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
void XCUIEditorCommandRouter::Clear() {
|
||||
m_commands.clear();
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::HasCommand(std::string_view commandId) const {
|
||||
return FindRecord(commandId) != nullptr;
|
||||
}
|
||||
|
||||
std::size_t XCUIEditorCommandRouter::GetCommandCount() const {
|
||||
return m_commands.size();
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::IsCommandEnabled(std::string_view commandId) const {
|
||||
const CommandRecord* record = FindRecord(commandId);
|
||||
return record != nullptr && EvaluateEnabled(*record);
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::InvokeCommand(std::string_view commandId) const {
|
||||
const CommandRecord* record = FindRecord(commandId);
|
||||
if (record == nullptr || !EvaluateEnabled(*record) || !record->invoke) {
|
||||
return false;
|
||||
}
|
||||
|
||||
record->invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
XCUIEditorCommandShortcutMatch XCUIEditorCommandRouter::MatchShortcut(
|
||||
const XCUIEditorCommandShortcutQuery& query) const {
|
||||
XCUIEditorCommandShortcutMatch result = {};
|
||||
if (query.snapshot == nullptr) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const XCUIEditorCommandInputSnapshot& snapshot = *query.snapshot;
|
||||
if (query.requireWindowFocused && !snapshot.windowFocused) {
|
||||
return result;
|
||||
}
|
||||
if (!query.allowWhenKeyboardCaptured && snapshot.wantCaptureKeyboard) {
|
||||
return result;
|
||||
}
|
||||
if (!query.allowWhenTextInputActive && snapshot.wantTextInput) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const CommandRecord& record : m_commands) {
|
||||
if (!EvaluateEnabled(record)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const XCUIEditorCommandAccelerator& accelerator : record.accelerators) {
|
||||
if (!AcceleratorMatchesSnapshot(accelerator, snapshot)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.matched = true;
|
||||
result.commandId = record.commandId;
|
||||
result.accelerator = accelerator;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::InvokeMatchingShortcut(
|
||||
const XCUIEditorCommandShortcutQuery& query,
|
||||
XCUIEditorCommandShortcutMatch* outMatch) const {
|
||||
const XCUIEditorCommandShortcutMatch match = MatchShortcut(query);
|
||||
if (outMatch != nullptr) {
|
||||
*outMatch = match;
|
||||
}
|
||||
|
||||
return match.matched && InvokeCommand(match.commandId);
|
||||
}
|
||||
|
||||
XCUIEditorCommandRouter::CommandRecord* XCUIEditorCommandRouter::FindRecord(std::string_view commandId) {
|
||||
const auto it = std::find_if(
|
||||
m_commands.begin(),
|
||||
m_commands.end(),
|
||||
[commandId](const CommandRecord& record) {
|
||||
return record.commandId == commandId;
|
||||
});
|
||||
return it != m_commands.end() ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
const XCUIEditorCommandRouter::CommandRecord* XCUIEditorCommandRouter::FindRecord(
|
||||
std::string_view commandId) const {
|
||||
const auto it = std::find_if(
|
||||
m_commands.begin(),
|
||||
m_commands.end(),
|
||||
[commandId](const CommandRecord& record) {
|
||||
return record.commandId == commandId;
|
||||
});
|
||||
return it != m_commands.end() ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::EvaluateEnabled(const CommandRecord& record) {
|
||||
return !record.isEnabled || record.isEnabled();
|
||||
}
|
||||
|
||||
} // namespace XCUIBackend
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
101
new_editor/src/XCUIBackend/XCUIEditorCommandRouter.h
Normal file
101
new_editor/src/XCUIBackend/XCUIEditorCommandRouter.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace XCUIBackend {
|
||||
|
||||
struct XCUIEditorCommandKeyState {
|
||||
std::int32_t keyCode = 0;
|
||||
bool down = false;
|
||||
bool repeat = false;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandInputSnapshot {
|
||||
UI::UIInputModifiers modifiers = {};
|
||||
bool windowFocused = false;
|
||||
bool wantCaptureKeyboard = false;
|
||||
bool wantTextInput = false;
|
||||
std::vector<XCUIEditorCommandKeyState> keys = {};
|
||||
|
||||
const XCUIEditorCommandKeyState* FindKeyState(std::int32_t keyCode) const;
|
||||
bool IsKeyDown(std::int32_t keyCode) const;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandAccelerator {
|
||||
std::int32_t keyCode = 0;
|
||||
UI::UIInputModifiers modifiers = {};
|
||||
bool exactModifiers = true;
|
||||
bool allowRepeat = false;
|
||||
|
||||
bool IsValid() const;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandDefinition {
|
||||
using InvokeCallback = std::function<void()>;
|
||||
using EnabledPredicate = std::function<bool()>;
|
||||
|
||||
std::string commandId = {};
|
||||
InvokeCallback invoke = {};
|
||||
EnabledPredicate isEnabled = {};
|
||||
std::vector<XCUIEditorCommandAccelerator> accelerators = {};
|
||||
|
||||
bool IsValid() const;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandShortcutQuery {
|
||||
const XCUIEditorCommandInputSnapshot* snapshot = nullptr;
|
||||
bool requireWindowFocused = true;
|
||||
bool allowWhenKeyboardCaptured = false;
|
||||
bool allowWhenTextInputActive = false;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandShortcutMatch {
|
||||
bool matched = false;
|
||||
std::string commandId = {};
|
||||
XCUIEditorCommandAccelerator accelerator = {};
|
||||
};
|
||||
|
||||
class XCUIEditorCommandRouter {
|
||||
public:
|
||||
bool RegisterCommand(const XCUIEditorCommandDefinition& definition);
|
||||
bool UnregisterCommand(std::string_view commandId);
|
||||
void Clear();
|
||||
|
||||
bool HasCommand(std::string_view commandId) const;
|
||||
std::size_t GetCommandCount() const;
|
||||
bool IsCommandEnabled(std::string_view commandId) const;
|
||||
|
||||
bool InvokeCommand(std::string_view commandId) const;
|
||||
XCUIEditorCommandShortcutMatch MatchShortcut(
|
||||
const XCUIEditorCommandShortcutQuery& query) const;
|
||||
bool InvokeMatchingShortcut(
|
||||
const XCUIEditorCommandShortcutQuery& query,
|
||||
XCUIEditorCommandShortcutMatch* outMatch = nullptr) const;
|
||||
|
||||
private:
|
||||
struct CommandRecord {
|
||||
std::string commandId = {};
|
||||
XCUIEditorCommandDefinition::InvokeCallback invoke = {};
|
||||
XCUIEditorCommandDefinition::EnabledPredicate isEnabled = {};
|
||||
std::vector<XCUIEditorCommandAccelerator> accelerators = {};
|
||||
};
|
||||
|
||||
CommandRecord* FindRecord(std::string_view commandId);
|
||||
const CommandRecord* FindRecord(std::string_view commandId) const;
|
||||
static bool EvaluateEnabled(const CommandRecord& record);
|
||||
|
||||
std::vector<CommandRecord> m_commands = {};
|
||||
};
|
||||
|
||||
} // namespace XCUIBackend
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
Reference in New Issue
Block a user