diff --git a/CMakeLists.txt b/CMakeLists.txt index c800b496..55f9a955 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) enable_testing() option(XCENGINE_ENABLE_MONO_SCRIPTING "Build the Mono-based C# scripting runtime" ON) -option(XCENGINE_BUILD_NEW_EDITOR "Build the experimental new_editor skeleton" ON) +option(XCENGINE_BUILD_NEW_EDITOR "Build the XCUI new_editor shell app" ON) set( XCENGINE_MONO_ROOT_DIR "${CMAKE_SOURCE_DIR}/参考/Fermion/Fermion/external/mono" @@ -25,9 +25,7 @@ set( add_subdirectory(engine) add_subdirectory(editor) -if(XCENGINE_BUILD_NEW_EDITOR) - add_subdirectory(new_editor) -endif() +add_subdirectory(new_editor) add_subdirectory(managed) add_subdirectory(mvs/RenderDoc) add_subdirectory(tests) diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 2569b0d2..a1426678 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -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 diff --git a/engine/include/XCEngine/UI/Input/UIInputDispatcher.h b/engine/include/XCEngine/UI/Input/UIInputDispatcher.h index c56e18ab..21c84b0c 100644 --- a/engine/include/XCEngine/UI/Input/UIInputDispatcher.h +++ b/engine/include/XCEngine/UI/Input/UIInputDispatcher.h @@ -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 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 diff --git a/engine/include/XCEngine/UI/Input/UIShortcutRegistry.h b/engine/include/XCEngine/UI/Input/UIShortcutRegistry.h index a683e698..ec22073e 100644 --- a/engine/include/XCEngine/UI/Input/UIShortcutRegistry.h +++ b/engine/include/XCEngine/UI/Input/UIShortcutRegistry.h @@ -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 { diff --git a/engine/include/XCEngine/UI/Layout/UISplitterLayout.h b/engine/include/XCEngine/UI/Layout/UISplitterLayout.h new file mode 100644 index 00000000..117df9cf --- /dev/null +++ b/engine/include/XCEngine/UI/Layout/UISplitterLayout.h @@ -0,0 +1,275 @@ +#pragma once + +#include + +#include +#include + +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 diff --git a/engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h b/engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h index f0358d61..b19f7695 100644 --- a/engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h +++ b/engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -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 m_verticalScrollOffsets = {}; + std::unordered_map m_splitterRatios = {}; PointerState m_pointerState = {}; + SplitterDragRuntimeState m_splitterDragState = {}; InputDebugSnapshot m_inputDebugSnapshot = {}; ScrollDebugSnapshot m_scrollDebugSnapshot = {}; }; diff --git a/engine/include/XCEngine/UI/Types.h b/engine/include/XCEngine/UI/Types.h index da0c88f4..59a1d073 100644 --- a/engine/include/XCEngine/UI/Types.h +++ b/engine/include/XCEngine/UI/Types.h @@ -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; diff --git a/engine/include/XCEngine/UI/Widgets/UISplitterInteraction.h b/engine/include/XCEngine/UI/Widgets/UISplitterInteraction.h new file mode 100644 index 00000000..e40fafec --- /dev/null +++ b/engine/include/XCEngine/UI/Widgets/UISplitterInteraction.h @@ -0,0 +1,124 @@ +#pragma once + +#include +#include + +#include + +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 diff --git a/engine/src/UI/Input/UIInputDispatcher.cpp b/engine/src/UI/Input/UIInputDispatcher.cpp index ce6da490..7888f318 100644 --- a/engine/src/UI/Input/UIInputDispatcher.cpp +++ b/engine/src/UI/Input/UIInputDispatcher.cpp @@ -1,8 +1,19 @@ #include +#include + namespace XCEngine { namespace UI { +namespace { + +bool IsKeyboardActivationKey(std::int32_t keyCode) { + return keyCode == static_cast(Input::KeyCode::Enter) || + keyCode == static_cast(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(); } diff --git a/engine/src/UI/Input/UIShortcutRegistry.cpp b/engine/src/UI/Input/UIShortcutRegistry.cpp index 96e97094..ac821513 100644 --- a/engine/src/UI/Input/UIShortcutRegistry.cpp +++ b/engine/src/UI/Input/UIShortcutRegistry.cpp @@ -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( diff --git a/engine/src/UI/Runtime/UIScreenDocumentHost.cpp b/engine/src/UI/Runtime/UIScreenDocumentHost.cpp index 7aef0312..38e80ebd 100644 --- a/engine/src/UI/Runtime/UIScreenDocumentHost.cpp +++ b/engine/src/UI/Runtime/UIScreenDocumentHost.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -22,6 +23,7 @@ namespace Runtime { namespace { using XCEngine::Math::Color; +using XCEngine::Input::KeyCode; using XCEngine::Resources::CompileUIDocument; using XCEngine::Resources::GetUIDocumentDefaultRootTag; using XCEngine::Resources::UIDocumentAttribute; @@ -54,6 +56,22 @@ struct RuntimeLayoutNode { bool focusable = false; bool wantsPointerCapture = false; bool isScrollView = false; + bool isSplitter = false; + bool textInput = false; + Layout::UILayoutAxis splitterAxis = Layout::UILayoutAxis::Horizontal; + Layout::UISplitterMetrics splitterMetrics = {}; + Layout::UISplitterConstraints splitterConstraints = {}; + float splitterRatio = 0.5f; + UIRect splitterHandleRect = {}; + UIRect splitterHandleHitRect = {}; + bool hasShortcutBinding = false; + UIShortcutBinding shortcutBinding = {}; + enum class ShortcutScopeRoot : std::uint8_t { + None = 0, + Window, + Panel, + Widget + } shortcutScopeRoot = ShortcutScopeRoot::None; }; struct RuntimeNodeVisualState { @@ -165,6 +183,232 @@ bool ParseBoolAttribute( return TryParseBoolString(GetAttribute(node, name), value) ? value : fallback; } +std::string TrimAscii(std::string value) { + std::size_t start = 0u; + while (start < value.size() && + std::isspace(static_cast(value[start]))) { + ++start; + } + + std::size_t end = value.size(); + while (end > start && + std::isspace(static_cast(value[end - 1u]))) { + --end; + } + + return value.substr(start, end - start); +} + +bool TryParseShortcutScope( + const std::string& text, + UIShortcutScope& outScope) { + const std::string normalized = ToLowerAscii(TrimAscii(text)); + if (normalized == "global") { + outScope = UIShortcutScope::Global; + return true; + } + if (normalized == "window") { + outScope = UIShortcutScope::Window; + return true; + } + if (normalized == "panel") { + outScope = UIShortcutScope::Panel; + return true; + } + if (normalized == "widget") { + outScope = UIShortcutScope::Widget; + return true; + } + + return false; +} + +RuntimeLayoutNode::ShortcutScopeRoot ParseShortcutScopeRoot(const UIDocumentNode& node) { + const std::string normalized = ToLowerAscii(GetAttribute(node, "shortcutScopeRoot")); + if (normalized == "window") { + return RuntimeLayoutNode::ShortcutScopeRoot::Window; + } + if (normalized == "panel") { + return RuntimeLayoutNode::ShortcutScopeRoot::Panel; + } + if (normalized == "widget") { + return RuntimeLayoutNode::ShortcutScopeRoot::Widget; + } + + return RuntimeLayoutNode::ShortcutScopeRoot::None; +} + +bool IsTextInputNode(const UIDocumentNode& node) { + return ParseBoolAttribute(node, "textInput", false) || + ParseBoolAttribute(node, "shortcutTextInput", false); +} + +bool TryParseShortcutKeyToken( + const std::string& token, + std::int32_t& outKeyCode) { + const std::string normalized = ToLowerAscii(TrimAscii(token)); + if (normalized.size() == 1u) { + const char ch = normalized.front(); + if (ch >= 'a' && ch <= 'z') { + outKeyCode = static_cast(KeyCode::A) + (ch - 'a'); + return true; + } + if (ch >= '0' && ch <= '9') { + outKeyCode = ch == '0' + ? static_cast(KeyCode::Zero) + : static_cast(KeyCode::One) + (ch - '1'); + return true; + } + } + + if (normalized == "space") { + outKeyCode = static_cast(KeyCode::Space); + return true; + } + if (normalized == "tab") { + outKeyCode = static_cast(KeyCode::Tab); + return true; + } + if (normalized == "enter" || normalized == "return") { + outKeyCode = static_cast(KeyCode::Enter); + return true; + } + if (normalized == "escape" || normalized == "esc") { + outKeyCode = static_cast(KeyCode::Escape); + return true; + } + if (normalized == "up") { + outKeyCode = static_cast(KeyCode::Up); + return true; + } + if (normalized == "down") { + outKeyCode = static_cast(KeyCode::Down); + return true; + } + if (normalized == "left") { + outKeyCode = static_cast(KeyCode::Left); + return true; + } + if (normalized == "right") { + outKeyCode = static_cast(KeyCode::Right); + return true; + } + if (normalized == "home") { + outKeyCode = static_cast(KeyCode::Home); + return true; + } + if (normalized == "end") { + outKeyCode = static_cast(KeyCode::End); + return true; + } + if (normalized == "pageup") { + outKeyCode = static_cast(KeyCode::PageUp); + return true; + } + if (normalized == "pagedown") { + outKeyCode = static_cast(KeyCode::PageDown); + return true; + } + if (normalized == "delete") { + outKeyCode = static_cast(KeyCode::Delete); + return true; + } + if (normalized == "backspace") { + outKeyCode = static_cast(KeyCode::Backspace); + return true; + } + + if (normalized.size() >= 2u && + normalized.front() == 'f' && + std::isdigit(static_cast(normalized[1u]))) { + const int functionIndex = std::atoi(normalized.c_str() + 1u); + if (functionIndex >= 1 && functionIndex <= 12) { + outKeyCode = static_cast(KeyCode::F1) + (functionIndex - 1); + return true; + } + } + + return false; +} + +bool TryParseShortcutChord( + const UIDocumentNode& node, + UIShortcutChord& outChord) { + const std::string expression = GetAttribute(node, "shortcut"); + if (expression.empty()) { + return false; + } + + UIShortcutChord chord = {}; + chord.allowRepeat = ParseBoolAttribute(node, "shortcutAllowRepeat", false); + + std::size_t segmentStart = 0u; + bool foundKey = false; + while (segmentStart <= expression.size()) { + const std::size_t separator = expression.find('+', segmentStart); + const std::string token = TrimAscii(expression.substr( + segmentStart, + separator == std::string::npos ? std::string::npos : separator - segmentStart)); + if (!token.empty()) { + const std::string normalized = ToLowerAscii(token); + if (normalized == "ctrl" || normalized == "control") { + chord.modifiers.control = true; + } else if (normalized == "shift") { + chord.modifiers.shift = true; + } else if (normalized == "alt") { + chord.modifiers.alt = true; + } else if (normalized == "super" || normalized == "win" || normalized == "cmd") { + chord.modifiers.super = true; + } else if (!foundKey && TryParseShortcutKeyToken(token, chord.keyCode)) { + foundKey = true; + } else { + return false; + } + } + + if (separator == std::string::npos) { + break; + } + segmentStart = separator + 1u; + } + + if (!foundKey || chord.keyCode == 0) { + return false; + } + + outChord = chord; + return true; +} + +bool TryBuildShortcutBinding( + const UIDocumentNode& node, + UIElementId ownerId, + UIShortcutBinding& outBinding) { + const std::string commandId = GetAttribute(node, "shortcutCommand"); + if (commandId.empty()) { + return false; + } + + UIShortcutChord chord = {}; + if (!TryParseShortcutChord(node, chord)) { + return false; + } + + UIShortcutScope scope = UIShortcutScope::Widget; + const std::string scopeText = GetAttribute(node, "shortcutScope"); + if (!scopeText.empty() && !TryParseShortcutScope(scopeText, scope)) { + return false; + } + + UIShortcutBinding binding = {}; + binding.scope = scope; + binding.ownerId = ownerId; + binding.chord = chord; + binding.commandId = commandId; + outBinding = binding; + return true; +} + float MeasureHeaderTextWidth(const UIDocumentNode& node) { float width = 0.0f; @@ -232,6 +476,18 @@ float ParseFloatAttribute( return TryParseFloat(GetAttribute(node, name), value) ? value : fallback; } +float ParseFloatAttributeAny( + const UIDocumentNode& node, + const char* primaryName, + const char* secondaryName, + float fallback) { + if (FindAttribute(node, primaryName) != nullptr) { + return ParseFloatAttribute(node, primaryName, fallback); + } + + return ParseFloatAttribute(node, secondaryName, fallback); +} + Layout::UILayoutLength ParseLengthAttribute( const UIDocumentNode& node, const char* name) { @@ -275,6 +531,38 @@ Layout::UILayoutItem BuildLayoutItem( return item; } +float GetSizeMainExtent(const UISize& size, Layout::UILayoutAxis axis) { + return axis == Layout::UILayoutAxis::Horizontal ? size.width : size.height; +} + +Layout::UISplitterConstraints BuildSplitterConstraints( + const UIDocumentNode& source, + Layout::UILayoutAxis axis, + const RuntimeLayoutNode& primaryChild, + const RuntimeLayoutNode& secondaryChild) { + Layout::UISplitterConstraints constraints = {}; + constraints.primaryMin = (std::max)( + GetSizeMainExtent(primaryChild.minimumSize, axis), + ParseFloatAttributeAny(source, "primaryMin", "minPrimary", 0.0f)); + constraints.secondaryMin = (std::max)( + GetSizeMainExtent(secondaryChild.minimumSize, axis), + ParseFloatAttributeAny(source, "secondaryMin", "minSecondary", 0.0f)); + + const float primaryMax = ParseFloatAttributeAny( + source, + "primaryMax", + "maxPrimary", + Layout::GetUnboundedLayoutExtent()); + const float secondaryMax = ParseFloatAttributeAny( + source, + "secondaryMax", + "maxSecondary", + Layout::GetUnboundedLayoutExtent()); + constraints.primaryMax = primaryMax > 0.0f ? primaryMax : Layout::GetUnboundedLayoutExtent(); + constraints.secondaryMax = secondaryMax > 0.0f ? secondaryMax : Layout::GetUnboundedLayoutExtent(); + return constraints; +} + std::string ResolveNodeText(const UIDocumentNode& node) { const std::string text = GetAttribute(node, "text"); if (!text.empty()) { @@ -297,10 +585,46 @@ bool IsScrollViewTag(const std::string& tagName) { return tagName == "ScrollView"; } +bool IsSplitterTag(const std::string& tagName) { + return tagName == "Splitter"; +} + bool IsButtonTag(const std::string& tagName) { return tagName == "Button"; } +Layout::UILayoutAxis ParseAxisAttribute( + const UIDocumentNode& node, + const char* name, + Layout::UILayoutAxis fallback) { + const std::string normalized = ToLowerAscii(GetAttribute(node, name)); + if (normalized == "horizontal" || normalized == "row" || normalized == "x") { + return Layout::UILayoutAxis::Horizontal; + } + if (normalized == "vertical" || normalized == "column" || normalized == "y") { + return Layout::UILayoutAxis::Vertical; + } + return fallback; +} + +float ParseRatioAttribute( + const UIDocumentNode& node, + const char* name, + float fallback) { + return Layout::SplitterDetail::ClampFiniteExtent(ParseFloatAttribute(node, name, fallback), 0.0f, 1.0f); +} + +Layout::UISplitterMetrics ParseSplitterMetrics(const UIDocumentNode& node) { + Layout::UISplitterMetrics metrics = {}; + metrics.thickness = (std::max)( + 2.0f, + ParseFloatAttributeAny(node, "splitterSize", "handleThickness", metrics.thickness)); + metrics.hitThickness = (std::max)( + metrics.thickness, + ParseFloatAttributeAny(node, "splitterHitSize", "hitThickness", metrics.hitThickness)); + return metrics; +} + bool IsContainerTag(const UIDocumentNode& node) { if (node.children.Size() > 0u) { return true; @@ -310,6 +634,7 @@ bool IsContainerTag(const UIDocumentNode& node) { return tagName == "View" || tagName == "Column" || tagName == "Row" || + tagName == "Splitter" || tagName == "ScrollView" || tagName == "Card" || tagName == "Button"; @@ -317,7 +642,10 @@ bool IsContainerTag(const UIDocumentNode& node) { bool IsPointerInteractiveNode(const UIDocumentNode& node) { const std::string tagName = ToStdString(node.tagName); - return ParseBoolAttribute(node, "interactive", IsButtonTag(tagName) || IsScrollViewTag(tagName)); + return ParseBoolAttribute( + node, + "interactive", + IsButtonTag(tagName) || IsScrollViewTag(tagName) || IsSplitterTag(tagName)); } bool IsFocusableNode(const UIDocumentNode& node) { @@ -381,11 +709,15 @@ UIRect IntersectRects(const UIRect& lhs, const UIRect& rhs) { } const UIRect& GetNodeInteractionRect(const RuntimeLayoutNode& node) { + if (node.isSplitter) { + return node.splitterHandleHitRect; + } + return node.isScrollView ? node.scrollViewportRect : node.rect; } bool IsNodeTargetable(const RuntimeLayoutNode& node) { - return node.pointerInteractive || node.focusable || node.isScrollView; + return node.pointerInteractive || node.focusable || node.isScrollView || node.isSplitter; } const RuntimeLayoutNode* FindNodeByElementId( @@ -420,12 +752,107 @@ RuntimeLayoutNode* FindNodeByElementId( return nullptr; } +bool CollectNodesForInputPath( + const RuntimeLayoutNode& node, + const UIInputPath& path, + std::size_t index, + std::vector& outNodes) { + if (index >= path.elements.size() || node.elementId != path.elements[index]) { + return false; + } + + outNodes.push_back(&node); + if (index + 1u == path.elements.size()) { + return true; + } + + for (const RuntimeLayoutNode& child : node.children) { + if (CollectNodesForInputPath(child, path, index + 1u, outNodes)) { + return true; + } + } + + outNodes.pop_back(); + return false; +} + +std::string ResolveStateKeyForElementId( + const RuntimeLayoutNode& root, + UIElementId elementId) { + if (elementId == 0u) { + return {}; + } + + if (const RuntimeLayoutNode* node = FindNodeByElementId(root, elementId); node != nullptr) { + return node->stateKey; + } + + return {}; +} + bool PathTargetExists( const RuntimeLayoutNode& root, const UIInputPath& path) { return !path.Empty() && FindNodeByElementId(root, path.Target()) != nullptr; } +bool SplitterTargetExists( + const RuntimeLayoutNode& root, + const UIDocumentScreenHost::SplitterDragRuntimeState& splitterDragState) { + if (!splitterDragState.drag.active || splitterDragState.drag.ownerId == 0u) { + return false; + } + + const RuntimeLayoutNode* node = FindNodeByElementId(root, splitterDragState.drag.ownerId); + return node != nullptr && + node->isSplitter && + node->children.size() == 2u; +} + +bool SyncSplitterDragStateFromCurrentLayout( + const RuntimeLayoutNode& root, + const std::unordered_map& splitterRatios, + UIDocumentScreenHost::SplitterDragRuntimeState& splitterDragState) { + if (!SplitterTargetExists(root, splitterDragState)) { + splitterDragState = {}; + return false; + } + + const RuntimeLayoutNode* node = FindNodeByElementId(root, splitterDragState.drag.ownerId); + if (node == nullptr || splitterDragState.stateKey != node->stateKey) { + splitterDragState = {}; + return false; + } + + splitterDragState.drag.axis = node->splitterAxis; + splitterDragState.drag.bounds = node->rect; + splitterDragState.drag.constraints = node->splitterConstraints; + splitterDragState.drag.metrics = node->splitterMetrics; + const auto foundRatio = splitterRatios.find(node->stateKey); + splitterDragState.drag.splitRatio = foundRatio != splitterRatios.end() + ? foundRatio->second + : node->splitterRatio; + return true; +} + +std::string ValidateRuntimeLayoutTree(const RuntimeLayoutNode& node) { + if (node.isSplitter && node.children.size() != 2u) { + const std::string splitterName = node.stateKey.empty() + ? std::string("") + : node.stateKey; + return "Splitter '" + splitterName + "' must contain exactly 2 child elements."; + } + + for (const RuntimeLayoutNode& child : node.children) { + const std::string error = ValidateRuntimeLayoutTree(child); + if (!error.empty()) { + return error; + } + } + + return {}; +} + std::string ResolveStateKeyForPathTarget( const RuntimeLayoutNode& root, const UIInputPath& path) { @@ -440,6 +867,146 @@ std::string ResolveStateKeyForPathTarget( return {}; } +struct RuntimeResolvedShortcutContext { + UIShortcutScopeChain commandScope = {}; + bool textInputActive = false; +}; + +RuntimeResolvedShortcutContext ResolveShortcutContext( + const RuntimeLayoutNode& root, + const UIInputPath& path) { + RuntimeResolvedShortcutContext resolved = {}; + resolved.commandScope.path = path; + if (path.Empty()) { + return resolved; + } + + std::vector nodes = {}; + if (!CollectNodesForInputPath(root, path, 0u, nodes)) { + return resolved; + } + + for (const RuntimeLayoutNode* node : nodes) { + if (node == nullptr) { + continue; + } + + switch (node->shortcutScopeRoot) { + case RuntimeLayoutNode::ShortcutScopeRoot::Window: + resolved.commandScope.windowId = node->elementId; + break; + case RuntimeLayoutNode::ShortcutScopeRoot::Panel: + resolved.commandScope.panelId = node->elementId; + break; + case RuntimeLayoutNode::ShortcutScopeRoot::Widget: + resolved.commandScope.widgetId = node->elementId; + break; + case RuntimeLayoutNode::ShortcutScopeRoot::None: + default: + break; + } + + resolved.textInputActive = resolved.textInputActive || node->textInput; + } + + if (resolved.commandScope.widgetId == 0u && !path.Empty()) { + resolved.commandScope.widgetId = path.Target(); + } + + return resolved; +} + +void RegisterShortcutBindings( + const RuntimeLayoutNode& node, + UIShortcutRegistry& registry) { + if (node.hasShortcutBinding) { + registry.RegisterBinding(node.shortcutBinding); + } + + for (const RuntimeLayoutNode& child : node.children) { + RegisterShortcutBindings(child, registry); + } +} + +const char* GetShortcutScopeDebugName(UIShortcutScope scope) { + switch (scope) { + case UIShortcutScope::Window: + return "Window"; + case UIShortcutScope::Panel: + return "Panel"; + case UIShortcutScope::Widget: + return "Widget"; + case UIShortcutScope::Global: + default: + return "Global"; + } +} + +void SyncShortcutContext( + const RuntimeLayoutNode& root, + UIInputDispatcher& inputDispatcher) { + const RuntimeResolvedShortcutContext resolved = ResolveShortcutContext( + root, + inputDispatcher.GetFocusController().GetFocusedPath()); + + UIShortcutContext shortcutContext = {}; + shortcutContext.focusedPath = inputDispatcher.GetFocusController().GetFocusedPath(); + shortcutContext.activePath = inputDispatcher.GetFocusController().GetActivePath(); + shortcutContext.commandScope = resolved.commandScope; + shortcutContext.textInputActive = resolved.textInputActive; + inputDispatcher.SetShortcutContext(shortcutContext); +} + +void CollectFocusablePaths( + const RuntimeLayoutNode& node, + std::vector& outPaths) { + if (node.focusable) { + outPaths.push_back(node.inputPath); + } + + for (const RuntimeLayoutNode& child : node.children) { + CollectFocusablePaths(child, outPaths); + } +} + +bool TryAdvanceFocusByTab( + const RuntimeLayoutNode& root, + UIFocusController& focusController, + bool reverse) { + std::vector focusablePaths = {}; + CollectFocusablePaths(root, focusablePaths); + if (focusablePaths.empty()) { + return false; + } + + std::size_t nextIndex = reverse ? focusablePaths.size() - 1u : 0u; + const UIInputPath& currentPath = focusController.GetFocusedPath(); + for (std::size_t index = 0; index < focusablePaths.size(); ++index) { + if (focusablePaths[index] != currentPath) { + continue; + } + + if (reverse) { + nextIndex = index == 0u ? focusablePaths.size() - 1u : index - 1u; + } else { + nextIndex = (index + 1u) % focusablePaths.size(); + } + break; + } + + focusController.SetFocusedPath(focusablePaths[nextIndex]); + focusController.ClearActivePath(); + return true; +} + +bool IsFocusTraversalEvent(const UIInputEvent& event) { + return event.type == UIInputEventType::KeyDown && + event.keyCode == static_cast(KeyCode::Tab) && + !event.modifiers.control && + !event.modifiers.alt && + !event.modifiers.super; +} + const RuntimeLayoutNode* FindDeepestInputTarget( const RuntimeLayoutNode& node, const UIPoint& point, @@ -458,13 +1025,23 @@ const RuntimeLayoutNode* FindDeepestInputTarget( : node.scrollViewportRect; } + const UIRect& targetRect = GetNodeInteractionRect(node); + const bool splitterHitPriority = + node.isSplitter && + IsNodeTargetable(node) && + HasPositiveArea(targetRect) && + RectContainsPoint(targetRect, point) && + (clipRect == nullptr || RectContainsPoint(*clipRect, point)); + if (splitterHitPriority) { + return &node; + } + for (const RuntimeLayoutNode& child : node.children) { if (const RuntimeLayoutNode* found = FindDeepestInputTarget(child, point, &nextClip); found != nullptr) { return found; } } - const UIRect& targetRect = GetNodeInteractionRect(node); if (!IsNodeTargetable(node) || !HasPositiveArea(targetRect)) { return nullptr; } @@ -583,6 +1160,16 @@ RuntimeLayoutNode BuildLayoutTree( node.focusable = ParseBoolAttribute(source, "focusable", IsFocusableNode(source)); node.wantsPointerCapture = WantsPointerCapture(source); node.isScrollView = IsScrollViewTag(ToStdString(source.tagName)); + node.isSplitter = IsSplitterTag(ToStdString(source.tagName)); + node.textInput = IsTextInputNode(source); + node.splitterAxis = ParseAxisAttribute(source, "axis", Layout::UILayoutAxis::Horizontal); + node.splitterMetrics = ParseSplitterMetrics(source); + node.splitterRatio = ParseRatioAttribute( + source, + "splitRatio", + ParseRatioAttribute(source, "ratio", 0.5f)); + node.shortcutScopeRoot = ParseShortcutScopeRoot(source); + node.hasShortcutBinding = TryBuildShortcutBinding(source, node.elementId, node.shortcutBinding); node.children.reserve(source.children.Size()); for (std::size_t index = 0; index < source.children.Size(); ++index) { node.children.push_back(BuildLayoutTree(source.children[index], node.stateKey, node.inputPath, index)); @@ -611,6 +1198,68 @@ UISize MeasureNode(RuntimeLayoutNode& node) { return node.desiredSize; } + if (node.isSplitter && node.children.size() == 2u) { + RuntimeLayoutNode& primaryChild = node.children[0]; + RuntimeLayoutNode& secondaryChild = node.children[1]; + MeasureNode(primaryChild); + MeasureNode(secondaryChild); + + node.splitterConstraints = BuildSplitterConstraints( + source, + node.splitterAxis, + primaryChild, + secondaryChild); + + const float primaryDesiredMain = GetSizeMainExtent(primaryChild.desiredSize, node.splitterAxis); + const float secondaryDesiredMain = GetSizeMainExtent(secondaryChild.desiredSize, node.splitterAxis); + const float primaryMinMain = GetSizeMainExtent(primaryChild.minimumSize, node.splitterAxis); + const float secondaryMinMain = GetSizeMainExtent(secondaryChild.minimumSize, node.splitterAxis); + const float crossDesired = (std::max)( + GetSizeMainExtent(primaryChild.desiredSize, node.splitterAxis == Layout::UILayoutAxis::Horizontal + ? Layout::UILayoutAxis::Vertical + : Layout::UILayoutAxis::Horizontal), + GetSizeMainExtent(secondaryChild.desiredSize, node.splitterAxis == Layout::UILayoutAxis::Horizontal + ? Layout::UILayoutAxis::Vertical + : Layout::UILayoutAxis::Horizontal)); + const float crossMin = (std::max)( + GetSizeMainExtent(primaryChild.minimumSize, node.splitterAxis == Layout::UILayoutAxis::Horizontal + ? Layout::UILayoutAxis::Vertical + : Layout::UILayoutAxis::Horizontal), + GetSizeMainExtent(secondaryChild.minimumSize, node.splitterAxis == Layout::UILayoutAxis::Horizontal + ? Layout::UILayoutAxis::Vertical + : Layout::UILayoutAxis::Horizontal)); + + if (node.splitterAxis == Layout::UILayoutAxis::Horizontal) { + node.desiredSize = UISize( + primaryDesiredMain + secondaryDesiredMain + node.splitterMetrics.thickness, + crossDesired); + node.minimumSize = UISize( + primaryMinMain + secondaryMinMain + node.splitterMetrics.thickness, + crossMin); + } else { + node.desiredSize = UISize( + crossDesired, + primaryDesiredMain + secondaryDesiredMain + node.splitterMetrics.thickness); + node.minimumSize = UISize( + crossMin, + primaryMinMain + secondaryMinMain + node.splitterMetrics.thickness); + } + + node.contentDesiredSize = node.desiredSize; + + float explicitWidth = 0.0f; + if (TryParseFloat(GetAttribute(source, "width"), explicitWidth)) { + node.desiredSize.width = (std::max)(node.desiredSize.width, explicitWidth); + } + + float explicitHeight = 0.0f; + if (TryParseFloat(GetAttribute(source, "height"), explicitHeight)) { + node.desiredSize.height = (std::max)(node.desiredSize.height, explicitHeight); + } + + return node.desiredSize; + } + Layout::UIStackLayoutOptions options = {}; options.axis = IsHorizontalTag(tagName) ? Layout::UILayoutAxis::Horizontal @@ -675,16 +1324,49 @@ UISize MeasureNode(RuntimeLayoutNode& node) { void ArrangeNode( RuntimeLayoutNode& node, const UIRect& rect, - const std::unordered_map& verticalScrollOffsets) { + const std::unordered_map& verticalScrollOffsets, + const std::unordered_map& splitterRatios) { node.rect = rect; node.scrollViewportRect = {}; node.scrollOffsetY = 0.0f; + node.splitterHandleRect = {}; + node.splitterHandleHitRect = {}; const UIDocumentNode& source = *node.source; if (!IsContainerTag(source)) { return; } + if (node.isSplitter && node.children.size() == 2u) { + const auto foundRatio = splitterRatios.find(node.stateKey); + const float requestedRatio = foundRatio != splitterRatios.end() + ? foundRatio->second + : node.splitterRatio; + const Layout::UISplitterLayoutResult arranged = Layout::ArrangeUISplitter( + rect, + node.splitterAxis, + requestedRatio, + node.splitterConstraints, + node.splitterMetrics); + node.splitterRatio = arranged.splitRatio; + node.splitterHandleRect = arranged.handleRect; + node.splitterHandleHitRect = Widgets::ExpandUISplitterHandleHitRect( + arranged.handleRect, + node.splitterAxis, + (std::max)(0.0f, (node.splitterMetrics.hitThickness - node.splitterMetrics.thickness) * 0.5f)); + ArrangeNode( + node.children[0], + arranged.primaryRect, + verticalScrollOffsets, + splitterRatios); + ArrangeNode( + node.children[1], + arranged.secondaryRect, + verticalScrollOffsets, + splitterRatios); + return; + } + const std::string tagName = ToStdString(source.tagName); Layout::UIStackLayoutOptions options = {}; options.axis = IsHorizontalTag(tagName) @@ -727,7 +1409,8 @@ void ArrangeNode( ArrangeNode( node.children[index], arranged.children[index].arrangedRect, - verticalScrollOffsets); + verticalScrollOffsets, + splitterRatios); } return; } @@ -737,7 +1420,8 @@ void ArrangeNode( ArrangeNode( node.children[index], arranged.children[index].arrangedRect, - verticalScrollOffsets); + verticalScrollOffsets, + splitterRatios); } } @@ -946,6 +1630,13 @@ void SanitizeInputDispatcherState( } } +void SanitizeSplitterDragState( + const RuntimeLayoutNode& root, + const std::unordered_map& splitterRatios, + UIDocumentScreenHost::SplitterDragRuntimeState& splitterDragState) { + SyncSplitterDragStateFromCurrentLayout(root, splitterRatios, splitterDragState); +} + void UpdateInputDebugSnapshot( const RuntimeLayoutNode& root, const UIInputPath& hoveredPath, @@ -965,16 +1656,32 @@ void UpdateInputDebugSnapshot( inputDebugSnapshot.captureStateKey = ResolveStateKeyForPathTarget( root, inputDispatcher.GetFocusController().GetPointerCapturePath()); + const UIShortcutContext& shortcutContext = inputDispatcher.GetShortcutContext(); + inputDebugSnapshot.textInputActive = shortcutContext.textInputActive; + inputDebugSnapshot.commandScopeStateKey = ResolveStateKeyForPathTarget( + root, + shortcutContext.commandScope.path); + inputDebugSnapshot.windowScopeStateKey = ResolveStateKeyForElementId( + root, + shortcutContext.commandScope.windowId); + inputDebugSnapshot.panelScopeStateKey = ResolveStateKeyForElementId( + root, + shortcutContext.commandScope.panelId); + inputDebugSnapshot.widgetScopeStateKey = ResolveStateKeyForElementId( + root, + shortcutContext.commandScope.widgetId); } -void DispatchInputEvent( +bool DispatchInputEvent( RuntimeLayoutNode& root, const UIInputEvent& event, const UIInputPath& hoveredPath, UIInputDispatcher& inputDispatcher, + std::unordered_map& splitterRatios, + UIDocumentScreenHost::SplitterDragRuntimeState& splitterDragState, UIDocumentScreenHost::InputDebugSnapshot& inputDebugSnapshot) { if (event.type == UIInputEventType::PointerWheel) { - return; + return false; } if (UIInputRouter::IsPointerEvent(event.type)) { @@ -984,6 +1691,11 @@ void DispatchInputEvent( inputDebugSnapshot.lastEventType = GetInputEventTypeDebugName(event.type); inputDebugSnapshot.lastTargetKind = "None"; inputDebugSnapshot.lastTargetStateKey.clear(); + inputDebugSnapshot.lastShortcutCommandId.clear(); + inputDebugSnapshot.lastShortcutScope.clear(); + inputDebugSnapshot.lastShortcutOwnerStateKey.clear(); + inputDebugSnapshot.lastShortcutHandled = false; + inputDebugSnapshot.lastShortcutSuppressed = false; inputDebugSnapshot.lastResult = "No target"; if (event.type == UIInputEventType::FocusLost) { @@ -991,11 +1703,31 @@ void DispatchInputEvent( focusController.ClearPointerCapturePath(); focusController.ClearActivePath(); focusController.ClearFocus(); + splitterDragState = {}; inputDebugSnapshot.lastResult = "Focus cleared"; - return; + return false; + } + + const bool focusTraversalSuppressed = + IsFocusTraversalEvent(event) && + inputDispatcher.GetShortcutContext().textInputActive; + if (IsFocusTraversalEvent(event) && !focusTraversalSuppressed) { + UIFocusController& focusController = inputDispatcher.GetFocusController(); + if (TryAdvanceFocusByTab(root, focusController, event.modifiers.shift)) { + inputDebugSnapshot.lastTargetKind = "Focused"; + inputDebugSnapshot.lastTargetStateKey = ResolveStateKeyForPathTarget( + root, + focusController.GetFocusedPath()); + inputDebugSnapshot.lastResult = "Focus traversed"; + } else { + inputDebugSnapshot.lastResult = "No focusable target"; + } + return false; } bool pointerCaptureStarted = false; + bool layoutChanged = false; + bool splitterDragFinished = false; const UIInputDispatchSummary summary = inputDispatcher.Dispatch( event, hoveredPath, @@ -1009,6 +1741,76 @@ void DispatchInputEvent( return UIInputDispatchDecision{}; } + if (node->isSplitter && + splitterDragState.drag.active && + splitterDragState.drag.ownerId == node->elementId && + !SyncSplitterDragStateFromCurrentLayout( + root, + splitterRatios, + splitterDragState)) { + return UIInputDispatchDecision{}; + } + + if (event.type == UIInputEventType::PointerButtonDown && + event.pointerButton == UIPointerButton::Left && + node->isSplitter && + Widgets::BeginUISplitterDrag( + node->elementId, + node->splitterAxis, + node->rect, + Layout::ArrangeUISplitter( + node->rect, + node->splitterAxis, + splitterRatios.count(node->stateKey) > 0u + ? splitterRatios[node->stateKey] + : node->splitterRatio, + node->splitterConstraints, + node->splitterMetrics), + node->splitterConstraints, + node->splitterMetrics, + event.position, + splitterDragState.drag)) { + splitterDragState.stateKey = node->stateKey; + inputDispatcher.GetFocusController().SetPointerCapturePath(node->inputPath); + pointerCaptureStarted = true; + return UIInputDispatchDecision{ true, false }; + } + + if (node->isSplitter && + splitterDragState.drag.active && + splitterDragState.drag.ownerId == node->elementId && + event.type == UIInputEventType::PointerMove) { + Layout::UISplitterLayoutResult draggedLayout = {}; + if (Widgets::UpdateUISplitterDrag( + splitterDragState.drag, + event.position, + draggedLayout)) { + splitterRatios[splitterDragState.stateKey] = draggedLayout.splitRatio; + layoutChanged = true; + return UIInputDispatchDecision{ true, false }; + } + } + + if (node->isSplitter && + splitterDragState.drag.active && + splitterDragState.drag.ownerId == node->elementId && + event.type == UIInputEventType::PointerButtonUp && + event.pointerButton == UIPointerButton::Left) { + Layout::UISplitterLayoutResult draggedLayout = {}; + if (Widgets::UpdateUISplitterDrag( + splitterDragState.drag, + event.position, + draggedLayout)) { + splitterRatios[splitterDragState.stateKey] = draggedLayout.splitRatio; + layoutChanged = true; + } + inputDispatcher.GetFocusController().ClearPointerCapturePath(); + Widgets::EndUISplitterDrag(splitterDragState.drag); + splitterDragState.stateKey.clear(); + splitterDragFinished = true; + return UIInputDispatchDecision{ true, false }; + } + if (event.type == UIInputEventType::PointerButtonDown && event.pointerButton == UIPointerButton::Left && node->wantsPointerCapture) { @@ -1023,17 +1825,49 @@ void DispatchInputEvent( inputDebugSnapshot.lastTargetKind = GetInputTargetKindDebugName(summary.routing.plan.targetKind); inputDebugSnapshot.lastTargetStateKey = ResolveStateKeyForPathTarget(root, summary.routing.plan.targetPath); inputDebugSnapshot.lastResult = summary.routing.plan.HasTargetPath() ? "Dispatched" : "No target"; + if (summary.shortcutMatched) { + inputDebugSnapshot.lastShortcutCommandId = summary.commandId; + inputDebugSnapshot.lastShortcutScope = GetShortcutScopeDebugName(summary.shortcutScope); + inputDebugSnapshot.lastShortcutOwnerStateKey = ResolveStateKeyForElementId( + root, + summary.shortcutOwnerId); + inputDebugSnapshot.lastShortcutHandled = summary.shortcutHandled; + inputDebugSnapshot.lastShortcutSuppressed = summary.shortcutSuppressed; + inputDebugSnapshot.recentShortcutCommandId = summary.commandId; + inputDebugSnapshot.recentShortcutScope = inputDebugSnapshot.lastShortcutScope; + inputDebugSnapshot.recentShortcutOwnerStateKey = inputDebugSnapshot.lastShortcutOwnerStateKey; + inputDebugSnapshot.recentShortcutHandled = summary.shortcutHandled; + inputDebugSnapshot.recentShortcutSuppressed = summary.shortcutSuppressed; + } - if (pointerCaptureStarted) { - inputDebugSnapshot.lastResult = "Pointer capture started"; + if (summary.shortcutHandled) { + inputDebugSnapshot.lastResult = "Shortcut handled"; + } else if (summary.shortcutSuppressed) { + inputDebugSnapshot.lastResult = "Shortcut suppressed by text input"; + } else if (focusTraversalSuppressed) { + inputDebugSnapshot.lastResult = "Focus traversal suppressed by text input"; + } else if (pointerCaptureStarted) { + inputDebugSnapshot.lastResult = splitterDragState.drag.active + ? "Splitter drag started" + : "Pointer capture started"; + } else if (splitterDragFinished) { + inputDebugSnapshot.lastResult = "Splitter drag finished"; } else if (event.type == UIInputEventType::PointerButtonUp && event.pointerButton == UIPointerButton::Left && inputDispatcher.GetFocusController().HasPointerCapture()) { inputDispatcher.GetFocusController().ClearPointerCapturePath(); inputDebugSnapshot.lastResult = "Pointer capture cleared"; } else if (summary.routing.handled) { - inputDebugSnapshot.lastResult = "Handled"; + inputDebugSnapshot.lastResult = layoutChanged ? "Splitter resized" : "Handled"; } + + if (event.type == UIInputEventType::PointerButtonUp && + event.pointerButton == UIPointerButton::Left && + !splitterDragState.drag.active) { + splitterDragState.stateKey.clear(); + } + + return layoutChanged; } void SyncScrollOffsets( @@ -1053,6 +1887,18 @@ void SyncScrollOffsets( } } +void SyncSplitterRatios( + const RuntimeLayoutNode& node, + std::unordered_map& splitterRatios) { + if (node.isSplitter) { + splitterRatios[node.stateKey] = node.splitterRatio; + } + + for (const RuntimeLayoutNode& child : node.children) { + SyncSplitterRatios(child, splitterRatios); + } +} + void EmitNode( const RuntimeLayoutNode& node, const UIInputPath& hoveredPath, @@ -1081,6 +1927,27 @@ void EmitNode( } } + if (node.isSplitter && HasPositiveArea(node.splitterHandleRect)) { + const Color splitterColor = + visualState.capture || visualState.active + ? Color(0.72f, 0.72f, 0.72f, 1.0f) + : (visualState.hovered + ? Color(0.56f, 0.56f, 0.56f, 1.0f) + : Color(0.38f, 0.38f, 0.38f, 1.0f)); + drawList.AddFilledRect(node.splitterHandleRect, ToUIColor(splitterColor), 4.0f); + ++stats.filledRectCommandCount; + } + + if (node.isSplitter && node.splitterHandleRect.width > 0.0f && node.splitterHandleRect.height > 0.0f) { + const Color handleColor = visualState.capture || visualState.active + ? Color(0.72f, 0.72f, 0.72f, 1.0f) + : (visualState.hovered + ? Color(0.56f, 0.56f, 0.56f, 1.0f) + : Color(0.36f, 0.36f, 0.36f, 1.0f)); + drawList.AddFilledRect(node.splitterHandleRect, ToUIColor(handleColor), 6.0f); + ++stats.filledRectCommandCount; + } + const std::string title = GetAttribute(source, "title"); const std::string subtitle = GetAttribute(source, "subtitle"); float textY = node.rect.y + kHeaderTextInset; @@ -1253,6 +2120,10 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( ? document.displayName : document.sourcePath; RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode, stateRoot, UIInputPath(), 0u); + result.errorMessage = ValidateRuntimeLayoutTree(root); + if (!result.errorMessage.empty()) { + return result; + } MeasureNode(root); UIRect viewportRect = input.viewportRect; @@ -1267,8 +2138,12 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( m_pointerState.insideViewport = false; } - ArrangeNode(root, viewportRect, m_verticalScrollOffsets); + ArrangeNode(root, viewportRect, m_verticalScrollOffsets, m_splitterRatios); SanitizeInputDispatcherState(root, m_inputDispatcher); + SanitizeSplitterDragState(root, m_splitterRatios, m_splitterDragState); + m_inputDispatcher.GetShortcutRegistry().Clear(); + RegisterShortcutBindings(root, m_inputDispatcher.GetShortcutRegistry()); + SyncShortcutContext(root, m_inputDispatcher); UIPoint pointerPosition = m_pointerState.position; bool hasPointerPosition = m_pointerState.hasPosition; @@ -1281,6 +2156,7 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( UIFocusController& focusController = m_inputDispatcher.GetFocusController(); focusController.ClearPointerCapturePath(); focusController.ClearActivePath(); + m_splitterDragState = {}; } for (const UIInputEvent& event : input.events) { @@ -1297,7 +2173,8 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( m_inputDebugSnapshot.lastTargetKind = "Hovered"; m_inputDebugSnapshot.lastResult = "No hovered ScrollView"; if (ApplyScrollWheelEvent(root, event, m_verticalScrollOffsets, m_scrollDebugSnapshot)) { - ArrangeNode(root, viewportRect, m_verticalScrollOffsets); + ArrangeNode(root, viewportRect, m_verticalScrollOffsets, m_splitterRatios); + SanitizeSplitterDragState(root, m_splitterRatios, m_splitterDragState); } continue; } @@ -1307,7 +2184,18 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( pointerPosition, hasPointerPosition, pointerInsideViewport); - DispatchInputEvent(root, event, eventHoveredPath, m_inputDispatcher, m_inputDebugSnapshot); + SyncShortcutContext(root, m_inputDispatcher); + if (DispatchInputEvent( + root, + event, + eventHoveredPath, + m_inputDispatcher, + m_splitterRatios, + m_splitterDragState, + m_inputDebugSnapshot)) { + ArrangeNode(root, viewportRect, m_verticalScrollOffsets, m_splitterRatios); + SanitizeSplitterDragState(root, m_splitterRatios, m_splitterDragState); + } } const UIInputPath hoveredPath = ResolveHoveredPath( @@ -1316,6 +2204,8 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( hasPointerPosition, pointerInsideViewport); SanitizeInputDispatcherState(root, m_inputDispatcher); + SanitizeSplitterDragState(root, m_splitterRatios, m_splitterDragState); + SyncShortcutContext(root, m_inputDispatcher); m_pointerState.position = pointerPosition; m_pointerState.hasPosition = hasPointerPosition; m_pointerState.insideViewport = pointerInsideViewport; @@ -1327,6 +2217,8 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( pointerInsideViewport, m_inputDebugSnapshot); SyncScrollOffsets(root, m_verticalScrollOffsets); + m_splitterRatios.clear(); + SyncSplitterRatios(root, m_splitterRatios); const UIFocusController& focusController = m_inputDispatcher.GetFocusController(); diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 34c26927..2818a581 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -9,15 +9,16 @@ file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCNEWEDITOR_REPO_ROOT_PATH) set(NEW_EDITOR_RESOURCE_FILES ui/views/editor_shell.xcui ui/themes/editor_shell.xctheme - ui/schemas/editor_inspector_shell.xcschema ) add_library(XCNewEditorLib STATIC - src/SandboxFrameBuilder.cpp + src/editor/EditorShellAsset.cpp + src/Widgets/UIEditorCollectionPrimitives.cpp ) target_include_directories(XCNewEditorLib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/engine/include ) @@ -25,6 +26,7 @@ target_include_directories(XCNewEditorLib target_compile_definitions(XCNewEditorLib PUBLIC UNICODE _UNICODE + XCNEWEDITOR_REPO_ROOT="${XCNEWEDITOR_REPO_ROOT_PATH}" ) if(MSVC) @@ -37,41 +39,70 @@ target_link_libraries(XCNewEditorLib PUBLIC XCEngine ) -add_executable(XCNewEditorApp WIN32 - src/main.cpp - src/Application.cpp - src/AutoScreenshot.cpp - src/NativeRenderer.cpp - ${NEW_EDITOR_RESOURCE_FILES} +add_library(XCNewEditorHost STATIC + src/Host/AutoScreenshot.cpp + src/Host/NativeRenderer.cpp ) -target_include_directories(XCNewEditorApp PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/engine/include +target_include_directories(XCNewEditorHost + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/engine/include ) -target_compile_definitions(XCNewEditorApp PRIVATE +target_compile_definitions(XCNewEditorHost PUBLIC UNICODE _UNICODE - XCNEWEDITOR_REPO_ROOT="${XCNEWEDITOR_REPO_ROOT_PATH}" ) if(MSVC) - target_compile_options(XCNewEditorApp PRIVATE /utf-8 /FS) - set_property(TARGET XCNewEditorApp PROPERTY + target_compile_options(XCNewEditorHost PRIVATE /utf-8 /FS) + set_property(TARGET XCNewEditorHost PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") endif() -target_link_libraries(XCNewEditorApp PRIVATE - XCNewEditorLib +target_link_libraries(XCNewEditorHost PUBLIC + XCEngine d2d1.lib dwrite.lib windowscodecs.lib ) -set_target_properties(XCNewEditorApp PROPERTIES - OUTPUT_NAME "XCNewEditor" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin" -) +if(XCENGINE_BUILD_NEW_EDITOR) + add_executable(XCNewEditorApp WIN32 + src/main.cpp + src/Host/Application.cpp + ${NEW_EDITOR_RESOURCE_FILES} + ) + + target_include_directories(XCNewEditorApp PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/engine/include + ) + + target_compile_definitions(XCNewEditorApp PRIVATE + UNICODE + _UNICODE + XCNEWEDITOR_REPO_ROOT="${XCNEWEDITOR_REPO_ROOT_PATH}" + ) + + if(MSVC) + target_compile_options(XCNewEditorApp PRIVATE /utf-8 /FS) + set_property(TARGET XCNewEditorApp PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + endif() + + target_link_libraries(XCNewEditorApp PRIVATE + XCNewEditorLib + XCNewEditorHost + ) + + set_target_properties(XCNewEditorApp PROPERTIES + OUTPUT_NAME "XCNewEditor" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin" + ) +endif() source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${NEW_EDITOR_RESOURCE_FILES}) diff --git a/new_editor/src/AutoScreenshot.h b/new_editor/include/XCNewEditor/Host/AutoScreenshot.h similarity index 93% rename from new_editor/src/AutoScreenshot.h rename to new_editor/include/XCNewEditor/Host/AutoScreenshot.h index 8d69fdda..3e3dc282 100644 --- a/new_editor/src/AutoScreenshot.h +++ b/new_editor/include/XCNewEditor/Host/AutoScreenshot.h @@ -11,8 +11,7 @@ #include #include -namespace XCEngine { -namespace NewEditor { +namespace XCEngine::XCUI::Host { class NativeRenderer; @@ -50,5 +49,4 @@ private: bool m_capturePending = false; }; -} // namespace NewEditor -} // namespace XCEngine +} // namespace XCEngine::XCUI::Host diff --git a/new_editor/include/XCNewEditor/Host/InputModifierTracker.h b/new_editor/include/XCNewEditor/Host/InputModifierTracker.h new file mode 100644 index 00000000..3a174cba --- /dev/null +++ b/new_editor/include/XCNewEditor/Host/InputModifierTracker.h @@ -0,0 +1,173 @@ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include + +#include + +#include +#include + +namespace XCEngine::XCUI::Host { + +class InputModifierTracker { +public: + void Reset() { + m_leftShift = false; + m_rightShift = false; + m_leftControl = false; + m_rightControl = false; + m_leftAlt = false; + m_rightAlt = false; + m_leftSuper = false; + m_rightSuper = false; + } + + void SyncFromSystemState() { + m_leftShift = (GetKeyState(VK_LSHIFT) & 0x8000) != 0; + m_rightShift = (GetKeyState(VK_RSHIFT) & 0x8000) != 0; + m_leftControl = (GetKeyState(VK_LCONTROL) & 0x8000) != 0; + m_rightControl = (GetKeyState(VK_RCONTROL) & 0x8000) != 0; + m_leftAlt = (GetKeyState(VK_LMENU) & 0x8000) != 0; + m_rightAlt = (GetKeyState(VK_RMENU) & 0x8000) != 0; + m_leftSuper = (GetKeyState(VK_LWIN) & 0x8000) != 0; + m_rightSuper = (GetKeyState(VK_RWIN) & 0x8000) != 0; + } + + ::XCEngine::UI::UIInputModifiers GetCurrentModifiers() const { + return BuildModifiers(); + } + + ::XCEngine::UI::UIInputModifiers BuildPointerModifiers(std::size_t wParam) const { + ::XCEngine::UI::UIInputModifiers modifiers = BuildModifiers(); + modifiers.shift = modifiers.shift || (wParam & MK_SHIFT) != 0; + modifiers.control = modifiers.control || (wParam & MK_CONTROL) != 0; + return modifiers; + } + + ::XCEngine::UI::UIInputModifiers ApplyKeyMessage( + ::XCEngine::UI::UIInputEventType type, + WPARAM wParam, + LPARAM lParam) { + if (type == ::XCEngine::UI::UIInputEventType::KeyDown) { + SetModifierState(ResolveModifierKey(wParam, lParam), true); + } else if (type == ::XCEngine::UI::UIInputEventType::KeyUp) { + SetModifierState(ResolveModifierKey(wParam, lParam), false); + } + + return BuildModifiers(); + } + +private: + enum class ModifierKey : std::uint8_t { + None = 0, + LeftShift, + RightShift, + LeftControl, + RightControl, + LeftAlt, + RightAlt, + LeftSuper, + RightSuper + }; + + static bool IsExtendedKey(LPARAM lParam) { + return (static_cast(lParam) & 0x01000000u) != 0u; + } + + static std::uint32_t ExtractScanCode(LPARAM lParam) { + return (static_cast(lParam) >> 16u) & 0xffu; + } + + static ModifierKey ResolveModifierKey(WPARAM wParam, LPARAM lParam) { + switch (static_cast(wParam)) { + case VK_SHIFT: { + const UINT shiftVirtualKey = MapVirtualKeyW(ExtractScanCode(lParam), MAPVK_VSC_TO_VK_EX); + return shiftVirtualKey == VK_RSHIFT + ? ModifierKey::RightShift + : ModifierKey::LeftShift; + } + case VK_LSHIFT: + return ModifierKey::LeftShift; + case VK_RSHIFT: + return ModifierKey::RightShift; + case VK_CONTROL: + return IsExtendedKey(lParam) + ? ModifierKey::RightControl + : ModifierKey::LeftControl; + case VK_LCONTROL: + return ModifierKey::LeftControl; + case VK_RCONTROL: + return ModifierKey::RightControl; + case VK_MENU: + return IsExtendedKey(lParam) + ? ModifierKey::RightAlt + : ModifierKey::LeftAlt; + case VK_LMENU: + return ModifierKey::LeftAlt; + case VK_RMENU: + return ModifierKey::RightAlt; + case VK_LWIN: + return ModifierKey::LeftSuper; + case VK_RWIN: + return ModifierKey::RightSuper; + default: + return ModifierKey::None; + } + } + + void SetModifierState(ModifierKey key, bool pressed) { + switch (key) { + case ModifierKey::LeftShift: + m_leftShift = pressed; + break; + case ModifierKey::RightShift: + m_rightShift = pressed; + break; + case ModifierKey::LeftControl: + m_leftControl = pressed; + break; + case ModifierKey::RightControl: + m_rightControl = pressed; + break; + case ModifierKey::LeftAlt: + m_leftAlt = pressed; + break; + case ModifierKey::RightAlt: + m_rightAlt = pressed; + break; + case ModifierKey::LeftSuper: + m_leftSuper = pressed; + break; + case ModifierKey::RightSuper: + m_rightSuper = pressed; + break; + case ModifierKey::None: + default: + break; + } + } + + ::XCEngine::UI::UIInputModifiers BuildModifiers() const { + ::XCEngine::UI::UIInputModifiers modifiers = {}; + modifiers.shift = m_leftShift || m_rightShift; + modifiers.control = m_leftControl || m_rightControl; + modifiers.alt = m_leftAlt || m_rightAlt; + modifiers.super = m_leftSuper || m_rightSuper; + return modifiers; + } + + bool m_leftShift = false; + bool m_rightShift = false; + bool m_leftControl = false; + bool m_rightControl = false; + bool m_leftAlt = false; + bool m_rightAlt = false; + bool m_leftSuper = false; + bool m_rightSuper = false; +}; + +} // namespace XCEngine::XCUI::Host diff --git a/new_editor/src/NativeRenderer.h b/new_editor/include/XCNewEditor/Host/NativeRenderer.h similarity index 95% rename from new_editor/src/NativeRenderer.h rename to new_editor/include/XCNewEditor/Host/NativeRenderer.h index cb379f14..0d6bc6fd 100644 --- a/new_editor/src/NativeRenderer.h +++ b/new_editor/include/XCNewEditor/Host/NativeRenderer.h @@ -18,8 +18,7 @@ #include #include -namespace XCEngine { -namespace NewEditor { +namespace XCEngine::XCUI::Host { class NativeRenderer { public: @@ -63,5 +62,4 @@ private: bool m_wicComInitialized = false; }; -} // namespace NewEditor -} // namespace XCEngine +} // namespace XCEngine::XCUI::Host diff --git a/engine/include/XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h b/new_editor/include/XCNewEditor/Widgets/UIEditorCollectionPrimitives.h similarity index 100% rename from engine/include/XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h rename to new_editor/include/XCNewEditor/Widgets/UIEditorCollectionPrimitives.h diff --git a/engine/include/XCEngine/UI/Widgets/UIEditorPanelChrome.h b/new_editor/include/XCNewEditor/Widgets/UIEditorPanelChrome.h similarity index 100% rename from engine/include/XCEngine/UI/Widgets/UIEditorPanelChrome.h rename to new_editor/include/XCNewEditor/Widgets/UIEditorPanelChrome.h diff --git a/new_editor/src/Application.cpp b/new_editor/src/Host/Application.cpp similarity index 65% rename from new_editor/src/Application.cpp rename to new_editor/src/Host/Application.cpp index 5da8a1b9..a4073eeb 100644 --- a/new_editor/src/Application.cpp +++ b/new_editor/src/Host/Application.cpp @@ -1,6 +1,6 @@ #include "Application.h" -#include "SandboxFrameBuilder.h" +#include #include #include @@ -15,8 +15,7 @@ #define XCNEWEDITOR_REPO_ROOT "." #endif -namespace XCEngine { -namespace NewEditor { +namespace XCEngine::NewEditor { namespace { @@ -25,14 +24,14 @@ using ::XCEngine::UI::UIDrawData; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; -using ::XCEngine::UI::UIInputModifiers; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::UIRect; using ::XCEngine::UI::Runtime::UIScreenFrameInput; +using ::XCEngine::Input::KeyCode; -constexpr const wchar_t* kWindowClassName = L"XCNewEditorNativeSandbox"; -constexpr const wchar_t* kWindowTitle = L"XCNewEditor Native Sandbox"; +constexpr const wchar_t* kWindowClassName = L"XCNewEditorShellHost"; +constexpr const wchar_t* kWindowTitle = L"XCUI New Editor"; constexpr auto kReloadPollInterval = std::chrono::milliseconds(150); constexpr UIColor kOverlayBgColor(0.10f, 0.10f, 0.10f, 0.95f); @@ -83,21 +82,79 @@ std::string FormatPoint(const UIPoint& point) { return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")"; } -std::string FormatRect(const UIRect& rect) { - return "(" + FormatFloat(rect.x) + - ", " + FormatFloat(rect.y) + - ", " + FormatFloat(rect.width) + - ", " + FormatFloat(rect.height) + - ")"; +std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) { + switch (wParam) { + case 'A': return static_cast(KeyCode::A); + case 'B': return static_cast(KeyCode::B); + case 'C': return static_cast(KeyCode::C); + case 'D': return static_cast(KeyCode::D); + case 'E': return static_cast(KeyCode::E); + case 'F': return static_cast(KeyCode::F); + case 'G': return static_cast(KeyCode::G); + case 'H': return static_cast(KeyCode::H); + case 'I': return static_cast(KeyCode::I); + case 'J': return static_cast(KeyCode::J); + case 'K': return static_cast(KeyCode::K); + case 'L': return static_cast(KeyCode::L); + case 'M': return static_cast(KeyCode::M); + case 'N': return static_cast(KeyCode::N); + case 'O': return static_cast(KeyCode::O); + case 'P': return static_cast(KeyCode::P); + case 'Q': return static_cast(KeyCode::Q); + case 'R': return static_cast(KeyCode::R); + case 'S': return static_cast(KeyCode::S); + case 'T': return static_cast(KeyCode::T); + case 'U': return static_cast(KeyCode::U); + case 'V': return static_cast(KeyCode::V); + case 'W': return static_cast(KeyCode::W); + case 'X': return static_cast(KeyCode::X); + case 'Y': return static_cast(KeyCode::Y); + case 'Z': return static_cast(KeyCode::Z); + case '0': return static_cast(KeyCode::Zero); + case '1': return static_cast(KeyCode::One); + case '2': return static_cast(KeyCode::Two); + case '3': return static_cast(KeyCode::Three); + case '4': return static_cast(KeyCode::Four); + case '5': return static_cast(KeyCode::Five); + case '6': return static_cast(KeyCode::Six); + case '7': return static_cast(KeyCode::Seven); + case '8': return static_cast(KeyCode::Eight); + case '9': return static_cast(KeyCode::Nine); + case VK_SPACE: return static_cast(KeyCode::Space); + case VK_TAB: return static_cast(KeyCode::Tab); + case VK_RETURN: return static_cast(KeyCode::Enter); + case VK_ESCAPE: return static_cast(KeyCode::Escape); + case VK_SHIFT: return static_cast(KeyCode::LeftShift); + case VK_CONTROL: return static_cast(KeyCode::LeftCtrl); + case VK_MENU: return static_cast(KeyCode::LeftAlt); + case VK_UP: return static_cast(KeyCode::Up); + case VK_DOWN: return static_cast(KeyCode::Down); + case VK_LEFT: return static_cast(KeyCode::Left); + case VK_RIGHT: return static_cast(KeyCode::Right); + case VK_HOME: return static_cast(KeyCode::Home); + case VK_END: return static_cast(KeyCode::End); + case VK_PRIOR: return static_cast(KeyCode::PageUp); + case VK_NEXT: return static_cast(KeyCode::PageDown); + case VK_DELETE: return static_cast(KeyCode::Delete); + case VK_BACK: return static_cast(KeyCode::Backspace); + case VK_F1: return static_cast(KeyCode::F1); + case VK_F2: return static_cast(KeyCode::F2); + case VK_F3: return static_cast(KeyCode::F3); + case VK_F4: return static_cast(KeyCode::F4); + case VK_F5: return static_cast(KeyCode::F5); + case VK_F6: return static_cast(KeyCode::F6); + case VK_F7: return static_cast(KeyCode::F7); + case VK_F8: return static_cast(KeyCode::F8); + case VK_F9: return static_cast(KeyCode::F9); + case VK_F10: return static_cast(KeyCode::F10); + case VK_F11: return static_cast(KeyCode::F11); + case VK_F12: return static_cast(KeyCode::F12); + default: return static_cast(KeyCode::None); + } } -UIInputModifiers BuildInputModifiers(size_t wParam) { - UIInputModifiers modifiers = {}; - modifiers.shift = (wParam & MK_SHIFT) != 0; - modifiers.control = (wParam & MK_CONTROL) != 0; - modifiers.alt = (GetKeyState(VK_MENU) & 0x8000) != 0; - modifiers.super = (GetKeyState(VK_LWIN) & 0x8000) != 0 || (GetKeyState(VK_RWIN) & 0x8000) != 0; - return modifiers; +bool IsRepeatKeyMessage(LPARAM lParam) { + return (static_cast(lParam) & (1ul << 30)) != 0ul; } } // namespace @@ -130,6 +187,7 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_hInstance = hInstance; + m_shellAssetDefinition = BuildDefaultEditorShellAsset(ResolveRepoRootPath()); WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); @@ -170,7 +228,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_startTime = std::chrono::steady_clock::now(); m_lastFrameTime = m_startTime; - m_autoScreenshot.Initialize(ResolveRepoRelativePath("new_editor/captures")); + m_autoScreenshot.Initialize(m_shellAssetDefinition.captureRootPath); LoadStructuredScreen("startup"); return true; } @@ -209,7 +267,6 @@ void Application::RenderFrame() { const float height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); const auto now = std::chrono::steady_clock::now(); - const double timeSeconds = std::chrono::duration(now - m_startTime).count(); double deltaTimeSeconds = std::chrono::duration(now - m_lastFrameTime).count(); if (deltaTimeSeconds <= 0.0) { deltaTimeSeconds = 1.0 / 60.0; @@ -234,17 +291,10 @@ void Application::RenderFrame() { drawData.AddDrawList(drawList); } - m_runtimeStatus = "Authored XCUI"; + m_runtimeStatus = "XCUI Editor Shell"; m_runtimeError = frame.errorMessage; - } - - if (drawData.Empty()) { - SandboxFrameOptions options = {}; - options.width = width; - options.height = height; - options.timeSeconds = timeSeconds; - drawData = BuildSandboxFrame(options); - m_runtimeStatus = "Fallback Sandbox"; + } else { + m_runtimeStatus = "Editor Shell | Load Error"; if (m_runtimeError.empty() && !m_screenPlayer.IsLoaded()) { m_runtimeError = m_screenPlayer.GetLastError(); } @@ -276,7 +326,7 @@ void Application::QueuePointerEvent(UIInputEventType type, UIPointerButton butto event.position = UIPoint( static_cast(GET_X_LPARAM(lParam)), static_cast(GET_Y_LPARAM(lParam))); - event.modifiers = BuildInputModifiers(static_cast(wParam)); + event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); m_pendingInputEvents.push_back(event); } @@ -307,19 +357,43 @@ void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM event.type = UIInputEventType::PointerWheel; event.position = UIPoint(static_cast(screenPoint.x), static_cast(screenPoint.y)); event.wheelDelta = static_cast(wheelDelta); - event.modifiers = BuildInputModifiers(static_cast(wParam)); + event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); + m_pendingInputEvents.push_back(event); +} + +void Application::QueueKeyEvent(UIInputEventType type, WPARAM wParam, LPARAM lParam) { + UIInputEvent event = {}; + event.type = type; + event.keyCode = MapVirtualKeyToUIKeyCode(wParam); + event.modifiers = m_inputModifierTracker.ApplyKeyMessage(type, wParam, lParam); + event.repeat = IsRepeatKeyMessage(lParam); + m_pendingInputEvents.push_back(event); +} + +void Application::QueueCharacterEvent(WPARAM wParam, LPARAM) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(wParam); + event.modifiers = m_inputModifierTracker.GetCurrentModifiers(); + m_pendingInputEvents.push_back(event); +} + +void Application::QueueWindowFocusEvent(UIInputEventType type) { + UIInputEvent event = {}; + event.type = type; m_pendingInputEvents.push_back(event); } bool Application::LoadStructuredScreen(const char* triggerReason) { + (void)triggerReason; m_screenAsset = {}; - m_screenAsset.screenId = "new_editor.editor_shell"; - m_screenAsset.documentPath = ResolveRepoRelativePath("new_editor/ui/views/editor_shell.xcui").string(); - m_screenAsset.themePath = ResolveRepoRelativePath("new_editor/ui/themes/editor_shell.xctheme").string(); + m_screenAsset.screenId = m_shellAssetDefinition.screenId; + m_screenAsset.documentPath = m_shellAssetDefinition.documentPath.string(); + m_screenAsset.themePath = m_shellAssetDefinition.themePath.string(); const bool loaded = m_screenPlayer.Load(m_screenAsset); m_useStructuredScreen = loaded; - m_runtimeStatus = loaded ? "Authored XCUI" : "Fallback Sandbox"; + m_runtimeStatus = loaded ? "XCUI Editor Shell" : "Editor Shell | Load Error"; m_runtimeError = loaded ? std::string() : m_screenPlayer.GetLastError(); RebuildTrackedFileStates(); return loaded; @@ -404,12 +478,13 @@ bool Application::DetectTrackedFileChange() const { void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const { const bool authoredMode = m_useStructuredScreen && m_screenPlayer.IsLoaded(); - const float panelWidth = authoredMode ? 420.0f : 360.0f; + const float panelWidth = authoredMode ? 430.0f : 380.0f; std::vector detailLines = {}; detailLines.push_back( authoredMode - ? "Hot reload watches authored UI resources." - : "Using native fallback while authored UI is invalid."); + ? "Hot reload watches editor shell resources." + : "Authored editor shell failed to load."); + detailLines.push_back("Document: editor_shell.xcui"); if (authoredMode) { const auto& inputDebug = m_documentHost.GetInputDebugSnapshot(); @@ -424,18 +499,24 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float " | " + ExtractStateKeyTail(inputDebug.captureStateKey)); if (!inputDebug.lastEventType.empty()) { + const std::string eventPosition = inputDebug.lastEventType == "KeyDown" || + inputDebug.lastEventType == "KeyUp" || + inputDebug.lastEventType == "Character" || + inputDebug.lastEventType == "FocusGained" || + inputDebug.lastEventType == "FocusLost" + ? std::string() + : " at " + FormatPoint(inputDebug.pointerPosition); detailLines.push_back( "Last input: " + inputDebug.lastEventType + - " at " + - FormatPoint(inputDebug.pointerPosition)); + eventPosition); detailLines.push_back( "Route: " + inputDebug.lastTargetKind + " -> " + ExtractStateKeyTail(inputDebug.lastTargetStateKey)); detailLines.push_back( - "Result: " + + "Last event result: " + (inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult)); } } @@ -445,13 +526,15 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float } else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u)); } else { - detailLines.push_back("Screenshots: manual only (F12)"); + detailLines.push_back("Screenshots: F12 -> new_editor/captures/"); } if (!m_runtimeError.empty()) { detailLines.push_back(TruncateText(m_runtimeError, 78u)); } else if (!m_autoScreenshot.GetLastCaptureError().empty()) { detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureError(), 78u)); + } else if (!authoredMode) { + detailLines.push_back("No fallback sandbox is rendered in this host."); } const float panelHeight = 38.0f + static_cast(detailLines.size()) * 18.0f; @@ -466,7 +549,7 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float 4.0f); overlay.AddText( UIPoint(panelRect.x + 28.0f, panelRect.y + 10.0f), - m_runtimeStatus.empty() ? "Runtime State" : m_runtimeStatus, + m_runtimeStatus.empty() ? "XCUI Editor Shell" : m_runtimeStatus, kOverlayTextPrimary, 14.0f); @@ -484,8 +567,12 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float } } -std::filesystem::path Application::ResolveRepoRelativePath(const char* relativePath) { - return (std::filesystem::path(XCNEWEDITOR_REPO_ROOT) / relativePath).lexically_normal(); +std::filesystem::path Application::ResolveRepoRootPath() { + std::string root = XCNEWEDITOR_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + return std::filesystem::path(root).lexically_normal(); } LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { @@ -557,9 +644,40 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP return 0; } break; + case WM_SETFOCUS: + if (application != nullptr) { + application->m_inputModifierTracker.SyncFromSystemState(); + application->QueueWindowFocusEvent(UIInputEventType::FocusGained); + return 0; + } + break; + case WM_KILLFOCUS: + if (application != nullptr) { + application->m_inputModifierTracker.Reset(); + application->QueueWindowFocusEvent(UIInputEventType::FocusLost); + return 0; + } + break; case WM_KEYDOWN: - if (application != nullptr && wParam == VK_F12) { - application->m_autoScreenshot.RequestCapture("manual_f12"); + case WM_SYSKEYDOWN: + if (application != nullptr) { + if (wParam == VK_F12) { + application->m_autoScreenshot.RequestCapture("manual_f12"); + } + application->QueueKeyEvent(UIInputEventType::KeyDown, wParam, lParam); + return 0; + } + break; + case WM_KEYUP: + case WM_SYSKEYUP: + if (application != nullptr) { + application->QueueKeyEvent(UIInputEventType::KeyUp, wParam, lParam); + return 0; + } + break; + case WM_CHAR: + if (application != nullptr) { + application->QueueCharacterEvent(wParam, lParam); return 0; } break; @@ -579,9 +697,8 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP } int RunNewEditor(HINSTANCE hInstance, int nCmdShow) { - Application application = {}; + Application application; return application.Run(hInstance, nCmdShow); } -} // namespace NewEditor -} // namespace XCEngine +} // namespace XCEngine::NewEditor diff --git a/new_editor/src/Application.h b/new_editor/src/Host/Application.h similarity index 73% rename from new_editor/src/Application.h rename to new_editor/src/Host/Application.h index 2940b3d7..5c245295 100644 --- a/new_editor/src/Application.h +++ b/new_editor/src/Host/Application.h @@ -4,8 +4,11 @@ #define NOMINMAX #endif -#include "AutoScreenshot.h" -#include "NativeRenderer.h" +#include +#include +#include + +#include "editor/EditorShellAsset.h" #include #include @@ -19,8 +22,7 @@ #include #include -namespace XCEngine { -namespace NewEditor { +namespace XCEngine::NewEditor { class Application { public: @@ -44,27 +46,32 @@ private: void QueuePointerEvent(::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, WPARAM wParam, LPARAM lParam); void QueuePointerLeaveEvent(); void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam); + void QueueKeyEvent(::XCEngine::UI::UIInputEventType type, WPARAM wParam, LPARAM lParam); + void QueueCharacterEvent(WPARAM wParam, LPARAM lParam); + void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type); bool LoadStructuredScreen(const char* triggerReason); void RefreshStructuredScreen(); void RebuildTrackedFileStates(); bool DetectTrackedFileChange() const; void AppendRuntimeOverlay(::XCEngine::UI::UIDrawData& drawData, float width, float height) const; - static std::filesystem::path ResolveRepoRelativePath(const char* relativePath); + static std::filesystem::path ResolveRepoRootPath(); HWND m_hwnd = nullptr; HINSTANCE m_hInstance = nullptr; ATOM m_windowClassAtom = 0; - NativeRenderer m_renderer; - AutoScreenshotController m_autoScreenshot; + ::XCEngine::XCUI::Host::NativeRenderer m_renderer; + ::XCEngine::XCUI::Host::AutoScreenshotController m_autoScreenshot; ::XCEngine::UI::Runtime::UIDocumentScreenHost m_documentHost; ::XCEngine::UI::Runtime::UIScreenPlayer m_screenPlayer; ::XCEngine::UI::Runtime::UIScreenAsset m_screenAsset = {}; + EditorShellAsset m_shellAssetDefinition = {}; std::vector m_trackedFiles = {}; std::chrono::steady_clock::time_point m_startTime = {}; std::chrono::steady_clock::time_point m_lastFrameTime = {}; std::chrono::steady_clock::time_point m_lastReloadPollTime = {}; std::uint64_t m_frameIndex = 0; std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {}; + ::XCEngine::XCUI::Host::InputModifierTracker m_inputModifierTracker = {}; bool m_trackingMouseLeave = false; bool m_useStructuredScreen = false; std::string m_runtimeStatus = {}; @@ -73,5 +80,4 @@ private: int RunNewEditor(HINSTANCE hInstance, int nCmdShow); -} // namespace NewEditor -} // namespace XCEngine +} // namespace XCEngine::NewEditor diff --git a/new_editor/src/AutoScreenshot.cpp b/new_editor/src/Host/AutoScreenshot.cpp similarity index 96% rename from new_editor/src/AutoScreenshot.cpp rename to new_editor/src/Host/AutoScreenshot.cpp index 169c6465..8a2b5658 100644 --- a/new_editor/src/AutoScreenshot.cpp +++ b/new_editor/src/Host/AutoScreenshot.cpp @@ -1,6 +1,6 @@ -#include "AutoScreenshot.h" +#include -#include "NativeRenderer.h" +#include #include #include @@ -8,8 +8,7 @@ #include #include -namespace XCEngine { -namespace NewEditor { +namespace XCEngine::XCUI::Host { void AutoScreenshotController::Initialize(const std::filesystem::path& captureRoot) { m_captureRoot = captureRoot.lexically_normal(); @@ -144,5 +143,4 @@ std::string AutoScreenshotController::SanitizeReason(std::string_view reason) { return sanitized.empty() ? "capture" : sanitized; } -} // namespace NewEditor -} // namespace XCEngine +} // namespace XCEngine::XCUI::Host diff --git a/new_editor/src/NativeRenderer.cpp b/new_editor/src/Host/NativeRenderer.cpp similarity index 99% rename from new_editor/src/NativeRenderer.cpp rename to new_editor/src/Host/NativeRenderer.cpp index 6fd14e77..7cc3ab4e 100644 --- a/new_editor/src/NativeRenderer.cpp +++ b/new_editor/src/Host/NativeRenderer.cpp @@ -1,11 +1,10 @@ -#include "NativeRenderer.h" +#include #include #include #include -namespace XCEngine { -namespace NewEditor { +namespace XCEngine::XCUI::Host { namespace { @@ -483,5 +482,4 @@ std::wstring NativeRenderer::Utf8ToWide(std::string_view text) { return wideText; } -} // namespace NewEditor -} // namespace XCEngine +} // namespace XCEngine::XCUI::Host diff --git a/new_editor/src/SandboxFrameBuilder.cpp b/new_editor/src/SandboxFrameBuilder.cpp deleted file mode 100644 index 0e2feb9c..00000000 --- a/new_editor/src/SandboxFrameBuilder.cpp +++ /dev/null @@ -1,334 +0,0 @@ -#include "SandboxFrameBuilder.h" - -#include - -#include -#include -#include -#include - -namespace XCEngine { -namespace NewEditor { - -namespace { - -using ::XCEngine::UI::UIColor; -using ::XCEngine::UI::UIDrawData; -using ::XCEngine::UI::UIDrawList; -using ::XCEngine::UI::UIPoint; -using ::XCEngine::UI::UIRect; -using ::XCEngine::UI::Widgets::AppendUIEditorPanelChromeBackground; -using ::XCEngine::UI::Widgets::AppendUIEditorPanelChromeForeground; -using ::XCEngine::UI::Widgets::UIEditorPanelChromeMetrics; -using ::XCEngine::UI::Widgets::UIEditorPanelChromeState; -using ::XCEngine::UI::Widgets::UIEditorPanelChromeText; - -constexpr UIColor kWorkspaceColor(0.05f, 0.07f, 0.09f, 1.0f); -constexpr UIColor kShellColor(0.08f, 0.10f, 0.13f, 1.0f); -constexpr UIColor kSubtleLineColor(0.18f, 0.23f, 0.29f, 1.0f); -constexpr UIColor kTextPrimary(0.92f, 0.95f, 0.98f, 1.0f); -constexpr UIColor kTextSecondary(0.67f, 0.74f, 0.81f, 1.0f); -constexpr UIColor kTextMuted(0.49f, 0.56f, 0.64f, 1.0f); -constexpr UIColor kSelectionColor(0.16f, 0.37f, 0.64f, 0.90f); -constexpr UIColor kAccentColor(0.19f, 0.76f, 0.57f, 1.0f); -constexpr UIColor kWarningColor(0.96f, 0.72f, 0.22f, 1.0f); -constexpr UIColor kErrorColor(0.88f, 0.29f, 0.30f, 1.0f); - -float ClampPositive(float value, float fallback) { - return value > 1.0f ? value : fallback; -} - -float Pulse01(double timeSeconds, double speed) { - const double phase = std::sin(timeSeconds * speed); - return static_cast(0.5 + phase * 0.5); -} - -UIRect InsetRect(const UIRect& rect, float insetX, float insetY) { - return UIRect( - rect.x + insetX, - rect.y + insetY, - (std::max)(0.0f, rect.width - insetX * 2.0f), - (std::max)(0.0f, rect.height - insetY * 2.0f)); -} - -void AddSectionTitle( - UIDrawList& drawList, - float x, - float y, - std::string_view title, - std::string_view subtitle = {}) { - drawList.AddText(UIPoint(x, y), std::string(title), kTextPrimary, 17.0f); - if (!subtitle.empty()) { - drawList.AddText(UIPoint(x, y + 20.0f), std::string(subtitle), kTextMuted, 13.0f); - } -} - -void AddRow( - UIDrawList& drawList, - const UIRect& rect, - std::string_view label, - bool selected = false, - float indent = 0.0f, - const UIColor& dotColor = kAccentColor) { - if (selected) { - drawList.AddFilledRect(rect, kSelectionColor, 6.0f); - } - - drawList.AddFilledRect( - UIRect(rect.x + 10.0f + indent, rect.y + 10.0f, 8.0f, 8.0f), - dotColor, - 4.0f); - drawList.AddText( - UIPoint(rect.x + 26.0f + indent, rect.y + 6.0f), - std::string(label), - selected ? kTextPrimary : kTextSecondary, - 14.0f); -} - -void AddPropertyRow( - UIDrawList& drawList, - const UIRect& rect, - std::string_view name, - std::string_view value) { - drawList.AddText(UIPoint(rect.x, rect.y + 6.0f), std::string(name), kTextSecondary, 14.0f); - - const UIRect fieldRect(rect.x + rect.width * 0.42f, rect.y, rect.width * 0.58f, rect.height); - drawList.AddFilledRect(fieldRect, UIColor(0.11f, 0.15f, 0.20f, 1.0f), 6.0f); - drawList.AddRectOutline(fieldRect, kSubtleLineColor, 1.0f, 6.0f); - drawList.AddText( - UIPoint(fieldRect.x + 10.0f, fieldRect.y + 6.0f), - std::string(value), - kTextPrimary, - 14.0f); -} - -void AddLogRow( - UIDrawList& drawList, - const UIRect& rect, - const UIColor& levelColor, - std::string_view level, - std::string_view message) { - drawList.AddFilledRect( - UIRect(rect.x, rect.y + 7.0f, 6.0f, rect.height - 14.0f), - levelColor, - 3.0f); - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 4.0f), std::string(level), levelColor, 13.0f); - drawList.AddText(UIPoint(rect.x + 82.0f, rect.y + 4.0f), std::string(message), kTextSecondary, 13.0f); -} - -void AppendPanel( - UIDrawList& drawList, - const UIRect& panelRect, - std::string_view title, - std::string_view subtitle, - std::string_view footer, - bool active) { - UIEditorPanelChromeState state = {}; - state.active = active; - state.hovered = active; - - UIEditorPanelChromeText text = {}; - text.title = title; - text.subtitle = subtitle; - text.footer = footer; - - UIEditorPanelChromeMetrics metrics = {}; - metrics.headerHeight = 46.0f; - - AppendUIEditorPanelChromeBackground(drawList, panelRect, state, {}, metrics); - AppendUIEditorPanelChromeForeground(drawList, panelRect, text, {}, metrics); -} - -void AppendTopBar(UIDrawList& drawList, const UIRect& rect, double timeSeconds) { - drawList.AddFilledRect(rect, kShellColor, 0.0f); - drawList.AddRectOutline(rect, kSubtleLineColor, 1.0f, 0.0f); - drawList.AddText( - UIPoint(rect.x + 20.0f, rect.y + 14.0f), - "XCUI Native Sandbox", - kTextPrimary, - 20.0f); - drawList.AddText( - UIPoint(rect.x + 228.0f, rect.y + 17.0f), - "editor ui proving ground", - kTextMuted, - 13.0f); - - const float pulse = Pulse01(timeSeconds, 2.2); - const UIColor liveColor( - 0.18f + pulse * 0.16f, - 0.74f, - 0.58f, - 1.0f); - drawList.AddFilledRect( - UIRect(rect.x + rect.width - 170.0f, rect.y + 12.0f, 108.0f, 24.0f), - UIColor(0.10f, 0.16f, 0.12f, 1.0f), - 12.0f); - drawList.AddFilledRect( - UIRect(rect.x + rect.width - 160.0f, rect.y + 20.0f, 8.0f, 8.0f), - liveColor, - 4.0f); - drawList.AddText( - UIPoint(rect.x + rect.width - 146.0f, rect.y + 14.0f), - "Native Render", - kTextPrimary, - 13.0f); -} - -void AppendStatusBar(UIDrawList& drawList, const UIRect& rect, const SandboxFrameOptions& options) { - drawList.AddFilledRect(rect, kShellColor, 0.0f); - drawList.AddRectOutline(rect, kSubtleLineColor, 1.0f, 0.0f); - - const std::string leftText = - "Direct renderer | size " + - std::to_string(static_cast(options.width)) + - "x" + - std::to_string(static_cast(options.height)); - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 8.0f), leftText, kTextSecondary, 13.0f); - drawList.AddText( - UIPoint(rect.x + rect.width - 176.0f, rect.y + 8.0f), - "No ImGui Host", - kAccentColor, - 13.0f); -} - -void AppendHierarchyPanel(UIDrawList& drawList, const UIRect& panelRect) { - AppendPanel(drawList, panelRect, "Hierarchy", "scene graph", "12 items", false); - - const UIRect body = InsetRect(panelRect, 16.0f, 60.0f); - AddSectionTitle(drawList, body.x, body.y, "Active Scene", "Sandbox_City.xcscene"); - - float rowY = body.y + 42.0f; - const float rowHeight = 28.0f; - AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "World", false, 0.0f, kWarningColor); - rowY += rowHeight + 4.0f; - AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Main Camera", false, 18.0f); - rowY += rowHeight + 4.0f; - AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Directional Light", false, 18.0f, kWarningColor); - rowY += rowHeight + 4.0f; - AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Player", true, 18.0f); - rowY += rowHeight + 4.0f; - AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "WeaponSocket", false, 36.0f, UIColor(0.58f, 0.70f, 0.88f, 1.0f)); - rowY += rowHeight + 4.0f; - AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "FX_Trail", false, 36.0f, UIColor(0.76f, 0.43f, 0.93f, 1.0f)); -} - -void AppendInspectorPanel(UIDrawList& drawList, const UIRect& panelRect) { - AppendPanel(drawList, panelRect, "Inspector", "Player", "schema-first draft", false); - - const UIRect body = InsetRect(panelRect, 16.0f, 60.0f); - AddSectionTitle(drawList, body.x, body.y, "Transform", "shared widget experiment"); - - float rowY = body.y + 42.0f; - const float rowHeight = 30.0f; - AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Position", "12.0, 1.8, -4.0"); - rowY += rowHeight + 6.0f; - AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Rotation", "0.0, 36.5, 0.0"); - rowY += rowHeight + 6.0f; - AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Scale", "1.0, 1.0, 1.0"); - rowY += 48.0f; - - AddSectionTitle(drawList, body.x, rowY, "Character", "future AutoForm target"); - rowY += 42.0f; - AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "State", "Locomotion"); - rowY += rowHeight + 6.0f; - AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Move Speed", "6.4"); - rowY += rowHeight + 6.0f; - AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Jump Height", "1.3"); -} - -void AppendScenePanel(UIDrawList& drawList, const UIRect& panelRect, double timeSeconds) { - AppendPanel(drawList, panelRect, "Scene", "native shell draft", "ViewportSlot comes later", true); - - const UIRect body = InsetRect(panelRect, 16.0f, 60.0f); - drawList.AddFilledRect(body, UIColor(0.07f, 0.09f, 0.11f, 1.0f), 12.0f); - drawList.AddRectOutline(body, kSubtleLineColor, 1.0f, 12.0f); - - const float gridStep = 34.0f; - for (float x = body.x; x < body.x + body.width; x += gridStep) { - drawList.AddRectOutline(UIRect(x, body.y, 1.0f, body.height), UIColor(0.10f, 0.13f, 0.16f, 0.6f)); - } - for (float y = body.y; y < body.y + body.height; y += gridStep) { - drawList.AddRectOutline(UIRect(body.x, y, body.width, 1.0f), UIColor(0.10f, 0.13f, 0.16f, 0.6f)); - } - - const float pulse = Pulse01(timeSeconds, 1.8); - const UIRect selectionRect( - body.x + body.width * 0.32f, - body.y + body.height * 0.24f, - body.width * 0.22f, - body.height * 0.34f); - drawList.AddRectOutline( - selectionRect, - UIColor(0.24f + pulse * 0.12f, 0.56f, 0.95f, 1.0f), - 2.0f, - 8.0f); - drawList.AddText( - UIPoint(body.x + 18.0f, body.y + 18.0f), - "This area is intentionally native-rendered, not hosted by ImGui.", - kTextSecondary, - 15.0f); - drawList.AddText( - UIPoint(body.x + 18.0f, body.y + 40.0f), - "Use this sandbox to iterate shell chrome, panel composition, and draw packets.", - kTextMuted, - 13.0f); -} - -void AppendConsolePanel(UIDrawList& drawList, const UIRect& panelRect) { - AppendPanel(drawList, panelRect, "Console", "native renderer smoke log", "6 records", false); - - const UIRect body = InsetRect(panelRect, 16.0f, 60.0f); - AddLogRow(drawList, UIRect(body.x, body.y, body.width, 22.0f), kAccentColor, "Info", "XCUI native sandbox frame submitted."); - AddLogRow(drawList, UIRect(body.x, body.y + 26.0f, body.width, 22.0f), kWarningColor, "Warn", "Viewport host not connected in this phase."); - AddLogRow(drawList, UIRect(body.x, body.y + 52.0f, body.width, 22.0f), kTextSecondary, "Info", "Hierarchy / Inspector are draw-packet prototypes."); - AddLogRow(drawList, UIRect(body.x, body.y + 78.0f, body.width, 22.0f), kErrorColor, "Todo", "Replace bespoke scene panel with authored shell document."); -} - -} // namespace - -UIDrawData BuildSandboxFrame(const SandboxFrameOptions& options) { - const float width = ClampPositive(options.width, 1280.0f); - const float height = ClampPositive(options.height, 720.0f); - - const float margin = 18.0f; - const float gap = 14.0f; - const float topBarHeight = 48.0f; - const float statusBarHeight = 30.0f; - const float leftPanelWidth = 260.0f; - const float rightPanelWidth = 320.0f; - const float bottomPanelHeight = 210.0f; - - const UIRect fullRect(0.0f, 0.0f, width, height); - const UIRect topBarRect(0.0f, 0.0f, width, topBarHeight); - const UIRect statusRect(0.0f, height - statusBarHeight, width, statusBarHeight); - - const float workspaceTop = topBarHeight + margin; - const float workspaceBottom = height - statusBarHeight - margin; - const float workspaceHeight = (std::max)(0.0f, workspaceBottom - workspaceTop); - const float centerX = margin + leftPanelWidth + gap; - const float centerWidth = (std::max)(240.0f, width - margin * 2.0f - leftPanelWidth - rightPanelWidth - gap * 2.0f); - const float sceneHeight = (std::max)(220.0f, workspaceHeight - bottomPanelHeight - gap); - - const UIRect hierarchyRect(margin, workspaceTop, leftPanelWidth, workspaceHeight); - const UIRect sceneRect(centerX, workspaceTop, centerWidth, sceneHeight); - const UIRect consoleRect(centerX, workspaceTop + sceneHeight + gap, centerWidth, bottomPanelHeight); - const UIRect inspectorRect(centerX + centerWidth + gap, workspaceTop, rightPanelWidth, workspaceHeight); - - UIDrawData drawData = {}; - UIDrawList& drawList = drawData.EmplaceDrawList("XCUI Native Editor Sandbox"); - drawList.PushClipRect(fullRect); - drawList.AddFilledRect(fullRect, kWorkspaceColor, 0.0f); - - AppendTopBar(drawList, topBarRect, options.timeSeconds); - AppendHierarchyPanel(drawList, hierarchyRect); - AppendScenePanel(drawList, sceneRect, options.timeSeconds); - AppendConsolePanel(drawList, consoleRect); - AppendInspectorPanel(drawList, inspectorRect); - AppendStatusBar(drawList, statusRect, options); - - drawList.PopClipRect(); - return drawData; -} - -} // namespace NewEditor -} // namespace XCEngine diff --git a/new_editor/src/SandboxFrameBuilder.h b/new_editor/src/SandboxFrameBuilder.h deleted file mode 100644 index 581c1236..00000000 --- a/new_editor/src/SandboxFrameBuilder.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -namespace XCEngine { -namespace NewEditor { - -struct SandboxFrameOptions { - float width = 1280.0f; - float height = 720.0f; - double timeSeconds = 0.0; -}; - -::XCEngine::UI::UIDrawData BuildSandboxFrame(const SandboxFrameOptions& options); - -} // namespace NewEditor -} // namespace XCEngine diff --git a/engine/src/UI/Widgets/UIEditorCollectionPrimitives.cpp b/new_editor/src/Widgets/UIEditorCollectionPrimitives.cpp similarity index 98% rename from engine/src/UI/Widgets/UIEditorCollectionPrimitives.cpp rename to new_editor/src/Widgets/UIEditorCollectionPrimitives.cpp index c7f511a9..d135513b 100644 --- a/engine/src/UI/Widgets/UIEditorCollectionPrimitives.cpp +++ b/new_editor/src/Widgets/UIEditorCollectionPrimitives.cpp @@ -1,4 +1,4 @@ -#include +#include namespace XCEngine { namespace UI { diff --git a/new_editor/src/editor/EditorShellAsset.cpp b/new_editor/src/editor/EditorShellAsset.cpp new file mode 100644 index 00000000..ee43912a --- /dev/null +++ b/new_editor/src/editor/EditorShellAsset.cpp @@ -0,0 +1,13 @@ +#include "EditorShellAsset.h" + +namespace XCEngine::NewEditor { + +EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot) { + EditorShellAsset asset = {}; + asset.documentPath = (repoRoot / "new_editor/ui/views/editor_shell.xcui").lexically_normal(); + asset.themePath = (repoRoot / "new_editor/ui/themes/editor_shell.xctheme").lexically_normal(); + asset.captureRootPath = (repoRoot / "new_editor/captures").lexically_normal(); + return asset; +} + +} // namespace XCEngine::NewEditor diff --git a/new_editor/src/editor/EditorShellAsset.h b/new_editor/src/editor/EditorShellAsset.h new file mode 100644 index 00000000..a0d1f01c --- /dev/null +++ b/new_editor/src/editor/EditorShellAsset.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace XCEngine::NewEditor { + +struct EditorShellAsset { + std::string screenId = "new_editor.shell"; + std::filesystem::path documentPath = {}; + std::filesystem::path themePath = {}; + std::filesystem::path captureRootPath = {}; +}; + +EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot); + +} // namespace XCEngine::NewEditor diff --git a/new_editor/src/main.cpp b/new_editor/src/main.cpp index 8f6d2bdd..e72fd270 100644 --- a/new_editor/src/main.cpp +++ b/new_editor/src/main.cpp @@ -1,4 +1,4 @@ -#include "Application.h" +#include "Host/Application.h" int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { return XCEngine::NewEditor::RunNewEditor(hInstance, nCmdShow); diff --git a/new_editor/ui/schemas/editor_inspector_shell.xcschema b/new_editor/ui/schemas/editor_inspector_shell.xcschema deleted file mode 100644 index f8f756b1..00000000 --- a/new_editor/ui/schemas/editor_inspector_shell.xcschema +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/new_editor/ui/views/editor_shell.xcui b/new_editor/ui/views/editor_shell.xcui index 008ac0cb..8e181a3f 100644 --- a/new_editor/ui/views/editor_shell.xcui +++ b/new_editor/ui/views/editor_shell.xcui @@ -1,30 +1,8 @@ - - - - - - - - - - - - -