Add XCUI command routing and widget state models

This commit is contained in:
2026-04-05 12:10:55 +08:00
parent 511e94fd30
commit 68c4c80b06
18 changed files with 1329 additions and 9 deletions

View File

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

View File

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

View 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

View 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