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

@@ -522,9 +522,13 @@ add_library(XCEngine STATIC
${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
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UISelectionModel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenTypes.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h

View File

@@ -0,0 +1,40 @@
#pragma once
#include <cstddef>
namespace XCEngine {
namespace UI {
namespace Widgets {
class UIKeyboardNavigationModel {
public:
static constexpr std::size_t InvalidIndex = static_cast<std::size_t>(-1);
std::size_t GetItemCount() const;
bool SetItemCount(std::size_t itemCount);
bool ClampToItemCount();
bool HasCurrentIndex() const;
std::size_t GetCurrentIndex() const;
bool SetCurrentIndex(std::size_t index, bool updateAnchor = true);
bool ClearCurrentIndex();
bool HasSelectionAnchor() const;
std::size_t GetSelectionAnchorIndex() const;
bool SetSelectionAnchorIndex(std::size_t index);
bool ClearSelectionAnchor();
bool MoveNext();
bool MovePrevious();
bool MoveHome();
bool MoveEnd();
private:
std::size_t m_itemCount = 0;
std::size_t m_currentIndex = InvalidIndex;
std::size_t m_selectionAnchorIndex = InvalidIndex;
};
} // namespace Widgets
} // namespace UI
} // namespace XCEngine

View File

@@ -0,0 +1,32 @@
#pragma once
#include <string>
namespace XCEngine {
namespace UI {
namespace Widgets {
class UIPropertyEditModel {
public:
bool HasActiveEdit() const;
const std::string& GetActiveFieldId() const;
const std::string& GetStagedValue() const;
bool IsDirty() const;
bool BeginEdit(std::string fieldId, std::string initialValue);
bool UpdateStagedValue(std::string stagedValue);
bool CommitEdit(
std::string* outFieldId = nullptr,
std::string* outCommittedValue = nullptr);
bool CancelEdit();
private:
std::string m_activeFieldId = {};
std::string m_baselineValue = {};
std::string m_stagedValue = {};
bool m_dirty = false;
};
} // namespace Widgets
} // namespace UI
} // namespace XCEngine

View File

@@ -0,0 +1,162 @@
#include <XCEngine/UI/Widgets/UIKeyboardNavigationModel.h>
namespace XCEngine {
namespace UI {
namespace Widgets {
namespace {
bool ClampIndexToCount(std::size_t itemCount, std::size_t& index) {
if (index == UIKeyboardNavigationModel::InvalidIndex) {
return false;
}
if (itemCount == 0u) {
index = UIKeyboardNavigationModel::InvalidIndex;
return true;
}
if (index < itemCount) {
return false;
}
index = itemCount - 1u;
return true;
}
} // namespace
std::size_t UIKeyboardNavigationModel::GetItemCount() const {
return m_itemCount;
}
bool UIKeyboardNavigationModel::SetItemCount(std::size_t itemCount) {
const bool countChanged = m_itemCount != itemCount;
m_itemCount = itemCount;
return ClampToItemCount() || countChanged;
}
bool UIKeyboardNavigationModel::ClampToItemCount() {
bool changed = false;
changed = ClampIndexToCount(m_itemCount, m_currentIndex) || changed;
changed = ClampIndexToCount(m_itemCount, m_selectionAnchorIndex) || changed;
return changed;
}
bool UIKeyboardNavigationModel::HasCurrentIndex() const {
return m_currentIndex != InvalidIndex;
}
std::size_t UIKeyboardNavigationModel::GetCurrentIndex() const {
return m_currentIndex;
}
bool UIKeyboardNavigationModel::SetCurrentIndex(std::size_t index, bool updateAnchor) {
if (index >= m_itemCount) {
return false;
}
bool changed = false;
if (m_currentIndex != index) {
m_currentIndex = index;
changed = true;
}
if (updateAnchor && m_selectionAnchorIndex != index) {
m_selectionAnchorIndex = index;
changed = true;
}
return changed;
}
bool UIKeyboardNavigationModel::ClearCurrentIndex() {
if (m_currentIndex == InvalidIndex) {
return false;
}
m_currentIndex = InvalidIndex;
return true;
}
bool UIKeyboardNavigationModel::HasSelectionAnchor() const {
return m_selectionAnchorIndex != InvalidIndex;
}
std::size_t UIKeyboardNavigationModel::GetSelectionAnchorIndex() const {
return m_selectionAnchorIndex;
}
bool UIKeyboardNavigationModel::SetSelectionAnchorIndex(std::size_t index) {
if (index >= m_itemCount) {
return false;
}
if (m_selectionAnchorIndex == index) {
return false;
}
m_selectionAnchorIndex = index;
return true;
}
bool UIKeyboardNavigationModel::ClearSelectionAnchor() {
if (m_selectionAnchorIndex == InvalidIndex) {
return false;
}
m_selectionAnchorIndex = InvalidIndex;
return true;
}
bool UIKeyboardNavigationModel::MoveNext() {
if (m_itemCount == 0u) {
return false;
}
if (m_currentIndex == InvalidIndex) {
return SetCurrentIndex(0u);
}
if ((m_currentIndex + 1u) >= m_itemCount) {
return false;
}
return SetCurrentIndex(m_currentIndex + 1u);
}
bool UIKeyboardNavigationModel::MovePrevious() {
if (m_itemCount == 0u) {
return false;
}
if (m_currentIndex == InvalidIndex) {
return SetCurrentIndex(m_itemCount - 1u);
}
if (m_currentIndex == 0u) {
return false;
}
return SetCurrentIndex(m_currentIndex - 1u);
}
bool UIKeyboardNavigationModel::MoveHome() {
if (m_itemCount == 0u) {
return false;
}
return SetCurrentIndex(0u);
}
bool UIKeyboardNavigationModel::MoveEnd() {
if (m_itemCount == 0u) {
return false;
}
return SetCurrentIndex(m_itemCount - 1u);
}
} // namespace Widgets
} // namespace UI
} // namespace XCEngine

View File

@@ -0,0 +1,95 @@
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
#include <utility>
namespace XCEngine {
namespace UI {
namespace Widgets {
bool UIPropertyEditModel::HasActiveEdit() const {
return !m_activeFieldId.empty();
}
const std::string& UIPropertyEditModel::GetActiveFieldId() const {
return m_activeFieldId;
}
const std::string& UIPropertyEditModel::GetStagedValue() const {
return m_stagedValue;
}
bool UIPropertyEditModel::IsDirty() const {
return m_dirty;
}
bool UIPropertyEditModel::BeginEdit(std::string fieldId, std::string initialValue) {
if (fieldId.empty()) {
return false;
}
const bool stateChanged =
m_activeFieldId != fieldId ||
m_baselineValue != initialValue ||
m_stagedValue != initialValue ||
m_dirty;
if (!stateChanged) {
return false;
}
m_activeFieldId = std::move(fieldId);
m_baselineValue = std::move(initialValue);
m_stagedValue = m_baselineValue;
m_dirty = false;
return true;
}
bool UIPropertyEditModel::UpdateStagedValue(std::string stagedValue) {
if (!HasActiveEdit()) {
return false;
}
if (m_stagedValue == stagedValue) {
return false;
}
m_stagedValue = std::move(stagedValue);
m_dirty = (m_stagedValue != m_baselineValue);
return true;
}
bool UIPropertyEditModel::CommitEdit(
std::string* outFieldId,
std::string* outCommittedValue) {
if (!HasActiveEdit()) {
return false;
}
if (outFieldId != nullptr) {
*outFieldId = m_activeFieldId;
}
if (outCommittedValue != nullptr) {
*outCommittedValue = m_stagedValue;
}
m_activeFieldId.clear();
m_baselineValue.clear();
m_stagedValue.clear();
m_dirty = false;
return true;
}
bool UIPropertyEditModel::CancelEdit() {
if (!HasActiveEdit()) {
return false;
}
m_activeFieldId.clear();
m_baselineValue.clear();
m_stagedValue.clear();
m_dirty = false;
return true;
}
} // namespace Widgets
} // namespace UI
} // namespace XCEngine