From 8cde4e0649ac157a678249f94607dea7be889a6c Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 10 Apr 2026 16:40:11 +0800 Subject: [PATCH] Add new editor product shell baseline --- new_editor/CMakeLists.txt | 1 + new_editor/app/Application.cpp | 619 +++++++++--------- new_editor/app/Application.h | 70 +- new_editor/app/Shell/ProductShellAsset.cpp | 515 +++++++++++++++ new_editor/app/Shell/ProductShellAsset.h | 19 + .../Foundation/UIEditorCommandDispatcher.h | 36 +- .../Foundation/UIEditorCommandRegistry.h | 7 + .../Foundation/UIEditorShortcutManager.h | 8 + .../XCEditor/Shell/UIEditorShellCompose.h | 52 ++ .../XCEditor/Shell/UIEditorShellInteraction.h | 2 + .../Foundation/UIEditorCommandDispatcher.cpp | 98 ++- .../Foundation/UIEditorCommandRegistry.cpp | 22 + new_editor/src/Shell/UIEditorDockHost.cpp | 58 -- new_editor/src/Shell/UIEditorShellCompose.cpp | 219 ++++++- .../src/Shell/UIEditorShellInteraction.cpp | 2 + 15 files changed, 1290 insertions(+), 438 deletions(-) create mode 100644 new_editor/app/Shell/ProductShellAsset.cpp create mode 100644 new_editor/app/Shell/ProductShellAsset.h diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 8e338864..402ac7c2 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -145,6 +145,7 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) add_executable(XCUIEditorApp WIN32 app/main.cpp app/Application.cpp + app/Shell/ProductShellAsset.cpp ) target_include_directories(XCUIEditorApp PRIVATE diff --git a/new_editor/app/Application.cpp b/new_editor/app/Application.cpp index 3f162cd1..d9399269 100644 --- a/new_editor/app/Application.cpp +++ b/new_editor/app/Application.cpp @@ -1,15 +1,16 @@ #include "Application.h" +#include "Shell/ProductShellAsset.h" + +#include + #include +#include #include -#include -#include -#include +#include +#include #include -#include -#include -#include #ifndef XCUIEDITOR_REPO_ROOT #define XCUIEDITOR_REPO_ROOT "." @@ -19,6 +20,7 @@ namespace XCEngine::UI::Editor { namespace { +using ::XCEngine::Input::KeyCode; using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawData; using ::XCEngine::UI::UIDrawList; @@ -27,19 +29,11 @@ using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::UIRect; -using ::XCEngine::UI::Runtime::UIScreenFrameInput; -using ::XCEngine::Input::KeyCode; +using App::BuildProductShellAsset; +using App::BuildProductShellInteractionDefinition; -constexpr const wchar_t* kWindowClassName = L"XCUIEditorShellHost"; -constexpr const wchar_t* kWindowTitle = L"XCUI Editor"; -constexpr auto kReloadPollInterval = std::chrono::milliseconds(150); - -constexpr UIColor kOverlayBgColor(0.10f, 0.10f, 0.10f, 0.95f); -constexpr UIColor kOverlayBorderColor(0.25f, 0.25f, 0.25f, 1.0f); -constexpr UIColor kOverlayTextPrimary(0.93f, 0.93f, 0.93f, 1.0f); -constexpr UIColor kOverlayTextMuted(0.70f, 0.70f, 0.70f, 1.0f); -constexpr UIColor kOverlaySuccess(0.82f, 0.82f, 0.82f, 1.0f); -constexpr UIColor kOverlayFallback(0.56f, 0.56f, 0.56f, 1.0f); +constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; +constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; Application* GetApplicationFromWindow(HWND hwnd) { return reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); @@ -51,46 +45,30 @@ std::string TruncateText(const std::string& text, std::size_t maxLength) { } if (maxLength <= 3u) { - return text.substr(0, maxLength); + return text.substr(0u, maxLength); } - return text.substr(0, maxLength - 3u) + "..."; + return text.substr(0u, maxLength - 3u) + "..."; } -std::string ExtractStateKeyTail(const std::string& stateKey) { - if (stateKey.empty()) { - return "-"; +bool IsAutoCaptureOnStartupEnabled() { + const char* value = std::getenv("XCUI_AUTO_CAPTURE_ON_STARTUP"); + if (value == nullptr || value[0] == '\0') { + return false; } - const std::size_t separator = stateKey.find_last_of('/'); - if (separator == std::string::npos || separator + 1u >= stateKey.size()) { - return stateKey; - } - - return stateKey.substr(separator + 1u); -} - -std::string FormatFloat(float value) { - std::ostringstream stream; - stream.setf(std::ios::fixed, std::ios::floatfield); - stream.precision(1); - stream << value; - return stream.str(); -} - -std::string FormatPoint(const UIPoint& point) { - return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")"; -} - -void AppendErrorMessage(std::string& target, const std::string& message) { - if (message.empty()) { - return; - } - - if (!target.empty()) { - target += " | "; - } - target += message; + std::string normalized = value; + std::transform( + normalized.begin(), + normalized.end(), + normalized.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return normalized != "0" && + normalized != "false" && + normalized != "off" && + normalized != "no"; } std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) { @@ -170,10 +148,6 @@ bool IsRepeatKeyMessage(LPARAM lParam) { } // namespace -Application::Application() - : m_screenPlayer(m_documentHost) { -} - int Application::Run(HINSTANCE hInstance, int nCmdShow) { if (!Initialize(hInstance, nCmdShow)) { Shutdown(); @@ -198,10 +172,24 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_hInstance = hInstance; - m_shellAssetDefinition = BuildDefaultEditorShellAsset(ResolveRepoRootPath()); - m_structuredShell = BuildStructuredEditorShellBinding(m_shellAssetDefinition); - m_shellServices = BuildStructuredEditorShellServices(m_structuredShell); - m_screenAsset = m_structuredShell.screenAsset; + m_shellAsset = BuildProductShellAsset(ResolveRepoRootPath()); + m_shellValidation = ValidateEditorShellAsset(m_shellAsset); + m_validationMessage = m_shellValidation.message; + if (!m_shellValidation.IsValid()) { + return false; + } + + m_workspaceController = UIEditorWorkspaceController( + m_shellAsset.panelRegistry, + m_shellAsset.workspace, + m_shellAsset.workspaceSession); + m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset); + m_shortcutManager.SetHostCommandHandler(this); + m_shellServices = {}; + m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher(); + m_shellServices.shortcutManager = &m_shortcutManager; + m_lastStatus = "Ready"; + m_lastMessage = "Old editor shell baseline loaded."; WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); @@ -210,7 +198,6 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { windowClass.hInstance = hInstance; windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); windowClass.lpszClassName = kWindowClassName; - m_windowClassAtom = RegisterClassExW(&windowClass); if (m_windowClassAtom == 0) { return false; @@ -223,8 +210,8 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, - 1440, - 900, + 1540, + 940, nullptr, nullptr, hInstance, @@ -240,23 +227,21 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { return false; } - m_startTime = std::chrono::steady_clock::now(); - m_lastFrameTime = m_startTime; - m_autoScreenshot.Initialize(m_shellAssetDefinition.captureRootPath); - LoadStructuredScreen("startup"); + m_autoScreenshot.Initialize(m_shellAsset.captureRootPath); + if (IsAutoCaptureOnStartupEnabled()) { + m_autoScreenshot.RequestCapture("startup"); + m_lastStatus = "Capture"; + m_lastMessage = "Startup capture requested."; + } return true; } void Application::Shutdown() { - m_autoScreenshot.Shutdown(); - m_screenPlayer.Unload(); - m_trackedFiles.clear(); - m_screenAsset = {}; - m_useStructuredScreen = false; - m_runtimeStatus.clear(); - m_runtimeError.clear(); - m_frameIndex = 0; + if (GetCapture() == m_hwnd) { + ReleaseCapture(); + } + m_autoScreenshot.Shutdown(); m_renderer.Shutdown(); if (m_hwnd != nullptr && IsWindow(m_hwnd)) { @@ -280,47 +265,48 @@ void Application::RenderFrame() { const float width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); const float height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); - const auto now = std::chrono::steady_clock::now(); - double deltaTimeSeconds = std::chrono::duration(now - m_lastFrameTime).count(); - if (deltaTimeSeconds <= 0.0) { - deltaTimeSeconds = 1.0 / 60.0; - } - m_lastFrameTime = now; - - RefreshStructuredScreen(); - std::vector frameEvents = std::move(m_pendingInputEvents); - m_pendingInputEvents.clear(); - UIDrawData drawData = {}; - const bool hasAuthoredScreenDocument = !m_screenAsset.documentPath.empty(); - if (hasAuthoredScreenDocument && m_useStructuredScreen && m_screenPlayer.IsLoaded()) { - UIScreenFrameInput input = {}; - input.viewportRect = UIRect(0.0f, 0.0f, width, height); - input.events = std::move(frameEvents); - input.deltaTimeSeconds = deltaTimeSeconds; - input.frameIndex = ++m_frameIndex; - input.focused = GetForegroundWindow() == m_hwnd; + UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell"); + drawList.AddFilledRect( + UIRect(0.0f, 0.0f, width, height), + UIColor(0.10f, 0.10f, 0.10f, 1.0f)); - const auto& frame = m_screenPlayer.Update(input); - for (const auto& drawList : frame.drawData.GetDrawLists()) { - drawData.AddDrawList(drawList); - } + if (m_shellValidation.IsValid()) { + const auto& metrics = ResolveUIEditorShellInteractionMetrics(); + const auto& palette = ResolveUIEditorShellInteractionPalette(); + const UIEditorShellInteractionDefinition definition = BuildShellDefinition(); + std::vector frameEvents = std::move(m_pendingInputEvents); + m_pendingInputEvents.clear(); - m_runtimeStatus = "XCUI Editor Shell"; - m_runtimeError = frame.errorMessage; - } else if (!hasAuthoredScreenDocument) { - ++m_frameIndex; - m_runtimeStatus = "XCUI Editor Shell | Code-driven"; - m_runtimeError.clear(); + m_shellFrame = UpdateUIEditorShellInteraction( + m_shellInteractionState, + m_workspaceController, + UIRect(0.0f, 0.0f, width, height), + definition, + frameEvents, + m_shellServices, + metrics); + ApplyHostCaptureRequests(m_shellFrame.result); + UpdateLastStatus(m_shellFrame.result); + AppendUIEditorShellInteraction( + drawList, + m_shellFrame, + m_shellInteractionState, + palette, + metrics); } else { - m_runtimeStatus = "Editor Shell | Load Error"; - if (m_runtimeError.empty() && !m_screenPlayer.IsLoaded()) { - m_runtimeError = m_screenPlayer.GetLastError(); - } + drawList.AddText( + UIPoint(28.0f, 28.0f), + "Editor shell asset invalid.", + UIColor(0.92f, 0.92f, 0.92f, 1.0f), + 16.0f); + drawList.AddText( + UIPoint(28.0f, 54.0f), + m_validationMessage.empty() ? std::string("Unknown validation error.") : m_validationMessage, + UIColor(0.72f, 0.72f, 0.72f, 1.0f), + 12.0f); } - AppendRuntimeOverlay(drawData, width, height); - const bool framePresented = m_renderer.Render(drawData); m_autoScreenshot.CaptureIfRequested( m_renderer, @@ -338,7 +324,113 @@ void Application::OnResize(UINT width, UINT height) { m_renderer.Resize(width, height); } -void Application::QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) { +void Application::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) { + if (result.requestPointerCapture && GetCapture() != m_hwnd) { + SetCapture(m_hwnd); + } + if (result.releasePointerCapture && GetCapture() == m_hwnd) { + ReleaseCapture(); + } +} + +bool Application::HasInteractiveCaptureState() const { + if (m_shellInteractionState.workspaceInteractionState.dockHostInteractionState.splitterDragState.active) { + return true; + } + + for (const auto& panelState : m_shellInteractionState.workspaceInteractionState.composeState.panelStates) { + if (panelState.viewportShellState.inputBridgeState.captured) { + return true; + } + } + + return false; +} + +UIEditorShellInteractionDefinition Application::BuildShellDefinition() const { + std::string statusText = m_lastStatus; + if (!m_lastMessage.empty()) { + statusText += statusText.empty() ? m_lastMessage : ": " + m_lastMessage; + } + + std::string captureText = {}; + if (m_autoScreenshot.HasPendingCapture()) { + captureText = "Shot pending..."; + } else if (!m_autoScreenshot.GetLastCaptureError().empty()) { + captureText = TruncateText(m_autoScreenshot.GetLastCaptureError(), 38u); + } else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { + captureText = TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 38u); + } + + return BuildProductShellInteractionDefinition( + m_shellAsset, + m_workspaceController, + statusText, + captureText); +} + +void Application::UpdateLastStatus(const UIEditorShellInteractionResult& result) { + if (result.commandDispatched) { + m_lastStatus = std::string(GetUIEditorCommandDispatchStatusName(result.commandDispatchResult.status)); + m_lastMessage = result.commandDispatchResult.message.empty() + ? result.commandDispatchResult.displayName + : result.commandDispatchResult.message; + return; + } + + if (result.workspaceResult.dockHostResult.layoutChanged) { + m_lastStatus = "Layout"; + m_lastMessage = result.workspaceResult.dockHostResult.layoutResult.message; + return; + } + + if (result.workspaceResult.dockHostResult.commandExecuted) { + m_lastStatus = "Workspace"; + m_lastMessage = result.workspaceResult.dockHostResult.commandResult.message; + return; + } + + if (!result.viewportPanelId.empty()) { + m_lastStatus = result.viewportPanelId; + if (result.viewportInputFrame.captureStarted) { + m_lastMessage = "Viewport capture started."; + } else if (result.viewportInputFrame.captureEnded) { + m_lastMessage = "Viewport capture ended."; + } else if (result.viewportInputFrame.focusGained) { + m_lastMessage = "Viewport focused."; + } else if (result.viewportInputFrame.focusLost) { + m_lastMessage = "Viewport focus lost."; + } else if (result.viewportInputFrame.pointerPressedInside) { + m_lastMessage = "Viewport pointer down."; + } else if (result.viewportInputFrame.pointerReleasedInside) { + m_lastMessage = "Viewport pointer up."; + } else if (result.viewportInputFrame.pointerMoved) { + m_lastMessage = "Viewport pointer move."; + } else if (result.viewportInputFrame.wheelDelta != 0.0f) { + m_lastMessage = "Viewport wheel."; + } + return; + } + + if (result.menuMutation.changed) { + if (!result.itemId.empty() && !result.menuMutation.openedPopupId.empty()) { + m_lastStatus = "Menu"; + m_lastMessage = result.itemId + " opened child popup."; + } else if (!result.menuId.empty() && !result.menuMutation.openedPopupId.empty()) { + m_lastStatus = "Menu"; + m_lastMessage = result.menuId + " opened."; + } else { + m_lastStatus = "Menu"; + m_lastMessage = "Popup chain dismissed."; + } + } +} + +void Application::QueuePointerEvent( + UIInputEventType type, + UIPointerButton button, + WPARAM wParam, + LPARAM lParam) { UIInputEvent event = {}; event.type = type; event.pointerButton = button; @@ -356,7 +448,9 @@ void Application::QueuePointerLeaveEvent() { POINT clientPoint = {}; GetCursorPos(&clientPoint); ScreenToClient(m_hwnd, &clientPoint); - event.position = UIPoint(static_cast(clientPoint.x), static_cast(clientPoint.y)); + event.position = UIPoint( + static_cast(clientPoint.x), + static_cast(clientPoint.y)); } m_pendingInputEvents.push_back(event); } @@ -374,7 +468,9 @@ void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM UIInputEvent event = {}; event.type = UIInputEventType::PointerWheel; - event.position = UIPoint(static_cast(screenPoint.x), static_cast(screenPoint.y)); + event.position = UIPoint( + static_cast(screenPoint.x), + static_cast(screenPoint.y)); event.wheelDelta = static_cast(wheelDelta); event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); m_pendingInputEvents.push_back(event); @@ -403,209 +499,6 @@ void Application::QueueWindowFocusEvent(UIInputEventType type) { m_pendingInputEvents.push_back(event); } -bool Application::LoadStructuredScreen(const char* triggerReason) { - (void)triggerReason; - m_screenAsset = m_structuredShell.screenAsset; - const EditorShellAssetValidationResult& shellAssetValidation = - m_structuredShell.assetValidation; - const auto shortcutValidation = m_structuredShell.shortcutManager.ValidateConfiguration(); - const bool hasAuthoredScreenDocument = !m_screenAsset.documentPath.empty(); - const bool loaded = - hasAuthoredScreenDocument ? m_screenPlayer.Load(m_screenAsset) : shellAssetValidation.IsValid(); - m_useStructuredScreen = hasAuthoredScreenDocument && loaded; - m_runtimeStatus = - hasAuthoredScreenDocument - ? (loaded ? "XCUI Editor Shell" : "Editor Shell | Load Error") - : "XCUI Editor Shell | Code-driven"; - m_runtimeError.clear(); - if (hasAuthoredScreenDocument && !loaded) { - AppendErrorMessage(m_runtimeError, m_screenPlayer.GetLastError()); - } - if (!shellAssetValidation.IsValid()) { - AppendErrorMessage( - m_runtimeError, - "Editor shell asset invalid: " + shellAssetValidation.message); - } - if (!shortcutValidation.IsValid()) { - AppendErrorMessage( - m_runtimeError, - "Structured shell shortcut manager invalid: " + shortcutValidation.message); - } - RebuildTrackedFileStates(); - return loaded; -} - -void Application::RefreshStructuredScreen() { - const auto now = std::chrono::steady_clock::now(); - if (m_lastReloadPollTime.time_since_epoch().count() != 0 && - now - m_lastReloadPollTime < kReloadPollInterval) { - return; - } - - m_lastReloadPollTime = now; - if (DetectTrackedFileChange()) { - LoadStructuredScreen("reload"); - } -} - -void Application::RebuildTrackedFileStates() { - namespace fs = std::filesystem; - - m_trackedFiles.clear(); - std::unordered_set seenPaths = {}; - std::error_code errorCode = {}; - - auto appendTrackedPath = [&](const std::string& rawPath) { - if (rawPath.empty()) { - return; - } - - const fs::path normalizedPath = fs::path(rawPath).lexically_normal(); - const std::string key = normalizedPath.string(); - if (!seenPaths.insert(key).second) { - return; - } - - TrackedFileState state = {}; - state.path = normalizedPath; - state.exists = fs::exists(normalizedPath, errorCode); - errorCode.clear(); - if (state.exists) { - state.writeTime = fs::last_write_time(normalizedPath, errorCode); - errorCode.clear(); - } - m_trackedFiles.push_back(std::move(state)); - }; - - appendTrackedPath(m_screenAsset.documentPath); - - if (const auto* document = m_screenPlayer.GetDocument(); document != nullptr) { - for (const std::string& dependency : document->dependencies) { - appendTrackedPath(dependency); - } - } -} - -bool Application::DetectTrackedFileChange() const { - namespace fs = std::filesystem; - - std::error_code errorCode = {}; - for (const TrackedFileState& trackedFile : m_trackedFiles) { - const bool existsNow = fs::exists(trackedFile.path, errorCode); - errorCode.clear(); - if (existsNow != trackedFile.exists) { - return true; - } - - if (!existsNow) { - continue; - } - - const auto writeTimeNow = fs::last_write_time(trackedFile.path, errorCode); - errorCode.clear(); - if (writeTimeNow != trackedFile.writeTime) { - return true; - } - } - - return false; -} - -void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const { - const bool authoredMode = - !m_screenAsset.documentPath.empty() && m_useStructuredScreen && m_screenPlayer.IsLoaded(); - const float panelWidth = authoredMode ? 430.0f : 380.0f; - std::vector detailLines = {}; - detailLines.push_back( - authoredMode - ? "Hot reload watches editor shell document and dependencies." - : "Editor shell is composed directly from fixed code definitions."); - detailLines.push_back( - authoredMode - ? "Document: " + std::filesystem::path(m_screenAsset.documentPath).filename().string() - : "Document: (none)"); - - if (authoredMode) { - const auto& inputDebug = m_documentHost.GetInputDebugSnapshot(); - detailLines.push_back( - "Hover | Focus: " + - ExtractStateKeyTail(inputDebug.hoveredStateKey) + - " | " + - ExtractStateKeyTail(inputDebug.focusedStateKey)); - detailLines.push_back( - "Active | Capture: " + - ExtractStateKeyTail(inputDebug.activeStateKey) + - " | " + - 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 + - eventPosition); - detailLines.push_back( - "Route: " + - inputDebug.lastTargetKind + - " -> " + - ExtractStateKeyTail(inputDebug.lastTargetStateKey)); - detailLines.push_back( - "Last event result: " + - (inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult)); - } - } - - if (m_autoScreenshot.HasPendingCapture()) { - detailLines.push_back("Shot pending..."); - } else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { - detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u)); - } else { - 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 authored XCUI document is used by the editor shell host."); - } - - const float panelHeight = 38.0f + static_cast(detailLines.size()) * 18.0f; - const UIRect panelRect(width - panelWidth - 16.0f, height - panelHeight - 42.0f, panelWidth, panelHeight); - - UIDrawList& overlay = drawData.EmplaceDrawList("XCUI Editor Runtime Overlay"); - overlay.AddFilledRect(panelRect, kOverlayBgColor, 10.0f); - overlay.AddRectOutline(panelRect, kOverlayBorderColor, 1.0f, 10.0f); - overlay.AddFilledRect( - UIRect(panelRect.x + 12.0f, panelRect.y + 14.0f, 8.0f, 8.0f), - authoredMode ? kOverlaySuccess : kOverlayFallback, - 4.0f); - overlay.AddText( - UIPoint(panelRect.x + 28.0f, panelRect.y + 10.0f), - m_runtimeStatus.empty() ? "XCUI Editor Shell" : m_runtimeStatus, - kOverlayTextPrimary, - 14.0f); - - float detailY = panelRect.y + 30.0f; - for (std::size_t index = 0; index < detailLines.size(); ++index) { - const bool lastLine = index + 1u == detailLines.size(); - overlay.AddText( - UIPoint(panelRect.x + 28.0f, detailY), - detailLines[index], - lastLine && (!m_runtimeError.empty() || !m_autoScreenshot.GetLastCaptureError().empty()) - ? kOverlayFallback - : kOverlayTextMuted, - 12.0f); - detailY += 18.0f; - } -} - std::filesystem::path Application::ResolveRepoRootPath() { std::string root = XCUIEDITOR_REPO_ROOT; if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { @@ -614,6 +507,70 @@ std::filesystem::path Application::ResolveRepoRootPath() { return std::filesystem::path(root).lexically_normal(); } +UIEditorHostCommandEvaluationResult Application::EvaluateHostCommand( + std::string_view commandId) const { + UIEditorHostCommandEvaluationResult result = {}; + if (commandId == "file.exit") { + result.executable = true; + result.message = "Exit command is ready."; + return result; + } + + if (commandId == "help.about") { + result.executable = true; + result.message = "About placeholder is ready."; + return result; + } + + if (commandId.rfind("file.", 0u) == 0u) { + result.message = "Document command bridge is not attached yet."; + return result; + } + if (commandId.rfind("edit.", 0u) == 0u) { + result.message = "Edit route bridge is not attached yet."; + return result; + } + if (commandId.rfind("assets.", 0u) == 0u) { + result.message = "Asset pipeline bridge is not attached yet."; + return result; + } + if (commandId.rfind("run.", 0u) == 0u) { + result.message = "Runtime bridge is not attached yet."; + return result; + } + if (commandId.rfind("scripts.", 0u) == 0u) { + result.message = "Script pipeline bridge is not attached yet."; + return result; + } + + result.message = "Host command is not attached yet."; + return result; +} + +UIEditorHostCommandDispatchResult Application::DispatchHostCommand( + std::string_view commandId) { + UIEditorHostCommandDispatchResult result = {}; + if (commandId == "file.exit") { + result.commandExecuted = true; + result.message = "Exit requested."; + if (m_hwnd != nullptr) { + PostMessageW(m_hwnd, WM_CLOSE, 0, 0); + } + return result; + } + + if (commandId == "help.about") { + result.commandExecuted = true; + result.message = "About dialog will be wired after modal layer lands."; + m_lastStatus = "About"; + m_lastMessage = result.message; + return result; + } + + result.message = "Host command dispatch rejected."; + return result; +} + LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == WM_NCCREATE) { const auto* createStruct = reinterpret_cast(lParam); @@ -649,7 +606,11 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP application->m_trackingMouseLeave = true; } } - application->QueuePointerEvent(UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam); + application->QueuePointerEvent( + UIInputEventType::PointerMove, + UIPointerButton::None, + wParam, + lParam); return 0; } break; @@ -663,17 +624,21 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP case WM_LBUTTONDOWN: if (application != nullptr) { SetFocus(hwnd); - SetCapture(hwnd); - application->QueuePointerEvent(UIInputEventType::PointerButtonDown, UIPointerButton::Left, wParam, lParam); + application->QueuePointerEvent( + UIInputEventType::PointerButtonDown, + UIPointerButton::Left, + wParam, + lParam); return 0; } break; case WM_LBUTTONUP: if (application != nullptr) { - if (GetCapture() == hwnd) { - ReleaseCapture(); - } - application->QueuePointerEvent(UIInputEventType::PointerButtonUp, UIPointerButton::Left, wParam, lParam); + application->QueuePointerEvent( + UIInputEventType::PointerButtonUp, + UIPointerButton::Left, + wParam, + lParam); return 0; } break; @@ -697,6 +662,14 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP return 0; } break; + case WM_CAPTURECHANGED: + if (application != nullptr && + reinterpret_cast(lParam) != hwnd && + application->HasInteractiveCaptureState()) { + application->QueueWindowFocusEvent(UIInputEventType::FocusLost); + return 0; + } + break; case WM_KEYDOWN: case WM_SYSKEYDOWN: if (application != nullptr) { diff --git a/new_editor/app/Application.h b/new_editor/app/Application.h index 727ef156..ec3fcc32 100644 --- a/new_editor/app/Application.h +++ b/new_editor/app/Application.h @@ -8,76 +8,74 @@ #include #include -#include - -#include -#include +#include +#include +#include +#include #include #include -#include #include #include #include +#include #include namespace XCEngine::UI::Editor { -class Application { +class Application : public UIEditorHostCommandHandler { public: - Application(); + Application() = default; int Run(HINSTANCE hInstance, int nCmdShow); -private: - struct TrackedFileState { - std::filesystem::path path = {}; - std::filesystem::file_time_type writeTime = {}; - bool exists = false; - }; + UIEditorHostCommandEvaluationResult EvaluateHostCommand( + std::string_view commandId) const override; + UIEditorHostCommandDispatchResult DispatchHostCommand( + std::string_view commandId) override; +private: static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); bool Initialize(HINSTANCE hInstance, int nCmdShow); void Shutdown(); void RenderFrame(); void OnResize(UINT width, UINT height); - void QueuePointerEvent(::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, WPARAM wParam, LPARAM lParam); + void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result); + bool HasInteractiveCaptureState() const; + UIEditorShellInteractionDefinition BuildShellDefinition() const; + void UpdateLastStatus(const UIEditorShellInteractionResult& result); + 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 ResolveRepoRootPath(); HWND m_hwnd = nullptr; HINSTANCE m_hInstance = nullptr; ATOM m_windowClassAtom = 0; - ::XCEngine::UI::Editor::Host::NativeRenderer m_renderer; - ::XCEngine::UI::Editor::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 = {}; - StructuredEditorShellBinding m_structuredShell = {}; - UIEditorShellInteractionServices m_shellServices = {}; - 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::UI::Editor::Host::NativeRenderer m_renderer = {}; + ::XCEngine::UI::Editor::Host::AutoScreenshotController m_autoScreenshot = {}; ::XCEngine::UI::Editor::Host::InputModifierTracker m_inputModifierTracker = {}; + EditorShellAsset m_shellAsset = {}; + EditorShellAssetValidationResult m_shellValidation = {}; + UIEditorWorkspaceController m_workspaceController = {}; + UIEditorShortcutManager m_shortcutManager = {}; + UIEditorShellInteractionServices m_shellServices = {}; + UIEditorShellInteractionState m_shellInteractionState = {}; + UIEditorShellInteractionFrame m_shellFrame = {}; + std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {}; bool m_trackingMouseLeave = false; - bool m_useStructuredScreen = false; - std::string m_runtimeStatus = {}; - std::string m_runtimeError = {}; + std::string m_validationMessage = {}; + std::string m_lastStatus = {}; + std::string m_lastMessage = {}; }; int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow); diff --git a/new_editor/app/Shell/ProductShellAsset.cpp b/new_editor/app/Shell/ProductShellAsset.cpp new file mode 100644 index 00000000..4d09ff81 --- /dev/null +++ b/new_editor/app/Shell/ProductShellAsset.cpp @@ -0,0 +1,515 @@ +#include "ProductShellAsset.h" + +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIShortcutBinding; +using XCEngine::UI::UIShortcutScope; +using Widgets::UIEditorStatusBarSegment; +using Widgets::UIEditorStatusBarSlot; +using Widgets::UIEditorStatusBarTextTone; + +UIEditorPanelRegistry BuildPanelRegistry() { + UIEditorPanelRegistry registry = {}; + registry.panels = { + { "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::Placeholder, true, false, false }, + { "scene", "Scene", UIEditorPanelPresentationKind::ViewportShell, false, false, false }, + { "game", "Game", UIEditorPanelPresentationKind::ViewportShell, false, false, false }, + { "inspector", "Inspector", UIEditorPanelPresentationKind::Placeholder, true, false, false }, + { "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, false, false }, + { "project", "Project", UIEditorPanelPresentationKind::Placeholder, true, false, false } + }; + return registry; +} + +UIEditorWorkspaceModel BuildWorkspace() { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "workspace-root", + UIEditorWorkspaceSplitAxis::Vertical, + 0.75f, + BuildUIEditorWorkspaceSplit( + "workspace-top", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.15f, + BuildUIEditorWorkspacePanel( + "hierarchy-panel", + "hierarchy", + "Hierarchy", + true), + BuildUIEditorWorkspaceSplit( + "workspace-main", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.75f, + BuildUIEditorWorkspaceTabStack( + "center-tabs", + { + BuildUIEditorWorkspacePanel( + "scene-panel", + "scene", + "Scene", + false), + BuildUIEditorWorkspacePanel( + "game-panel", + "game", + "Game", + false) + }, + 0u), + BuildUIEditorWorkspacePanel( + "inspector-panel", + "inspector", + "Inspector", + true))), + BuildUIEditorWorkspaceTabStack( + "bottom-tabs", + { + BuildUIEditorWorkspacePanel( + "console-panel", + "console", + "Console", + true), + BuildUIEditorWorkspacePanel( + "project-panel", + "project", + "Project", + true) + }, + 0u)); + workspace.activePanelId = "scene"; + return workspace; +} + +UIEditorCommandDescriptor BuildHostCommand( + std::string commandId, + std::string displayName) { + UIEditorCommandDescriptor command = {}; + command.commandId = std::move(commandId); + command.displayName = std::move(displayName); + command.kind = UIEditorCommandKind::Host; + command.workspaceCommand.panelSource = UIEditorCommandPanelSource::None; + command.workspaceCommand.panelId.clear(); + return command; +} + +UIEditorCommandDescriptor BuildWorkspaceCommand( + std::string commandId, + std::string displayName, + UIEditorWorkspaceCommandKind kind, + std::string panelId = {}) { + UIEditorCommandDescriptor command = {}; + command.commandId = std::move(commandId); + command.displayName = std::move(displayName); + command.workspaceCommand.kind = kind; + if (kind == UIEditorWorkspaceCommandKind::ResetWorkspace) { + command.workspaceCommand.panelSource = UIEditorCommandPanelSource::None; + } else { + command.workspaceCommand.panelSource = UIEditorCommandPanelSource::FixedPanelId; + command.workspaceCommand.panelId = std::move(panelId); + } + return command; +} + +UIEditorCommandRegistry BuildCommandRegistry() { + UIEditorCommandRegistry registry = {}; + registry.commands = { + BuildHostCommand("file.new_project", "New Project..."), + BuildHostCommand("file.open_project", "Open Project..."), + BuildHostCommand("file.save_project", "Save Project"), + BuildHostCommand("file.new_scene", "New Scene"), + BuildHostCommand("file.open_scene", "Open Scene"), + BuildHostCommand("file.save_scene", "Save Scene"), + BuildHostCommand("file.save_scene_as", "Save Scene As..."), + BuildHostCommand("file.exit", "Exit"), + BuildHostCommand("edit.undo", "Undo"), + BuildHostCommand("edit.redo", "Redo"), + BuildHostCommand("edit.cut", "Cut"), + BuildHostCommand("edit.copy", "Copy"), + BuildHostCommand("edit.paste", "Paste"), + BuildHostCommand("edit.duplicate", "Duplicate"), + BuildHostCommand("edit.delete", "Delete"), + BuildHostCommand("edit.rename", "Rename"), + BuildHostCommand("assets.reimport_selected", "Reimport Selected Asset"), + BuildHostCommand("assets.reimport_all", "Reimport All Assets"), + BuildHostCommand("assets.clear_library", "Clear Library"), + BuildHostCommand("run.play", "Play"), + BuildHostCommand("run.pause", "Pause"), + BuildHostCommand("run.step", "Step"), + BuildHostCommand("scripts.rebuild", "Rebuild Script Assemblies"), + BuildHostCommand("help.about", "About"), + BuildWorkspaceCommand( + "view.reset_layout", + "Reset Layout", + UIEditorWorkspaceCommandKind::ResetWorkspace), + BuildWorkspaceCommand( + "view.activate_hierarchy", + "Hierarchy", + UIEditorWorkspaceCommandKind::ActivatePanel, + "hierarchy"), + BuildWorkspaceCommand( + "view.activate_scene", + "Scene", + UIEditorWorkspaceCommandKind::ActivatePanel, + "scene"), + BuildWorkspaceCommand( + "view.activate_game", + "Game", + UIEditorWorkspaceCommandKind::ActivatePanel, + "game"), + BuildWorkspaceCommand( + "view.activate_inspector", + "Inspector", + UIEditorWorkspaceCommandKind::ActivatePanel, + "inspector"), + BuildWorkspaceCommand( + "view.activate_console", + "Console", + UIEditorWorkspaceCommandKind::ActivatePanel, + "console"), + BuildWorkspaceCommand( + "view.activate_project", + "Project", + UIEditorWorkspaceCommandKind::ActivatePanel, + "project") + }; + return registry; +} + +UIEditorMenuItemDescriptor BuildCommandItem( + std::string itemId, + std::string label, + std::string commandId, + UIEditorMenuCheckedStateBinding checkedState = {}) { + UIEditorMenuItemDescriptor item = {}; + item.kind = UIEditorMenuItemKind::Command; + item.itemId = std::move(itemId); + item.label = std::move(label); + item.commandId = std::move(commandId); + item.checkedState = std::move(checkedState); + return item; +} + +UIEditorMenuItemDescriptor BuildSeparatorItem(std::string itemId) { + UIEditorMenuItemDescriptor item = {}; + item.kind = UIEditorMenuItemKind::Separator; + item.itemId = std::move(itemId); + return item; +} + +UIEditorMenuItemDescriptor BuildSubmenuItem( + std::string itemId, + std::string label, + std::vector children) { + UIEditorMenuItemDescriptor item = {}; + item.kind = UIEditorMenuItemKind::Submenu; + item.itemId = std::move(itemId); + item.label = std::move(label); + item.children = std::move(children); + return item; +} + +UIEditorMenuModel BuildMenuModel() { + UIEditorMenuModel model = {}; + + UIEditorMenuDescriptor fileMenu = {}; + fileMenu.menuId = "file"; + fileMenu.label = "File"; + fileMenu.items = { + BuildCommandItem("file-new-project", "New Project...", "file.new_project"), + BuildCommandItem("file-open-project", "Open Project...", "file.open_project"), + BuildCommandItem("file-save-project", "Save Project", "file.save_project"), + BuildSeparatorItem("file-separator-project"), + BuildCommandItem("file-new-scene", "New Scene", "file.new_scene"), + BuildCommandItem("file-open-scene", "Open Scene", "file.open_scene"), + BuildCommandItem("file-save-scene", "Save Scene", "file.save_scene"), + BuildCommandItem("file-save-scene-as", "Save Scene As...", "file.save_scene_as"), + BuildSeparatorItem("file-separator-exit"), + BuildCommandItem("file-exit", "Exit", "file.exit") + }; + + UIEditorMenuDescriptor editMenu = {}; + editMenu.menuId = "edit"; + editMenu.label = "Edit"; + editMenu.items = { + BuildCommandItem("edit-undo", "Undo", "edit.undo"), + BuildCommandItem("edit-redo", "Redo", "edit.redo"), + BuildSeparatorItem("edit-separator-history"), + BuildCommandItem("edit-cut", "Cut", "edit.cut"), + BuildCommandItem("edit-copy", "Copy", "edit.copy"), + BuildCommandItem("edit-paste", "Paste", "edit.paste"), + BuildCommandItem("edit-duplicate", "Duplicate", "edit.duplicate"), + BuildCommandItem("edit-delete", "Delete", "edit.delete"), + BuildCommandItem("edit-rename", "Rename", "edit.rename") + }; + + UIEditorMenuDescriptor assetsMenu = {}; + assetsMenu.menuId = "assets"; + assetsMenu.label = "Assets"; + assetsMenu.items = { + BuildCommandItem("assets-reimport-selected", "Reimport Selected Asset", "assets.reimport_selected"), + BuildCommandItem("assets-reimport-all", "Reimport All Assets", "assets.reimport_all"), + BuildSeparatorItem("assets-separator-clear"), + BuildCommandItem("assets-clear-library", "Clear Library", "assets.clear_library") + }; + + UIEditorMenuDescriptor runMenu = {}; + runMenu.menuId = "run"; + runMenu.label = "Run"; + runMenu.items = { + BuildCommandItem("run-play", "Play", "run.play"), + BuildCommandItem("run-pause", "Pause", "run.pause"), + BuildCommandItem("run-step", "Step", "run.step") + }; + + UIEditorMenuDescriptor scriptsMenu = {}; + scriptsMenu.menuId = "scripts"; + scriptsMenu.label = "Scripts"; + scriptsMenu.items = { + BuildCommandItem("scripts-rebuild", "Rebuild Script Assemblies", "scripts.rebuild") + }; + + UIEditorMenuCheckedStateBinding hierarchyActive = { + UIEditorMenuCheckedStateSource::PanelActive, + "hierarchy" + }; + UIEditorMenuCheckedStateBinding sceneActive = { + UIEditorMenuCheckedStateSource::PanelActive, + "scene" + }; + UIEditorMenuCheckedStateBinding gameActive = { + UIEditorMenuCheckedStateSource::PanelActive, + "game" + }; + UIEditorMenuCheckedStateBinding inspectorActive = { + UIEditorMenuCheckedStateSource::PanelActive, + "inspector" + }; + UIEditorMenuCheckedStateBinding consoleActive = { + UIEditorMenuCheckedStateSource::PanelActive, + "console" + }; + UIEditorMenuCheckedStateBinding projectActive = { + UIEditorMenuCheckedStateSource::PanelActive, + "project" + }; + + UIEditorMenuDescriptor viewMenu = {}; + viewMenu.menuId = "view"; + viewMenu.label = "View"; + viewMenu.items = { + BuildCommandItem("view-reset-layout", "Reset Layout", "view.reset_layout"), + BuildSeparatorItem("view-separator-panels"), + BuildSubmenuItem( + "view-panels", + "Panels", + { + BuildCommandItem("view-panel-hierarchy", "Hierarchy", "view.activate_hierarchy", hierarchyActive), + BuildCommandItem("view-panel-scene", "Scene", "view.activate_scene", sceneActive), + BuildCommandItem("view-panel-game", "Game", "view.activate_game", gameActive), + BuildCommandItem("view-panel-inspector", "Inspector", "view.activate_inspector", inspectorActive), + BuildCommandItem("view-panel-console", "Console", "view.activate_console", consoleActive), + BuildCommandItem("view-panel-project", "Project", "view.activate_project", projectActive) + }) + }; + + UIEditorMenuDescriptor helpMenu = {}; + helpMenu.menuId = "help"; + helpMenu.label = "Help"; + helpMenu.items = { + BuildCommandItem("help-about", "About", "help.about") + }; + + model.menus = { + std::move(fileMenu), + std::move(editMenu), + std::move(assetsMenu), + std::move(runMenu), + std::move(scriptsMenu), + std::move(viewMenu), + std::move(helpMenu) + }; + return model; +} + +UIShortcutBinding BuildBinding( + std::string commandId, + std::int32_t keyCode, + bool control = false, + bool shift = false, + bool alt = false) { + UIShortcutBinding binding = {}; + binding.scope = UIShortcutScope::Global; + binding.triggerEventType = UIInputEventType::KeyDown; + binding.commandId = std::move(commandId); + binding.chord.keyCode = keyCode; + binding.chord.modifiers.control = control; + binding.chord.modifiers.shift = shift; + binding.chord.modifiers.alt = alt; + return binding; +} + +std::vector BuildShortcutBindings() { + return { + BuildBinding("file.new_scene", static_cast(KeyCode::N), true), + BuildBinding("file.open_scene", static_cast(KeyCode::O), true), + BuildBinding("file.save_scene", static_cast(KeyCode::S), true), + BuildBinding("file.save_scene_as", static_cast(KeyCode::S), true, true), + BuildBinding("edit.undo", static_cast(KeyCode::Z), true), + BuildBinding("edit.redo", static_cast(KeyCode::Y), true), + BuildBinding("edit.cut", static_cast(KeyCode::X), true), + BuildBinding("edit.copy", static_cast(KeyCode::C), true), + BuildBinding("edit.paste", static_cast(KeyCode::V), true), + BuildBinding("edit.duplicate", static_cast(KeyCode::D), true), + BuildBinding("edit.delete", static_cast(KeyCode::Delete)), + BuildBinding("edit.rename", static_cast(KeyCode::F2)), + BuildBinding("run.play", static_cast(KeyCode::F5)), + BuildBinding("run.pause", static_cast(KeyCode::F6)), + BuildBinding("run.step", static_cast(KeyCode::F7)), + BuildBinding("file.exit", static_cast(KeyCode::F4), false, false, true) + }; +} + +UIEditorStatusBarSegment BuildStatusSegment( + std::string segmentId, + std::string label, + UIEditorStatusBarSlot slot, + UIEditorStatusBarTextTone tone, + float desiredWidth, + bool showSeparator) { + UIEditorStatusBarSegment segment = {}; + segment.segmentId = std::move(segmentId); + segment.label = std::move(label); + segment.slot = slot; + segment.tone = tone; + segment.interactive = false; + segment.desiredWidth = desiredWidth; + segment.showSeparator = showSeparator; + return segment; +} + +UIEditorShellToolbarButton BuildToolbarButton( + std::string buttonId, + UIEditorShellToolbarGlyph glyph) { + UIEditorShellToolbarButton button = {}; + button.buttonId = std::move(buttonId); + button.glyph = glyph; + button.enabled = true; + return button; +} + +UIEditorWorkspacePanelPresentationModel BuildPlaceholderPresentation( + std::string panelId) { + UIEditorWorkspacePanelPresentationModel presentation = {}; + presentation.panelId = std::move(panelId); + presentation.kind = UIEditorPanelPresentationKind::Placeholder; + return presentation; +} + +UIEditorWorkspacePanelPresentationModel BuildViewportPresentation( + std::string panelId, + std::string_view title, + std::string_view subtitle, + std::string statusText) { + UIEditorWorkspacePanelPresentationModel presentation = {}; + presentation.panelId = std::move(panelId); + presentation.kind = UIEditorPanelPresentationKind::ViewportShell; + presentation.viewportShellModel.spec.chrome.title = title; + presentation.viewportShellModel.spec.chrome.subtitle = subtitle; + presentation.viewportShellModel.spec.chrome.showTopBar = true; + presentation.viewportShellModel.spec.chrome.showBottomBar = true; + presentation.viewportShellModel.spec.statusSegments = { + BuildStatusSegment("viewport-mode", std::string(subtitle), UIEditorStatusBarSlot::Leading, UIEditorStatusBarTextTone::Primary, 112.0f, true), + BuildStatusSegment("viewport-status", std::move(statusText), UIEditorStatusBarSlot::Trailing, UIEditorStatusBarTextTone::Muted, 168.0f, false) + }; + presentation.viewportShellModel.frame.statusText = + presentation.viewportShellModel.spec.statusSegments.back().label; + return presentation; +} + +UIEditorShellInteractionDefinition BuildBaseShellDefinition() { + UIEditorShellInteractionDefinition definition = {}; + definition.menuModel = BuildMenuModel(); + definition.toolbarButtons = { + BuildToolbarButton("run.play", UIEditorShellToolbarGlyph::Play), + BuildToolbarButton("run.pause", UIEditorShellToolbarGlyph::Pause), + BuildToolbarButton("run.step", UIEditorShellToolbarGlyph::Step) + }; + definition.statusSegments = { + BuildStatusSegment("editor-mode", "Edit", UIEditorStatusBarSlot::Leading, UIEditorStatusBarTextTone::Primary, 84.0f, true), + BuildStatusSegment("editor-status", "Shell ready", UIEditorStatusBarSlot::Leading, UIEditorStatusBarTextTone::Muted, 192.0f, false), + BuildStatusSegment("capture", "F12 -> Screenshot", UIEditorStatusBarSlot::Trailing, UIEditorStatusBarTextTone::Muted, 168.0f, false), + BuildStatusSegment("active-panel", "Scene", UIEditorStatusBarSlot::Trailing, UIEditorStatusBarTextTone::Primary, 136.0f, true) + }; + definition.workspacePresentations = { + BuildPlaceholderPresentation("hierarchy"), + BuildViewportPresentation("scene", "Scene", "Perspective", "Viewport shell ready"), + BuildViewportPresentation("game", "Game", "Display 1", "Game preview host ready"), + BuildPlaceholderPresentation("inspector"), + BuildPlaceholderPresentation("console"), + BuildPlaceholderPresentation("project") + }; + return definition; +} + +std::string ResolvePanelTitle( + const UIEditorPanelRegistry& registry, + std::string_view panelId) { + if (const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(registry, panelId); + descriptor != nullptr) { + return descriptor->defaultTitle; + } + + return panelId.empty() ? std::string("(none)") : std::string(panelId); +} + +} // namespace + +EditorShellAsset BuildProductShellAsset(const std::filesystem::path& repoRoot) { + EditorShellAsset asset = {}; + asset.screenId = "editor.product.shell"; + asset.captureRootPath = (repoRoot / "new_editor/captures").lexically_normal(); + asset.panelRegistry = BuildPanelRegistry(); + asset.workspace = BuildWorkspace(); + asset.workspaceSession = + BuildDefaultUIEditorWorkspaceSession(asset.panelRegistry, asset.workspace); + asset.shellDefinition = BuildBaseShellDefinition(); + asset.shortcutAsset.commandRegistry = BuildCommandRegistry(); + asset.shortcutAsset.bindings = BuildShortcutBindings(); + return asset; +} + +UIEditorShellInteractionDefinition BuildProductShellInteractionDefinition( + const EditorShellAsset& asset, + const UIEditorWorkspaceController& controller, + std::string_view statusText, + std::string_view captureText) { + UIEditorShellInteractionDefinition definition = asset.shellDefinition; + const std::string activeTitle = + ResolvePanelTitle(asset.panelRegistry, controller.GetWorkspace().activePanelId); + const std::string resolvedStatus = + statusText.empty() ? std::string("Shell ready") : std::string(statusText); + const std::string resolvedCapture = + captureText.empty() ? std::string("F12 -> Screenshot") : std::string(captureText); + + for (UIEditorStatusBarSegment& segment : definition.statusSegments) { + if (segment.segmentId == "editor-status") { + segment.label = resolvedStatus; + } else if (segment.segmentId == "capture") { + segment.label = resolvedCapture; + } else if (segment.segmentId == "active-panel") { + segment.label = activeTitle; + } + } + return definition; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Shell/ProductShellAsset.h b/new_editor/app/Shell/ProductShellAsset.h new file mode 100644 index 00000000..3fa67f35 --- /dev/null +++ b/new_editor/app/Shell/ProductShellAsset.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +EditorShellAsset BuildProductShellAsset(const std::filesystem::path& repoRoot); + +UIEditorShellInteractionDefinition BuildProductShellInteractionDefinition( + const EditorShellAsset& asset, + const UIEditorWorkspaceController& controller, + std::string_view statusText, + std::string_view captureText); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/include/XCEditor/Foundation/UIEditorCommandDispatcher.h b/new_editor/include/XCEditor/Foundation/UIEditorCommandDispatcher.h index a7770722..7858f32f 100644 --- a/new_editor/include/XCEditor/Foundation/UIEditorCommandDispatcher.h +++ b/new_editor/include/XCEditor/Foundation/UIEditorCommandDispatcher.h @@ -8,11 +8,34 @@ namespace XCEngine::UI::Editor { +struct UIEditorHostCommandEvaluationResult { + bool executable = false; + std::string message = {}; +}; + +struct UIEditorHostCommandDispatchResult { + bool commandExecuted = false; + std::string message = {}; +}; + +class UIEditorHostCommandHandler { +public: + virtual ~UIEditorHostCommandHandler() = default; + + virtual UIEditorHostCommandEvaluationResult EvaluateHostCommand( + std::string_view commandId) const = 0; + + virtual UIEditorHostCommandDispatchResult DispatchHostCommand( + std::string_view commandId) = 0; +}; + enum class UIEditorCommandEvaluationCode : std::uint8_t { None = 0, InvalidCommandRegistry, UnknownCommandId, - MissingActivePanel + MissingActivePanel, + MissingHostCommandHandler, + HostCommandDisabled }; struct UIEditorCommandEvaluationResult { @@ -23,6 +46,7 @@ struct UIEditorCommandEvaluationResult { UIEditorWorkspaceCommand workspaceCommand = {}; UIEditorWorkspaceCommandResult previewResult = {}; std::string message = {}; + UIEditorCommandKind kind = UIEditorCommandKind::Workspace; [[nodiscard]] bool IsExecutable() const { return executable; @@ -42,6 +66,7 @@ struct UIEditorCommandDispatchResult { UIEditorWorkspaceCommand workspaceCommand = {}; UIEditorWorkspaceCommandResult commandResult = {}; std::string message = {}; + UIEditorCommandKind kind = UIEditorCommandKind::Workspace; }; std::string_view GetUIEditorCommandDispatchStatusName( @@ -56,6 +81,14 @@ public: return m_commandRegistry; } + void SetHostCommandHandler(UIEditorHostCommandHandler* handler) { + m_hostCommandHandler = handler; + } + + UIEditorHostCommandHandler* GetHostCommandHandler() const { + return m_hostCommandHandler; + } + UIEditorCommandRegistryValidationResult ValidateConfiguration() const; UIEditorCommandEvaluationResult Evaluate( @@ -68,6 +101,7 @@ public: private: UIEditorCommandRegistry m_commandRegistry = {}; + UIEditorHostCommandHandler* m_hostCommandHandler = nullptr; }; } // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Foundation/UIEditorCommandRegistry.h b/new_editor/include/XCEditor/Foundation/UIEditorCommandRegistry.h index e6a9ddda..e2b52846 100644 --- a/new_editor/include/XCEditor/Foundation/UIEditorCommandRegistry.h +++ b/new_editor/include/XCEditor/Foundation/UIEditorCommandRegistry.h @@ -9,6 +9,11 @@ namespace XCEngine::UI::Editor { +enum class UIEditorCommandKind : std::uint8_t { + Workspace = 0, + Host +}; + enum class UIEditorCommandPanelSource : std::uint8_t { None = 0, FixedPanelId, @@ -25,6 +30,7 @@ struct UIEditorCommandDescriptor { std::string commandId = {}; std::string displayName = {}; UIEditorWorkspaceCommandDescriptor workspaceCommand = {}; + UIEditorCommandKind kind = UIEditorCommandKind::Workspace; }; struct UIEditorCommandRegistry { @@ -52,6 +58,7 @@ struct UIEditorCommandRegistryValidationResult { }; std::string_view GetUIEditorCommandPanelSourceName(UIEditorCommandPanelSource source); +std::string_view GetUIEditorCommandKindName(UIEditorCommandKind kind); const UIEditorCommandDescriptor* FindUIEditorCommandDescriptor( const UIEditorCommandRegistry& registry, diff --git a/new_editor/include/XCEditor/Foundation/UIEditorShortcutManager.h b/new_editor/include/XCEditor/Foundation/UIEditorShortcutManager.h index 6215fa7f..157837e8 100644 --- a/new_editor/include/XCEditor/Foundation/UIEditorShortcutManager.h +++ b/new_editor/include/XCEditor/Foundation/UIEditorShortcutManager.h @@ -62,6 +62,14 @@ public: return m_commandDispatcher; } + void SetHostCommandHandler(UIEditorHostCommandHandler* handler) { + m_commandDispatcher.SetHostCommandHandler(handler); + } + + UIEditorHostCommandHandler* GetHostCommandHandler() const { + return m_commandDispatcher.GetHostCommandHandler(); + } + const UIEditorCommandRegistry& GetCommandRegistry() const { return m_commandDispatcher.GetCommandRegistry(); } diff --git a/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h b/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h index 8af84cb3..477e5f8b 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h +++ b/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h @@ -6,8 +6,55 @@ namespace XCEngine::UI::Editor { +enum class UIEditorShellToolbarGlyph : std::uint8_t { + Play = 0, + Pause, + Step +}; + +struct UIEditorShellToolbarButton { + std::string buttonId = {}; + UIEditorShellToolbarGlyph glyph = UIEditorShellToolbarGlyph::Play; + bool enabled = true; +}; + +struct UIEditorShellToolbarLayout { + ::XCEngine::UI::UIRect bounds = {}; + ::XCEngine::UI::UIRect groupRect = {}; + std::vector<::XCEngine::UI::UIRect> buttonRects = {}; +}; + +struct UIEditorShellToolbarMetrics { + float barHeight = 24.0f; + float groupPaddingX = 8.0f; + float groupPaddingY = 3.0f; + float buttonWidth = 20.0f; + float buttonHeight = 18.0f; + float buttonGap = 5.0f; + float groupCornerRounding = 6.0f; + float buttonCornerRounding = 4.0f; + float borderThickness = 1.0f; + float iconThickness = 1.35f; +}; + +struct UIEditorShellToolbarPalette { + ::XCEngine::UI::UIColor barColor = + ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); + ::XCEngine::UI::UIColor groupColor = + ::XCEngine::UI::UIColor(0.16f, 0.16f, 0.16f, 1.0f); + ::XCEngine::UI::UIColor groupBorderColor = + ::XCEngine::UI::UIColor(0.30f, 0.30f, 0.30f, 1.0f); + ::XCEngine::UI::UIColor buttonColor = + ::XCEngine::UI::UIColor(0.20f, 0.20f, 0.20f, 1.0f); + ::XCEngine::UI::UIColor buttonBorderColor = + ::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f); + ::XCEngine::UI::UIColor iconColor = + ::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f); +}; + struct UIEditorShellComposeModel { std::vector menuBarItems = {}; + std::vector toolbarButtons = {}; std::vector statusSegments = {}; std::vector workspacePresentations = {}; }; @@ -23,6 +70,7 @@ struct UIEditorShellComposeMetrics { float sectionGap = 8.0f; float surfaceCornerRounding = 10.0f; Widgets::UIEditorMenuBarMetrics menuBarMetrics = {}; + UIEditorShellToolbarMetrics toolbarMetrics = {}; Widgets::UIEditorDockHostMetrics dockHostMetrics = {}; Widgets::UIEditorViewportSlotMetrics viewportMetrics = {}; Widgets::UIEditorStatusBarMetrics statusBarMetrics = {}; @@ -34,6 +82,7 @@ struct UIEditorShellComposePalette { ::XCEngine::UI::UIColor surfaceBorderColor = ::XCEngine::UI::UIColor(0.27f, 0.27f, 0.27f, 1.0f); Widgets::UIEditorMenuBarPalette menuBarPalette = {}; + UIEditorShellToolbarPalette toolbarPalette = {}; Widgets::UIEditorDockHostPalette dockHostPalette = {}; Widgets::UIEditorViewportSlotPalette viewportPalette = {}; Widgets::UIEditorStatusBarPalette statusBarPalette = {}; @@ -43,9 +92,11 @@ struct UIEditorShellComposeLayout { ::XCEngine::UI::UIRect bounds = {}; ::XCEngine::UI::UIRect contentRect = {}; ::XCEngine::UI::UIRect menuBarRect = {}; + ::XCEngine::UI::UIRect toolbarRect = {}; ::XCEngine::UI::UIRect workspaceRect = {}; ::XCEngine::UI::UIRect statusBarRect = {}; Widgets::UIEditorMenuBarLayout menuBarLayout = {}; + UIEditorShellToolbarLayout toolbarLayout = {}; Widgets::UIEditorStatusBarLayout statusBarLayout = {}; }; @@ -62,6 +113,7 @@ struct UIEditorShellComposeFrame { UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( const ::XCEngine::UI::UIRect& bounds, const std::vector& menuBarItems, + const std::vector& toolbarButtons, const std::vector& statusSegments, const UIEditorShellComposeMetrics& metrics = {}); diff --git a/new_editor/include/XCEditor/Shell/UIEditorShellInteraction.h b/new_editor/include/XCEditor/Shell/UIEditorShellInteraction.h index a1fbe905..bed7b813 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorShellInteraction.h +++ b/new_editor/include/XCEditor/Shell/UIEditorShellInteraction.h @@ -16,12 +16,14 @@ namespace XCEngine::UI::Editor { struct UIEditorShellInteractionDefinition { UIEditorMenuModel menuModel = {}; + std::vector toolbarButtons = {}; std::vector statusSegments = {}; std::vector workspacePresentations = {}; }; struct UIEditorShellInteractionModel { UIEditorResolvedMenuModel resolvedMenuModel = {}; + std::vector toolbarButtons = {}; std::vector statusSegments = {}; std::vector workspacePresentations = {}; }; diff --git a/new_editor/src/Foundation/UIEditorCommandDispatcher.cpp b/new_editor/src/Foundation/UIEditorCommandDispatcher.cpp index 3f3085ea..e44dc1a7 100644 --- a/new_editor/src/Foundation/UIEditorCommandDispatcher.cpp +++ b/new_editor/src/Foundation/UIEditorCommandDispatcher.cpp @@ -13,7 +13,8 @@ UIEditorCommandEvaluationResult MakeEvaluationResult( std::string displayName, UIEditorWorkspaceCommand workspaceCommand, UIEditorWorkspaceCommandResult previewResult, - std::string message) { + std::string message, + UIEditorCommandKind kind) { UIEditorCommandEvaluationResult result = {}; result.code = code; result.executable = executable; @@ -22,6 +23,7 @@ UIEditorCommandEvaluationResult MakeEvaluationResult( result.workspaceCommand = std::move(workspaceCommand); result.previewResult = std::move(previewResult); result.message = std::move(message); + result.kind = kind; return result; } @@ -32,7 +34,8 @@ UIEditorCommandDispatchResult BuildDispatchResult( std::string displayName, UIEditorWorkspaceCommand workspaceCommand, UIEditorWorkspaceCommandResult commandResult, - std::string message) { + std::string message, + UIEditorCommandKind kind) { UIEditorCommandDispatchResult result = {}; result.status = status; result.commandExecuted = commandExecuted; @@ -41,6 +44,7 @@ UIEditorCommandDispatchResult BuildDispatchResult( result.workspaceCommand = std::move(workspaceCommand); result.commandResult = std::move(commandResult); result.message = std::move(message); + result.kind = kind; return result; } @@ -80,7 +84,8 @@ UIEditorCommandEvaluationResult UIEditorCommandDispatcher::Evaluate( {}, {}, {}, - "Command registry invalid: " + validation.message); + "Command registry invalid: " + validation.message, + UIEditorCommandKind::Workspace); } const UIEditorCommandDescriptor* descriptor = @@ -93,7 +98,38 @@ UIEditorCommandEvaluationResult UIEditorCommandDispatcher::Evaluate( {}, {}, {}, - "Editor command '" + std::string(commandId) + "' is not registered."); + "Editor command '" + std::string(commandId) + "' is not registered.", + UIEditorCommandKind::Workspace); + } + + if (descriptor->kind == UIEditorCommandKind::Host) { + if (m_hostCommandHandler == nullptr) { + return MakeEvaluationResult( + UIEditorCommandEvaluationCode::MissingHostCommandHandler, + false, + descriptor->commandId, + descriptor->displayName, + {}, + {}, + "Host command handler is not attached for '" + descriptor->commandId + "'.", + descriptor->kind); + } + + const UIEditorHostCommandEvaluationResult hostEvaluation = + m_hostCommandHandler->EvaluateHostCommand(descriptor->commandId); + return MakeEvaluationResult( + hostEvaluation.executable + ? UIEditorCommandEvaluationCode::None + : UIEditorCommandEvaluationCode::HostCommandDisabled, + hostEvaluation.executable, + descriptor->commandId, + descriptor->displayName, + {}, + {}, + hostEvaluation.message.empty() + ? std::string("Host command evaluated.") + : hostEvaluation.message, + descriptor->kind); } UIEditorWorkspaceCommand workspaceCommand = {}; @@ -110,13 +146,14 @@ UIEditorCommandEvaluationResult UIEditorCommandDispatcher::Evaluate( case UIEditorCommandPanelSource::ActivePanel: if (controller.GetWorkspace().activePanelId.empty()) { return MakeEvaluationResult( - UIEditorCommandEvaluationCode::MissingActivePanel, - false, - descriptor->commandId, - descriptor->displayName, - {}, - {}, - "Editor command '" + descriptor->commandId + "' requires an active panel."); + UIEditorCommandEvaluationCode::MissingActivePanel, + false, + descriptor->commandId, + descriptor->displayName, + {}, + {}, + "Editor command '" + descriptor->commandId + "' requires an active panel.", + descriptor->kind); } workspaceCommand.panelId = controller.GetWorkspace().activePanelId; break; @@ -133,7 +170,8 @@ UIEditorCommandEvaluationResult UIEditorCommandDispatcher::Evaluate( descriptor->displayName, std::move(workspaceCommand), std::move(previewResult), - "Editor command resolved."); + "Editor command resolved.", + descriptor->kind); } UIEditorCommandDispatchResult UIEditorCommandDispatcher::Dispatch( @@ -149,7 +187,38 @@ UIEditorCommandDispatchResult UIEditorCommandDispatcher::Dispatch( evaluation.displayName, evaluation.workspaceCommand, evaluation.previewResult, - evaluation.message); + evaluation.message, + evaluation.kind); + } + + if (evaluation.kind == UIEditorCommandKind::Host) { + if (m_hostCommandHandler == nullptr) { + return BuildDispatchResult( + UIEditorCommandDispatchStatus::Rejected, + false, + evaluation.commandId, + evaluation.displayName, + {}, + {}, + "Host command handler is not attached.", + evaluation.kind); + } + + const UIEditorHostCommandDispatchResult hostDispatch = + m_hostCommandHandler->DispatchHostCommand(evaluation.commandId); + return BuildDispatchResult( + hostDispatch.commandExecuted + ? UIEditorCommandDispatchStatus::Dispatched + : UIEditorCommandDispatchStatus::Rejected, + hostDispatch.commandExecuted, + evaluation.commandId, + evaluation.displayName, + {}, + {}, + hostDispatch.message.empty() + ? std::string("Host command dispatched.") + : hostDispatch.message, + evaluation.kind); } UIEditorWorkspaceCommandResult commandResult = @@ -168,7 +237,8 @@ UIEditorCommandDispatchResult UIEditorCommandDispatcher::Dispatch( std::move(commandResult), commandExecuted ? "Editor command dispatched." - : "Editor command dispatch was rejected."); + : "Editor command dispatch was rejected.", + evaluation.kind); } } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Foundation/UIEditorCommandRegistry.cpp b/new_editor/src/Foundation/UIEditorCommandRegistry.cpp index b821e442..dffe17d9 100644 --- a/new_editor/src/Foundation/UIEditorCommandRegistry.cpp +++ b/new_editor/src/Foundation/UIEditorCommandRegistry.cpp @@ -46,6 +46,17 @@ std::string_view GetUIEditorCommandPanelSourceName(UIEditorCommandPanelSource so return "Unknown"; } +std::string_view GetUIEditorCommandKindName(UIEditorCommandKind kind) { + switch (kind) { + case UIEditorCommandKind::Workspace: + return "Workspace"; + case UIEditorCommandKind::Host: + return "Host"; + } + + return "Unknown"; +} + const UIEditorCommandDescriptor* FindUIEditorCommandDescriptor( const UIEditorCommandRegistry& registry, std::string_view commandId) { @@ -80,6 +91,17 @@ UIEditorCommandRegistryValidationResult ValidateUIEditorCommandRegistry( "Editor command id '" + command.commandId + "' is duplicated."); } + if (command.kind == UIEditorCommandKind::Host) { + if (command.workspaceCommand.panelSource != UIEditorCommandPanelSource::None || + !command.workspaceCommand.panelId.empty()) { + return MakeValidationError( + UIEditorCommandRegistryValidationCode::UnexpectedPanelSource, + "Host editor command '" + command.commandId + + "' must not define workspace panel routing."); + } + continue; + } + const bool requiresPanelId = CommandKindRequiresPanelId(command.workspaceCommand.kind); switch (command.workspaceCommand.panelSource) { diff --git a/new_editor/src/Shell/UIEditorDockHost.cpp b/new_editor/src/Shell/UIEditorDockHost.cpp index 71b609f5..d50c00d9 100644 --- a/new_editor/src/Shell/UIEditorDockHost.cpp +++ b/new_editor/src/Shell/UIEditorDockHost.cpp @@ -23,13 +23,8 @@ using ::XCEngine::UI::Layout::UITabStripMeasureItem; using ::XCEngine::UI::Layout::UILayoutAxis; using ::XCEngine::UI::Widgets::ExpandUISplitterHandleHitRect; -constexpr std::string_view kStandalonePanelPlaceholder = "DockHost standalone panel"; constexpr std::string_view kStandalonePanelActiveFooter = "Active panel"; constexpr std::string_view kStandalonePanelInactiveFooter = "Panel placeholder"; -constexpr std::string_view kStandalonePanelActiveDetail = "Active panel body is ready for composition"; -constexpr std::string_view kStandalonePanelIdleDetail = "Select the header or body to activate this panel"; -constexpr std::string_view kTabContentPlaceholder = "DockHost tab content placeholder"; -constexpr std::string_view kTabContentDetailPrefix = "Selected panel: "; struct DockMeasureResult { bool visible = false; @@ -583,35 +578,6 @@ UIColor ResolveSplitterColor(const UIEditorDockHostSplitterLayout& splitter, con return palette.splitterColor; } -void AppendPlaceholderText( - UIDrawList& drawList, - const UIRect& bodyRect, - std::string title, - std::string detailLine, - std::string extraLine, - const UIEditorDockHostPalette& palette, - const UIEditorDockHostMetrics& metrics) { - if (bodyRect.width <= 0.0f || bodyRect.height <= 0.0f) { - return; - } - - drawList.AddText( - UIPoint(bodyRect.x, bodyRect.y), - std::move(title), - palette.placeholderTitleColor, - 16.0f); - drawList.AddText( - UIPoint(bodyRect.x, bodyRect.y + metrics.placeholderLineGap), - std::move(detailLine), - palette.placeholderTextColor, - 12.0f); - drawList.AddText( - UIPoint(bodyRect.x, bodyRect.y + metrics.placeholderLineGap * 2.0f), - std::move(extraLine), - palette.placeholderMutedColor, - 12.0f); -} - } // namespace const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout( @@ -792,14 +758,6 @@ void AppendUIEditorDockHostForeground( if (UsesExternalBodyPresentation(options, panel.panelId)) { continue; } - AppendPlaceholderText( - drawList, - panel.frameLayout.bodyRect, - panel.title, - std::string(kStandalonePanelPlaceholder), - panel.active ? std::string(kStandalonePanelActiveDetail) : std::string(kStandalonePanelIdleDetail), - palette, - metrics); } const UIEditorPanelFrameMetrics tabContentFrameMetrics = @@ -830,25 +788,9 @@ void AppendUIEditorDockHostForeground( palette.panelFramePalette, tabContentFrameMetrics); - std::string selectedTitle = "(none)"; - for (const UIEditorDockHostTabItemLayout& item : tabStack.items) { - if (item.panelId == tabStack.selectedPanelId) { - selectedTitle = item.title; - break; - } - } - if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) { continue; } - AppendPlaceholderText( - drawList, - tabStack.contentFrameLayout.bodyRect, - selectedTitle, - std::string(kTabContentPlaceholder), - std::string(kTabContentDetailPrefix) + tabStack.selectedPanelId, - palette, - metrics); } } diff --git a/new_editor/src/Shell/UIEditorShellCompose.cpp b/new_editor/src/Shell/UIEditorShellCompose.cpp index 77a69eb0..e5997b97 100644 --- a/new_editor/src/Shell/UIEditorShellCompose.cpp +++ b/new_editor/src/Shell/UIEditorShellCompose.cpp @@ -6,6 +6,8 @@ namespace XCEngine::UI::Editor { namespace { +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; using Widgets::AppendUIEditorMenuBarBackground; using Widgets::AppendUIEditorMenuBarForeground; @@ -29,11 +31,170 @@ UIRect InsetRect(const UIRect& rect, float inset) { (std::max)(0.0f, rect.height - insetY * 2.0f)); } +UIEditorShellToolbarLayout BuildUIEditorShellToolbarLayout( + const UIRect& bounds, + const std::vector& buttons, + const UIEditorShellToolbarMetrics& metrics) { + UIEditorShellToolbarLayout layout = {}; + layout.bounds = bounds; + if (buttons.empty() || bounds.width <= 0.0f || bounds.height <= 0.0f) { + return layout; + } + + const float buttonWidth = ClampNonNegative(metrics.buttonWidth); + const float buttonHeight = ClampNonNegative(metrics.buttonHeight); + const float buttonGap = ClampNonNegative(metrics.buttonGap); + const float groupPaddingX = ClampNonNegative(metrics.groupPaddingX); + const float groupPaddingY = ClampNonNegative(metrics.groupPaddingY); + const float totalButtonWidth = + buttonWidth * static_cast(buttons.size()) + + buttonGap * static_cast((std::max)(std::ptrdiff_t(0), static_cast(buttons.size()) - 1)); + const float groupWidth = totalButtonWidth + groupPaddingX * 2.0f; + const float groupHeight = + (std::min)(bounds.height, buttonHeight + groupPaddingY * 2.0f); + + layout.groupRect = UIRect( + bounds.x + (std::max)(0.0f, (bounds.width - groupWidth) * 0.5f), + bounds.y + (std::max)(0.0f, (bounds.height - groupHeight) * 0.5f), + (std::min)(groupWidth, bounds.width), + groupHeight); + + const float buttonY = + layout.groupRect.y + (std::max)(0.0f, (layout.groupRect.height - buttonHeight) * 0.5f); + float buttonX = layout.groupRect.x + groupPaddingX; + layout.buttonRects.reserve(buttons.size()); + for (std::size_t index = 0; index < buttons.size(); ++index) { + layout.buttonRects.push_back(UIRect(buttonX, buttonY, buttonWidth, buttonHeight)); + buttonX += buttonWidth + buttonGap; + } + + return layout; +} + +void AppendPlayGlyph( + ::XCEngine::UI::UIDrawList& drawList, + const UIRect& rect, + const UIColor& color, + float thickness) { + const UIPoint a(rect.x + rect.width * 0.38f, rect.y + rect.height * 0.27f); + const UIPoint b(rect.x + rect.width * 0.38f, rect.y + rect.height * 0.73f); + const UIPoint c(rect.x + rect.width * 0.70f, rect.y + rect.height * 0.50f); + drawList.AddLine(a, b, color, thickness); + drawList.AddLine(a, c, color, thickness); + drawList.AddLine(b, c, color, thickness); +} + +void AppendPauseGlyph( + ::XCEngine::UI::UIDrawList& drawList, + const UIRect& rect, + const UIColor& color, + float thickness) { + const float top = rect.y + rect.height * 0.26f; + const float bottom = rect.y + rect.height * 0.74f; + const float left = rect.x + rect.width * 0.40f; + const float right = rect.x + rect.width * 0.60f; + drawList.AddLine(UIPoint(left, top), UIPoint(left, bottom), color, thickness); + drawList.AddLine(UIPoint(right, top), UIPoint(right, bottom), color, thickness); +} + +void AppendStepGlyph( + ::XCEngine::UI::UIDrawList& drawList, + const UIRect& rect, + const UIColor& color, + float thickness) { + const UIPoint a(rect.x + rect.width * 0.30f, rect.y + rect.height * 0.27f); + const UIPoint b(rect.x + rect.width * 0.30f, rect.y + rect.height * 0.73f); + const UIPoint c(rect.x + rect.width * 0.58f, rect.y + rect.height * 0.50f); + const float barX = rect.x + rect.width * 0.70f; + drawList.AddLine(a, b, color, thickness); + drawList.AddLine(a, c, color, thickness); + drawList.AddLine(b, c, color, thickness); + drawList.AddLine( + UIPoint(barX, rect.y + rect.height * 0.25f), + UIPoint(barX, rect.y + rect.height * 0.75f), + color, + thickness); +} + +void AppendToolbarGlyph( + ::XCEngine::UI::UIDrawList& drawList, + const UIRect& rect, + UIEditorShellToolbarGlyph glyph, + const UIColor& color, + float thickness) { + switch (glyph) { + case UIEditorShellToolbarGlyph::Play: + AppendPlayGlyph(drawList, rect, color, thickness); + break; + case UIEditorShellToolbarGlyph::Pause: + AppendPauseGlyph(drawList, rect, color, thickness); + break; + case UIEditorShellToolbarGlyph::Step: + AppendStepGlyph(drawList, rect, color, thickness); + break; + default: + break; + } +} + +void AppendUIEditorShellToolbar( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellToolbarLayout& layout, + const std::vector& buttons, + const UIEditorShellToolbarPalette& palette, + const UIEditorShellToolbarMetrics& metrics) { + if (layout.bounds.width <= 0.0f || layout.bounds.height <= 0.0f) { + return; + } + + drawList.AddFilledRect(layout.bounds, palette.barColor); + drawList.AddLine( + UIPoint(layout.bounds.x, layout.bounds.y + layout.bounds.height - 1.0f), + UIPoint(layout.bounds.x + layout.bounds.width, layout.bounds.y + layout.bounds.height - 1.0f), + palette.groupBorderColor, + metrics.borderThickness); + + if (buttons.empty() || layout.groupRect.width <= 0.0f || layout.groupRect.height <= 0.0f) { + return; + } + + drawList.AddFilledRect( + layout.groupRect, + palette.groupColor, + metrics.groupCornerRounding); + drawList.AddRectOutline( + layout.groupRect, + palette.groupBorderColor, + metrics.borderThickness, + metrics.groupCornerRounding); + + const std::size_t buttonCount = (std::min)(buttons.size(), layout.buttonRects.size()); + for (std::size_t index = 0; index < buttonCount; ++index) { + const UIRect& buttonRect = layout.buttonRects[index]; + drawList.AddFilledRect( + buttonRect, + palette.buttonColor, + metrics.buttonCornerRounding); + drawList.AddRectOutline( + buttonRect, + palette.buttonBorderColor, + metrics.borderThickness, + metrics.buttonCornerRounding); + AppendToolbarGlyph( + drawList, + buttonRect, + buttons[index].glyph, + palette.iconColor, + metrics.iconThickness); + } +} + } // namespace UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( const UIRect& bounds, const std::vector& menuBarItems, + const std::vector& toolbarButtons, const std::vector& statusSegments, const UIEditorShellComposeMetrics& metrics) { UIEditorShellComposeLayout layout = {}; @@ -45,17 +206,35 @@ UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( layout.contentRect = InsetRect(layout.bounds, metrics.outerPadding); const float menuBarHeight = ClampNonNegative(metrics.menuBarMetrics.barHeight); + const float toolbarHeight = ClampNonNegative(metrics.toolbarMetrics.barHeight); const float statusBarHeight = ClampNonNegative(metrics.statusBarMetrics.barHeight); const float sectionGap = ClampNonNegative(metrics.sectionGap); + const bool hasMenuBar = menuBarHeight > 0.0f && !menuBarItems.empty(); + const bool hasToolbar = toolbarHeight > 0.0f && !toolbarButtons.empty(); + const bool hasStatusBar = statusBarHeight > 0.0f && !statusSegments.empty(); + + float gapCount = 0.0f; + if (hasMenuBar) { + gapCount += 1.0f; + } + if (hasToolbar) { + gapCount += 1.0f; + } + if (hasStatusBar) { + gapCount += 1.0f; + } + const float availableHeight = layout.contentRect.height; - const float combinedBars = menuBarHeight + statusBarHeight; - const float gapBudget = combinedBars > 0.0f ? sectionGap * 2.0f : 0.0f; - const float clampedGapBudget = (std::min)(gapBudget, availableHeight); + const float consumedHeight = + (hasMenuBar ? menuBarHeight : 0.0f) + + (hasToolbar ? toolbarHeight : 0.0f) + + (hasStatusBar ? statusBarHeight : 0.0f) + + sectionGap * gapCount; const float workspaceHeight = - (std::max)(0.0f, availableHeight - combinedBars - clampedGapBudget); + (std::max)(0.0f, availableHeight - consumedHeight); float cursorY = layout.contentRect.y; - if (menuBarHeight > 0.0f) { + if (hasMenuBar) { layout.menuBarRect = UIRect( layout.contentRect.x, cursorY, @@ -64,13 +243,22 @@ UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( cursorY += menuBarHeight + sectionGap; } + if (hasToolbar) { + layout.toolbarRect = UIRect( + layout.contentRect.x, + cursorY, + layout.contentRect.width, + toolbarHeight); + cursorY += toolbarHeight + sectionGap; + } + layout.workspaceRect = UIRect( layout.contentRect.x, cursorY, layout.contentRect.width, workspaceHeight); - if (statusBarHeight > 0.0f) { + if (hasStatusBar) { layout.statusBarRect = UIRect( layout.contentRect.x, layout.contentRect.y + layout.contentRect.height - statusBarHeight, @@ -80,6 +268,8 @@ UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( layout.menuBarLayout = BuildUIEditorMenuBarLayout(layout.menuBarRect, menuBarItems, metrics.menuBarMetrics); + layout.toolbarLayout = + BuildUIEditorShellToolbarLayout(layout.toolbarRect, toolbarButtons, metrics.toolbarMetrics); layout.statusBarLayout = BuildUIEditorStatusBarLayout(layout.statusBarRect, statusSegments, metrics.statusBarMetrics); return layout; @@ -98,6 +288,7 @@ UIEditorShellComposeRequest ResolveUIEditorShellComposeRequest( request.layout = BuildUIEditorShellComposeLayout( bounds, model.menuBarItems, + model.toolbarButtons, model.statusSegments, metrics); request.workspaceRequest = ResolveUIEditorWorkspaceComposeRequest( @@ -113,6 +304,10 @@ UIEditorShellComposeRequest ResolveUIEditorShellComposeRequest( request.layout.menuBarRect, model.menuBarItems, metrics.menuBarMetrics); + request.layout.toolbarLayout = BuildUIEditorShellToolbarLayout( + request.layout.toolbarRect, + model.toolbarButtons, + metrics.toolbarMetrics); request.layout.statusBarLayout = BuildUIEditorStatusBarLayout( request.layout.statusBarRect, model.statusSegments, @@ -135,6 +330,7 @@ UIEditorShellComposeFrame UpdateUIEditorShellCompose( frame.layout = BuildUIEditorShellComposeLayout( bounds, model.menuBarItems, + model.toolbarButtons, model.statusSegments, metrics); frame.workspaceFrame = UpdateUIEditorWorkspaceCompose( @@ -152,6 +348,10 @@ UIEditorShellComposeFrame UpdateUIEditorShellCompose( frame.layout.menuBarRect, model.menuBarItems, metrics.menuBarMetrics); + frame.layout.toolbarLayout = BuildUIEditorShellToolbarLayout( + frame.layout.toolbarRect, + model.toolbarButtons, + metrics.toolbarMetrics); frame.layout.statusBarLayout = BuildUIEditorStatusBarLayout( frame.layout.statusBarRect, model.statusSegments, @@ -191,6 +391,13 @@ void AppendUIEditorShellCompose( palette.menuBarPalette, metrics.menuBarMetrics); + AppendUIEditorShellToolbar( + drawList, + frame.layout.toolbarLayout, + model.toolbarButtons, + palette.toolbarPalette, + metrics.toolbarMetrics); + AppendUIEditorWorkspaceCompose( drawList, frame.workspaceFrame, diff --git a/new_editor/src/Shell/UIEditorShellInteraction.cpp b/new_editor/src/Shell/UIEditorShellInteraction.cpp index 0890d668..7186a77a 100644 --- a/new_editor/src/Shell/UIEditorShellInteraction.cpp +++ b/new_editor/src/Shell/UIEditorShellInteraction.cpp @@ -168,6 +168,7 @@ UIEditorShellComposeModel BuildShellComposeModel( const std::vector& menuBarItems) { UIEditorShellComposeModel shellModel = {}; shellModel.menuBarItems = menuBarItems; + shellModel.toolbarButtons = model.toolbarButtons; shellModel.statusSegments = model.statusSegments; shellModel.workspacePresentations = model.workspacePresentations; return shellModel; @@ -486,6 +487,7 @@ UIEditorShellInteractionModel ResolveUIEditorShellInteractionModel( controller, services.shortcutManager); } + model.toolbarButtons = definition.toolbarButtons; model.statusSegments = definition.statusSegments; model.workspacePresentations = definition.workspacePresentations; return model;